mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-05 04:27:40 +00:00
Compare commits
705
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8567da7db5 | ||
|
|
2846fe3ae6 | ||
|
|
1d89f5f4da | ||
|
|
4ec18cf758 | ||
|
|
26e05b38ee | ||
|
|
840d08aad7 | ||
|
|
7357f87042 | ||
|
|
ac7b8fa94b | ||
|
|
e12e8f1c36 | ||
|
|
cbb5d54566 | ||
|
|
7db5cd9358 | ||
|
|
b7ec793ba6 | ||
|
|
a781e63653 | ||
|
|
c06246b798 | ||
|
|
f940a6b59e | ||
|
|
af8142990d | ||
|
|
4934f83fea | ||
|
|
a4f491fcb7 | ||
|
|
1d04b550bb | ||
|
|
27248795cb | ||
|
|
9d751bdc11 | ||
|
|
2b33f3ebdd | ||
|
|
92f793bb2b | ||
|
|
5e8469966b | ||
|
|
eb2921067b | ||
|
|
0069073d12 | ||
|
|
a52134fb63 | ||
|
|
65d67fa261 | ||
|
|
5d6325a16f | ||
|
|
a65944ed33 | ||
|
|
096b768ff6 | ||
|
|
c2e8f0fdc7 | ||
|
|
c147fbfcae | ||
|
|
6ed6d20e76 | ||
|
|
fa01072596 | ||
|
|
8a8b6814f6 | ||
|
|
e4c0f00126 | ||
|
|
0333d2d4c6 | ||
|
|
7ccac733de | ||
|
|
641aefb1a7 | ||
|
|
f7004b871a | ||
|
|
b204c68fa5 | ||
|
|
5d7bd5094b | ||
|
|
712e4fbb65 | ||
|
|
2aff80cc0e | ||
|
|
b1dde27536 | ||
|
|
e3aab8ebd1 | ||
|
|
74820b4e95 | ||
|
|
d2b983bfab | ||
|
|
c8e426ad6b | ||
|
|
3b59758425 | ||
|
|
cc1439dcc2 | ||
|
|
9d5b3af7a3 | ||
|
|
f64406277c | ||
|
|
8534c6cd5d | ||
|
|
05327d0b87 | ||
|
|
dde1572070 | ||
|
|
235664e2e3 | ||
|
|
c358b7043f | ||
|
|
7ba1d3a256 | ||
|
|
32b4c19616 | ||
|
|
f2a27ef0fd | ||
|
|
720a5e8861 | ||
|
|
398482e06b | ||
|
|
c986ff5766 | ||
|
|
aa165cd3f1 | ||
|
|
cc2281d3cb | ||
|
|
e9d4e9e184 | ||
|
|
374616c796 | ||
|
|
9f4ffa23a0 | ||
|
|
9be2697619 | ||
|
|
fd437ec015 | ||
|
|
7d0f2646f4 | ||
|
|
33839d5142 | ||
|
|
a8dd32598b | ||
|
|
c9fe10352b | ||
|
|
7ecc855387 | ||
|
|
5819a1fbb9 | ||
|
|
f14b8a9e20 | ||
|
|
1b08dba6a5 | ||
|
|
eabba8d50b | ||
|
|
889813e802 | ||
|
|
9d8e082ea2 | ||
|
|
aad41605c9 | ||
|
|
8741319915 | ||
|
|
932737cbd7 | ||
|
|
f4cfb7de99 | ||
|
|
c7a6d11d2b | ||
|
|
a57b10b985 | ||
|
|
3a50e2127c | ||
|
|
c665bb0fa7 | ||
|
|
fee567d417 | ||
|
|
7a2687b0b2 | ||
|
|
e5f6fb236b | ||
|
|
345535de4c | ||
|
|
bf6c28f74b | ||
|
|
705d68f89f | ||
|
|
150b96f736 | ||
|
|
a73a606b4b | ||
|
|
0aebf26ada | ||
|
|
43e9e1ec67 | ||
|
|
dbb9d42d2d | ||
|
|
749e856959 | ||
|
|
960d13e120 | ||
|
|
d12dbe0bf9 | ||
|
|
34ea3fd6af | ||
|
|
07226822cd | ||
|
|
a41fb99862 | ||
|
|
cc6527afc1 | ||
|
|
d95e4efa57 | ||
|
|
74b2397c96 | ||
|
|
e7db0bc1dc | ||
|
|
9019d0df1a | ||
|
|
cd99ddc46d | ||
|
|
b26265c5f0 | ||
|
|
c8da5f99cc | ||
|
|
902e19ba8d | ||
|
|
62906636ed | ||
|
|
99057f9dd2 | ||
|
|
0850420fdf | ||
|
|
4c3847ef34 | ||
|
|
bd049c0467 | ||
|
|
66ca97fa5e | ||
|
|
2a82a62ffd | ||
|
|
2f80e60c01 | ||
|
|
048e7e483b | ||
|
|
88e76b0e9b | ||
|
|
8c78d28f0d | ||
|
|
b86da168e9 | ||
|
|
104b7fb0b7 | ||
|
|
9358d5d58f | ||
|
|
9d6b560a54 | ||
|
|
f4811cbe1e | ||
|
|
678cb9b73a | ||
|
|
f35408317e | ||
|
|
3f395f9954 | ||
|
|
3f958f637d | ||
|
|
b0143f9f9c | ||
|
|
24d3e5283f | ||
|
|
285f543d46 | ||
|
|
ce6bf08b23 | ||
|
|
265f83a447 | ||
|
|
88d17655de | ||
|
|
92c380dd7a | ||
|
|
e1dfbde590 | ||
|
|
04a80916a2 | ||
|
|
4315b98aec | ||
|
|
751c6064d1 | ||
|
|
ec7b4083c0 | ||
|
|
941693a0d8 | ||
|
|
66fb8b1ed4 | ||
|
|
7d550585d0 | ||
|
|
33dde24226 | ||
|
|
1bf75db62a | ||
|
|
e4a645a5c7 | ||
|
|
5b6174085b | ||
|
|
4ffff5149f | ||
|
|
4c74e79a40 | ||
|
|
ec0d537b69 | ||
|
|
86e48f5f69 | ||
|
|
024bfbaaf3 | ||
|
|
7d8b95407f | ||
|
|
94f1f978ff | ||
|
|
4cc5d11142 | ||
|
|
c8b65bb9d8 | ||
|
|
78f5b76a71 | ||
|
|
295d4a2c05 | ||
|
|
435192e3bf | ||
|
|
9c86a7c349 | ||
|
|
0a61a6de12 | ||
|
|
1021ee5e18 | ||
|
|
bef97f787b | ||
|
|
ca06478e24 | ||
|
|
919a687936 | ||
|
|
a2551d97dc | ||
|
|
a25d2fc75e | ||
|
|
6692b5191b | ||
|
|
c65db46bad | ||
|
|
a38f611bd5 | ||
|
|
528678a31b | ||
|
|
abc146a1b6 | ||
|
|
c7ff3eeb66 | ||
|
|
a3c72bdd93 | ||
|
|
e4408fcb55 | ||
|
|
5715cd27ef | ||
|
|
253104bb87 | ||
|
|
debf67a027 | ||
|
|
535ee80d2b | ||
|
|
559460739e | ||
|
|
0d69388e6d | ||
|
|
9bf075ea22 | ||
|
|
fe9c4e9432 | ||
|
|
1e4698409b | ||
|
|
39f46a8f93 | ||
|
|
3518ab8294 | ||
|
|
c45b7d2deb | ||
|
|
f904b87172 | ||
|
|
926787ddf3 | ||
|
|
fb9491caa5 | ||
|
|
04e8d47130 | ||
|
|
3bdddce67b | ||
|
|
d5cde97e5b | ||
|
|
66ed14dead | ||
|
|
602dfad6f3 | ||
|
|
0203586cdc | ||
|
|
3e5795255d | ||
|
|
a8912d3208 | ||
|
|
fe602b075d | ||
|
|
e445cfca85 | ||
|
|
18e9de38e0 | ||
|
|
f371795ef8 | ||
|
|
e9181c2287 | ||
|
|
5437bf2d8e | ||
|
|
b972e026ca | ||
|
|
0cad22f369 | ||
|
|
2a13c1819d | ||
|
|
4799eb46ac | ||
|
|
2569e4649d | ||
|
|
a59f3edcff | ||
|
|
027240de54 | ||
|
|
73f44911ed | ||
|
|
c0e3909853 | ||
|
|
31ff3b4df0 | ||
|
|
bbee375623 | ||
|
|
12dfa8a87f | ||
|
|
c9da65117f | ||
|
|
222448e1ad | ||
|
|
e1610bcc6f | ||
|
|
7f906300e3 | ||
|
|
bf7eda8a48 | ||
|
|
2ee1d96e5c | ||
|
|
65ec12bb0d | ||
|
|
dd3fc786b5 | ||
|
|
10868858c5 | ||
|
|
3bd3ebba2c | ||
|
|
39cbbd7e90 | ||
|
|
0567f01786 | ||
|
|
fc12a35418 | ||
|
|
2580acfb07 | ||
|
|
56a6083689 | ||
|
|
29725ed704 | ||
|
|
b44df132d0 | ||
|
|
676f26be34 | ||
|
|
46034ec39f | ||
|
|
e2d21c61c2 | ||
|
|
f8e1b7a436 | ||
|
|
9447eaada9 | ||
|
|
43533c838b | ||
|
|
7306db5f8f | ||
|
|
fbd7b37c21 | ||
|
|
8d3754b42f | ||
|
|
6a556e0601 | ||
|
|
9c1b1cb063 | ||
|
|
ae7bf65383 | ||
|
|
fb1360cce2 | ||
|
|
374e52c4d1 | ||
|
|
42aeac3469 | ||
|
|
98ac57b032 | ||
|
|
3e6e7c87b5 | ||
|
|
e908eda1da | ||
|
|
4fccdb78b9 | ||
|
|
a14127867e | ||
|
|
c02aac265a | ||
|
|
c24592cbe8 | ||
|
|
ff096e12e5 | ||
|
|
5fcc1598dd | ||
|
|
9b8493b450 | ||
|
|
f1df96f1f6 | ||
|
|
f36c524fa7 | ||
|
|
9ab5542326 | ||
|
|
0187c05e65 | ||
|
|
914c354da0 | ||
|
|
a771387edd | ||
|
|
09bc14e64b | ||
|
|
58c0170fb1 | ||
|
|
5b4b3e2a71 | ||
|
|
65a5c9f4ef | ||
|
|
a2936402c2 | ||
|
|
f069ed935f | ||
|
|
dcbbd744ac | ||
|
|
bca06e3382 | ||
|
|
8723863fb0 | ||
|
|
c775fa8b2f | ||
|
|
80e7d4b3d1 | ||
|
|
e6a727eeda | ||
|
|
887e18efe6 | ||
|
|
20b87feb1d | ||
|
|
e2e9c9ba60 | ||
|
|
337eb0cb12 | ||
|
|
504a2b0fb0 | ||
|
|
16b07c6374 | ||
|
|
f5b20d4f80 | ||
|
|
ae14bf5df3 | ||
|
|
9dc85852ba | ||
|
|
f7d29f6a14 | ||
|
|
1184618731 | ||
|
|
3d15d59ce0 | ||
|
|
15fccc31d6 | ||
|
|
48f753caaf | ||
|
|
edc4673983 | ||
|
|
9e03f02296 | ||
|
|
25ea1f827b | ||
|
|
8f11a32c9d | ||
|
|
576b8c552f | ||
|
|
85db5a2394 | ||
|
|
98989d4f0a | ||
|
|
8c976cfffd | ||
|
|
0ce5422db3 | ||
|
|
d939009949 | ||
|
|
50efade5c4 | ||
|
|
682a59fc0e | ||
|
|
3d3f370b86 | ||
|
|
eebeba09db | ||
|
|
6cceb26814 | ||
|
|
a77b132fa5 | ||
|
|
86aa05f1ec | ||
|
|
953f579adb | ||
|
|
d429b3afe8 | ||
|
|
a65a4f601a | ||
|
|
546cd35681 | ||
|
|
8ecf976067 | ||
|
|
3e54dc1c00 | ||
|
|
9eb9a9da5c | ||
|
|
a74529b9ec | ||
|
|
7ccb53572a | ||
|
|
c515188a06 | ||
|
|
cdcb950ccd | ||
|
|
13ca3b8a39 | ||
|
|
34acff0fbf | ||
|
|
fa75ab4243 | ||
|
|
1589d8bdd7 | ||
|
|
a5597a1ab9 | ||
|
|
955b245e66 | ||
|
|
2c4bca4202 | ||
|
|
6c4c240e9a | ||
|
|
e21d472609 | ||
|
|
8f89312e99 | ||
|
|
ec9fdd17bf | ||
|
|
1ed550fb92 | ||
|
|
7d69b74b08 | ||
|
|
f50a29ae08 | ||
|
|
7a4226f483 | ||
|
|
4523108293 | ||
|
|
c92e63da0d | ||
|
|
33d949a4be | ||
|
|
a9ff58e4a7 | ||
|
|
b8936ace0e | ||
|
|
59ed9cd827 | ||
|
|
a0e4d2708b | ||
|
|
c0c1d9881c | ||
|
|
c1d5b7bd55 | ||
|
|
932d6e954a | ||
|
|
3e6b169b7b | ||
|
|
ec4ea28325 | ||
|
|
5157dc573b | ||
|
|
a0aefae01b | ||
|
|
46cf7d7d57 | ||
|
|
9a7b199bb9 | ||
|
|
1e03dc4af0 | ||
|
|
673d1b57ab | ||
|
|
6ae360d029 | ||
|
|
d5a29aa241 | ||
|
|
e0154de186 | ||
|
|
915083522d | ||
|
|
c6e7ac33e2 | ||
|
|
bc494a4526 | ||
|
|
1ee871cc79 | ||
|
|
b83e4c2491 | ||
|
|
c1f8e945c9 | ||
|
|
9013f83734 | ||
|
|
a46604d8f3 | ||
|
|
037de071eb | ||
|
|
2c116f9503 | ||
|
|
50681ef90b | ||
|
|
0501422c64 | ||
|
|
786318453f | ||
|
|
61662f3965 | ||
|
|
b3cfda76e5 | ||
|
|
a20f1ce28e | ||
|
|
8a292755b0 | ||
|
|
915fc6aa90 | ||
|
|
cac63f978e | ||
|
|
d5edba1fc2 | ||
|
|
2f0852cd03 | ||
|
|
a65f84305e | ||
|
|
1c0d5ea628 | ||
|
|
a0a5b93408 | ||
|
|
e8cb32a4ca | ||
|
|
706dea756c | ||
|
|
1ea8aba88e | ||
|
|
c2d6a4b751 | ||
|
|
db61bce014 | ||
|
|
a3c03f18f0 | ||
|
|
8977a0a112 | ||
|
|
09a5e3c1e7 | ||
|
|
4fa821e15f | ||
|
|
c9a383919a | ||
|
|
e8ba069000 | ||
|
|
acd2c340ea | ||
|
|
24fd6fd9b1 | ||
|
|
86bb710ca4 | ||
|
|
7148c15f98 | ||
|
|
2d7093651d | ||
|
|
874e5aac4b | ||
|
|
8051605c16 | ||
|
|
134624825f | ||
|
|
7d0fcc8415 | ||
|
|
c27b72d6bc | ||
|
|
066c0d0e14 | ||
|
|
e77b87f1f2 | ||
|
|
2dd5e1a509 | ||
|
|
ab9e97805e | ||
|
|
65c584ff7d | ||
|
|
4c76ca1fb4 | ||
|
|
094896b5f1 | ||
|
|
669dad5b2e | ||
|
|
b9902e0c49 | ||
|
|
0a955c1f15 | ||
|
|
9f649b5f5b | ||
|
|
bb992ca85d | ||
|
|
679cf4ea1f | ||
|
|
0004074ffe | ||
|
|
35e5d8df90 | ||
|
|
fa0de5f576 | ||
|
|
4162618473 | ||
|
|
284af7ca1d | ||
|
|
c6f1f7b483 | ||
|
|
6cea90d507 | ||
|
|
46608af767 | ||
|
|
24167a7902 | ||
|
|
e300c4e7e3 | ||
|
|
1a815b315f | ||
|
|
a7fed2c2d8 | ||
|
|
9e92037674 | ||
|
|
e536725731 | ||
|
|
f59370cee6 | ||
|
|
bdaf320c08 | ||
|
|
fb60d6f7fd | ||
|
|
22295eedcf | ||
|
|
21c215e9fd | ||
|
|
0061613883 | ||
|
|
aecbcc6409 | ||
|
|
642dcc9a43 | ||
|
|
2b05990a2c | ||
|
|
975084baca | ||
|
|
775cd7751a | ||
|
|
c5da503cb3 | ||
|
|
5f8a492f4f | ||
|
|
8dd5da195e | ||
|
|
61aa14cd86 | ||
|
|
86a6bac145 | ||
|
|
26616c506e | ||
|
|
f8c48ceb8a | ||
|
|
3d739c642e | ||
|
|
25b96be1a3 | ||
|
|
397ef318f8 | ||
|
|
fbb06fed05 | ||
|
|
064a203db0 | ||
|
|
5987ec7b64 | ||
|
|
d72524879b | ||
|
|
db46cea0eb | ||
|
|
8f75d6813b | ||
|
|
1f577ec82e | ||
|
|
1f801757e9 | ||
|
|
4588e58af5 | ||
|
|
6fe1bec80c | ||
|
|
a2735b1167 | ||
|
|
249b0b2bb7 | ||
|
|
62320f5fbe | ||
|
|
cebbda72ec | ||
|
|
c51033db77 | ||
|
|
f863c41d40 | ||
|
|
ab7b4ca094 | ||
|
|
179c271618 | ||
|
|
2e6dfe3bc7 | ||
|
|
122cf48d3e | ||
|
|
dea747c628 | ||
|
|
bbbc553064 | ||
|
|
43c40ae9a0 | ||
|
|
6fdb17d64a | ||
|
|
aa69d698a7 | ||
|
|
f4a32b4350 | ||
|
|
688b3a5c77 | ||
|
|
ff336cd5d9 | ||
|
|
85dae05ad3 | ||
|
|
2939620066 | ||
|
|
0b3a390cb9 | ||
|
|
3d4ffd7900 | ||
|
|
06907f9b67 | ||
|
|
7c343a2b3e | ||
|
|
b4f66e9bf3 | ||
|
|
138e078984 | ||
|
|
e28177cb9b | ||
|
|
a1f2f0cdb9 | ||
|
|
e77e558d64 | ||
|
|
7d5e5b7d97 | ||
|
|
315d7a6b59 | ||
|
|
67231fd326 | ||
|
|
f4d8abd7d9 | ||
|
|
9574376d38 | ||
|
|
f2666a6248 | ||
|
|
ee5878d641 | ||
|
|
470d1959ba | ||
|
|
f5d6f5782b | ||
|
|
5cb6bbdf7a | ||
|
|
9f0547f6af | ||
|
|
fcf94e34ce | ||
|
|
5d88fda158 | ||
|
|
7b7186e293 | ||
|
|
d83c6451c8 | ||
|
|
eaa05f107e | ||
|
|
d0fdca4dc1 | ||
|
|
37f18972e7 | ||
|
|
7471ac90c5 | ||
|
|
d826cbd138 | ||
|
|
5293a8526d | ||
|
|
74aa7b354e | ||
|
|
ffddf9a9b3 | ||
|
|
77785d14c2 | ||
|
|
f715f776a4 | ||
|
|
8a8752f80c | ||
|
|
b361b5c3e2 | ||
|
|
98c0552ee3 | ||
|
|
9a46bdf6bf | ||
|
|
3f7d2e56d7 | ||
|
|
5b638e298e | ||
|
|
6e232131b9 | ||
|
|
d9eb55fccf | ||
|
|
afb317d12a | ||
|
|
2f90b401cb | ||
|
|
28500e1667 | ||
|
|
4b8f8cf22a | ||
|
|
37fb4abd2e | ||
|
|
ec9bcba781 | ||
|
|
ed0f15959b | ||
|
|
0c90b7d95c | ||
|
|
2248b0cbbe | ||
|
|
a244a25513 | ||
|
|
f072b3eb02 | ||
|
|
6ed0bc3436 | ||
|
|
793a44855d | ||
|
|
4937d0baba | ||
|
|
29c0338c61 | ||
|
|
ed640588c5 | ||
|
|
335e1b5501 | ||
|
|
d4e29c56a3 | ||
|
|
dc08631274 | ||
|
|
0234900baf | ||
|
|
30aa3f13a0 | ||
|
|
1b170b9ff9 | ||
|
|
ed6b98ce46 | ||
|
|
f680b3af36 | ||
|
|
9ef3379ccf | ||
|
|
9081b01773 | ||
|
|
706283c16b | ||
|
|
e51813e1bb | ||
|
|
6b4e575d1f | ||
|
|
f0cca71673 | ||
|
|
aef113eacf | ||
|
|
3f8ddb0502 | ||
|
|
5b4b8cf2f6 | ||
|
|
ee1d68e7ef | ||
|
|
261aeb4fb7 | ||
|
|
5ccb6cd7ff | ||
|
|
fa7537d539 | ||
|
|
c1a6633302 | ||
|
|
e515d53aaa | ||
|
|
0ddbb0ad82 | ||
|
|
c1e5f23d72 | ||
|
|
8825eb9ac6 | ||
|
|
7957377388 | ||
|
|
712a984b96 | ||
|
|
1236c3863f | ||
|
|
85f1bfdb7f | ||
|
|
086858945b | ||
|
|
df99e6dc1f | ||
|
|
5ee5ec8873 | ||
|
|
5170170bb1 | ||
|
|
76c48044c8 | ||
|
|
ead079dc5b | ||
|
|
add6fb17ed | ||
|
|
c4079b1c32 | ||
|
|
c4fb7de972 | ||
|
|
45ead3125f | ||
|
|
2b40d1216e | ||
|
|
e555b48811 | ||
|
|
d70cc8f59a | ||
|
|
cd79144a25 | ||
|
|
b7151de126 | ||
|
|
4eea0c7910 | ||
|
|
cd7fcdbf54 | ||
|
|
3df48febf0 | ||
|
|
7f6594ffb2 | ||
|
|
3f3dab7833 | ||
|
|
7dbabfe60f | ||
|
|
c10ed40a4a | ||
|
|
0f7b600516 | ||
|
|
7189d09d73 | ||
|
|
164a72738a | ||
|
|
9ab8040e4b | ||
|
|
d3b985c5ad | ||
|
|
6ba761db1a | ||
|
|
f4826ed46c | ||
|
|
4c41ff7929 | ||
|
|
91e2f61099 | ||
|
|
011009b294 | ||
|
|
1865651a5a | ||
|
|
889de45b18 | ||
|
|
f4acafb541 | ||
|
|
5ea2f91d73 | ||
|
|
05ea1efe3d | ||
|
|
095c6f9bca | ||
|
|
45612e3e82 | ||
|
|
c13fb3a948 | ||
|
|
9bbb5b4cde | ||
|
|
608a992bca | ||
|
|
74de508a28 | ||
|
|
6b94ae4ed3 | ||
|
|
b9ce4934d8 | ||
|
|
70f41f2e6f | ||
|
|
e2b56d78b2 | ||
|
|
5caff3af5a | ||
|
|
dccdd37464 | ||
|
|
179a0ea8b8 | ||
|
|
b774f9d2ec | ||
|
|
e110448c68 | ||
|
|
3775232757 | ||
|
|
a4b8a236b0 | ||
|
|
9bc4efd0b7 | ||
|
|
815e5b1a82 | ||
|
|
212ef1a90e | ||
|
|
2f01dd0118 | ||
|
|
fe132a380b | ||
|
|
2438de2864 | ||
|
|
c2ee08d486 | ||
|
|
732acbcea1 | ||
|
|
992be7f0d1 | ||
|
|
efafbd868c | ||
|
|
b95d04ac65 | ||
|
|
6eae8f3448 | ||
|
|
06bc8b5711 | ||
|
|
e5ca2265e4 | ||
|
|
1e33ff0f1b | ||
|
|
8bb2915446 | ||
|
|
4658e8348d | ||
|
|
7250e81fb6 | ||
|
|
f17d9ab52f | ||
|
|
da6e5062a9 | ||
|
|
d9a1e0ae30 | ||
|
|
b42ea9d2db | ||
|
|
eee9fc0c1b | ||
|
|
bcc593a61f | ||
|
|
efa9da1c87 | ||
|
|
c2a855f6e9 | ||
|
|
e47ea8d9aa | ||
|
|
cdb8f86a9f | ||
|
|
17ae4dfccf | ||
|
|
691c2f8b35 | ||
|
|
a66f9c52e6 | ||
|
|
edfe11b7bd | ||
|
|
fadf71a893 | ||
|
|
3dad553090 | ||
|
|
0a3a5cfacd | ||
|
|
a88e385d8b | ||
|
|
c8729313e7 | ||
|
|
dad442a4b0 | ||
|
|
d8ac0d44a2 | ||
|
|
d22972b053 | ||
|
|
c89121ff0e | ||
|
|
386b268c81 | ||
|
|
a72433135f | ||
|
|
345c566b54 | ||
|
|
e036b35308 | ||
|
|
b7abcad04c | ||
|
|
5021b25dd5 | ||
|
|
20389310af | ||
|
|
f29b880f78 | ||
|
|
e0ac4de1ef | ||
|
|
9ad95af90e | ||
|
|
64fefa92c6 | ||
|
|
54ba31c4cb | ||
|
|
c9ee29438b | ||
|
|
89a6ad1428 | ||
|
|
5a4a89ae31 | ||
|
|
3dbe098ad9 | ||
|
|
78aeedc251 | ||
|
|
62b625475b | ||
|
|
9a28e489a1 | ||
|
|
8b4a2a5954 | ||
|
|
8af5a13979 | ||
|
|
654f313e2e | ||
|
|
ef9285f344 | ||
|
|
82e0b5c537 | ||
|
|
5c259cfcad | ||
|
|
44cf3de90a | ||
|
|
7d9ece1e18 | ||
|
|
872372a8aa | ||
|
|
50e95b1531 | ||
|
|
200882d322 | ||
|
|
40093dd705 | ||
|
|
643b205490 | ||
|
|
5110d59a9f | ||
|
|
09f893b126 | ||
|
|
4d471dc33d | ||
|
|
4516486867 |
No files matched your search
@@ -0,0 +1 @@
|
||||
*.sh text eol=lf
|
||||
@@ -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
|
||||
+15
-1
@@ -7,8 +7,22 @@ firmware
|
||||
.cache
|
||||
compile_commands.json
|
||||
.vscode
|
||||
.claude
|
||||
.agents
|
||||
/docs
|
||||
k5_eeprom.raw
|
||||
|
||||
/build
|
||||
/serialtool/__pycache__
|
||||
__pycache__/
|
||||
|
||||
.DS_Store
|
||||
AGENTS.md
|
||||
|
||||
# Overlay-app build artifacts (generated by build.sh / compile-app.sh)
|
||||
App/apps/*/*.elf
|
||||
App/apps/*/*.bin
|
||||
App/apps/*/*.map
|
||||
App/apps/*/*.app
|
||||
App/apps/*/*_app_blob.h
|
||||
App/apps/*/*_assets.h
|
||||
build/apps/
|
||||
+134
-23
@@ -5,6 +5,8 @@ target_link_libraries(App INTERFACE PY32F071_Driver)
|
||||
target_compile_definitions(App INTERFACE PRINTF_INCLUDE_CONFIG_H)
|
||||
|
||||
target_sources(App INTERFACE
|
||||
# libgcc override (signed division)
|
||||
compact_div.S
|
||||
# Drivers
|
||||
driver/backlight.c
|
||||
driver/bk4829.c
|
||||
@@ -70,6 +72,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 +81,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 +97,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,21 +119,34 @@ 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_AIRCOPY_UART)
|
||||
if(ENABLE_AIRCOPY_UART AND NOT (ENABLE_AIRCOPY AND ENABLE_UART))
|
||||
message(FATAL_ERROR "ENABLE_AIRCOPY_UART requires ENABLE_AIRCOPY and ENABLE_UART.")
|
||||
endif()
|
||||
enable_feature(ENABLE_AIRCOPY_FLASH)
|
||||
if(ENABLE_AIRCOPY_FLASH AND NOT (ENABLE_AIRCOPY_UART AND ENABLE_FEAT_F4HWN_MULTIBOOT))
|
||||
message(FATAL_ERROR "ENABLE_AIRCOPY_FLASH requires ENABLE_AIRCOPY_UART and ENABLE_FEAT_F4HWN_MULTIBOOT.")
|
||||
endif()
|
||||
enable_feature(ENABLE_NOAA)
|
||||
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,44 +166,69 @@ 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)
|
||||
enable_feature(ENABLE_SHOW_CHARGE_LEVEL)
|
||||
enable_feature(ENABLE_REVERSE_BAT_SYMBOL)
|
||||
enable_feature(ENABLE_NO_CODE_SCAN_TIMEOUT)
|
||||
enable_feature(ENABLE_AM_FIX
|
||||
am_fix.c
|
||||
)
|
||||
enable_feature(ENABLE_SQUELCH_MORE_SENSITIVE)
|
||||
enable_feature(ENABLE_FASTER_CHANNEL_SCAN)
|
||||
enable_feature(ENABLE_RSSI_BAR)
|
||||
enable_feature(ENABLE_AUDIO_BAR)
|
||||
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)
|
||||
enable_feature(ENABLE_NAVIG_LEFT_RIGHT)
|
||||
|
||||
# ---- 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
|
||||
enable_feature(ENABLE_FEAT_F4HWN_FULL_WATCH)
|
||||
if(ENABLE_FEAT_F4HWN_FULL_WATCH AND NOT ENABLE_FEAT_F4HWN)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_FULL_WATCH requires ENABLE_FEAT_F4HWN.")
|
||||
endif()
|
||||
# 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_BEAM)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_OVERLAY_INFO
|
||||
stack_usage.c
|
||||
)
|
||||
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_BEAM AND NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
message(FATAL_ERROR "Overlay Beam service requires ENABLE_FEAT_F4HWN_OVERLAY_APPS.")
|
||||
endif()
|
||||
if(ENABLE_FEAT_F4HWN_OVERLAY_INFO AND NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
message(FATAL_ERROR "Overlay system information requires ENABLE_FEAT_F4HWN_OVERLAY_APPS.")
|
||||
endif()
|
||||
enable_feature(ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
k5viewer.c
|
||||
)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_SPECTRUM)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_RX_TX_TIMER)
|
||||
@@ -197,17 +238,87 @@ 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_RESET_CHANNEL)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_AUDIO)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_RESET_VFO)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_PMR)
|
||||
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_HOT_CFG)
|
||||
if(ENABLE_FEAT_F4HWN_MULTIBOOT_HOT_CFG AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MULTIBOOT_HOT_CFG requires ENABLE_FEAT_F4HWN_MULTIBOOT.")
|
||||
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()
|
||||
|
||||
# External-flash dump / restore over serial (host: UV Studio). Restore keeps the
|
||||
# device-specific calibration sector protected. Adds the
|
||||
# 0x0738 (read) / 0x073A (sector erase) / 0x073C (write) / 0x073E (CRC32) UART
|
||||
# commands and the PY25Q16 physical-address helpers they use.
|
||||
enable_feature(ENABLE_FEAT_F4HWN_EXT_FLASH_RW)
|
||||
if(ENABLE_FEAT_F4HWN_EXT_FLASH_RW AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_EXT_FLASH_RW requires ENABLE_FEAT_F4HWN_MULTIBOOT (the 0x0738/0x073A/0x073C/0x073E UART commands live under multiboot).")
|
||||
endif()
|
||||
|
||||
enable_feature(ENABLE_FEAT_F4HWN_QRCODE)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_LOGO)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_LOGO_SAV)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_MENU_CAT)
|
||||
|
||||
if(ENABLE_FEAT_F4HWN_LOGO_SAV AND NOT ENABLE_FEAT_F4HWN_LOGO)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_LOGO_SAV requires ENABLE_FEAT_F4HWN_LOGO.")
|
||||
endif()
|
||||
|
||||
if(ENABLE_FEAT_F4HWN_MENU_CAT AND NOT ENABLE_FEAT_F4HWN)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MENU_CAT requires ENABLE_FEAT_F4HWN.")
|
||||
endif()
|
||||
|
||||
# ---- DEBUGGING ----
|
||||
|
||||
enable_feature(ENABLE_AM_FIX_SHOW_DATA)
|
||||
enable_feature(ENABLE_AGC_SHOW_DATA)
|
||||
enable_feature(ENABLE_UART_RW_BK_REGS)
|
||||
|
||||
|
||||
-397
@@ -1,397 +0,0 @@
|
||||
|
||||
/* Copyright 2023 OneOfEleven
|
||||
* https://github.com/DualTachyon
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// code to 'try' and reduce the AM demodulator saturation problem
|
||||
//
|
||||
// that is until someone works out how to properly configure the BK chip !
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "am_fix.h"
|
||||
#include "app/main.h"
|
||||
#include "board.h"
|
||||
#include "driver/bk4819.h"
|
||||
#include "external/printf/printf.h"
|
||||
#include "frequencies.h"
|
||||
#include "functions.h"
|
||||
#include "misc.h"
|
||||
#include "settings.h"
|
||||
#ifdef ENABLE_AGC_SHOW_DATA
|
||||
#include "ui/main.h"
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t reg_val;
|
||||
int8_t gain_dB;
|
||||
} __attribute__((packed)) t_gain_table;
|
||||
|
||||
// REG_10 AGC gain table
|
||||
//
|
||||
// <15:10> ???
|
||||
//
|
||||
// <9:8> = LNA Gain Short
|
||||
// 3 = 0dB < original value
|
||||
// 2 = -19dB // was -11
|
||||
// 1 = -24dB // was -16
|
||||
// 0 = -28dB // was -19
|
||||
//
|
||||
// <7:5> = LNA Gain
|
||||
// 7 = 0dB
|
||||
// 6 = -2dB
|
||||
// 5 = -4dB < original value
|
||||
// 4 = -6dB
|
||||
// 3 = -9dB
|
||||
// 2 = -14dB
|
||||
// 1 = -19dB
|
||||
// 0 = -24dB
|
||||
//
|
||||
// <4:3> = MIXER Gain
|
||||
// 3 = 0dB < original value
|
||||
// 2 = -3dB
|
||||
// 1 = -6dB
|
||||
// 0 = -8dB
|
||||
//
|
||||
// <2:0> = PGA Gain
|
||||
// 7 = 0dB
|
||||
// 6 = -3dB < original value
|
||||
// 5 = -6dB
|
||||
// 4 = -9dB
|
||||
// 3 = -15dB
|
||||
// 2 = -21dB
|
||||
// 1 = -27dB
|
||||
// 0 = -33dB
|
||||
|
||||
// front end register dB values
|
||||
//
|
||||
// these values need to be accurate for the code to properly/reliably switch
|
||||
// between table entries when adjusting the front end registers.
|
||||
//
|
||||
// these 4 tables need a measuring/calibration update
|
||||
//
|
||||
//// static const int16_t lna_short_dB[] = { -19, -16, -11, 0}; // was (but wrong)
|
||||
// static const int16_t lna_short_dB[] = { (-28), (-24), (-19), 0}; // corrected'ish
|
||||
// static const int16_t lna_dB[] = { (-24), (-19), (-14), ( -9), (-6), (-4), (-2), 0};
|
||||
// static const int16_t mixer_dB[] = { ( -8), ( -6), ( -3), 0};
|
||||
// static const int16_t pga_dB[] = { (-33), (-27), (-21), (-15), (-9), (-6), (-3), 0};
|
||||
|
||||
// lookup table is hugely easier than writing code to do the same
|
||||
//
|
||||
|
||||
#define LOOKUP_TABLE 1
|
||||
|
||||
#if LOOKUP_TABLE
|
||||
static const t_gain_table gain_table[] =
|
||||
{
|
||||
{0x03BE, -7}, // 0 .. 3 5 3 6 .. 0dB -4dB 0dB -3dB .. -7dB original
|
||||
|
||||
{0x0000,-93}, // 1 .. 0 0 0 0 .. -28dB -24dB -8dB -33dB .. -93dB
|
||||
{0x0008,-91}, // 2 .. 0 0 1 0 .. -28dB -24dB -6dB -33dB .. -91dB
|
||||
{0x0010,-88}, // 3 .. 0 0 2 0 .. -28dB -24dB -3dB -33dB .. -88dB
|
||||
{0x0001,-87}, // 4 .. 0 0 0 1 .. -28dB -24dB -8dB -27dB .. -87dB
|
||||
{0x0009,-85}, // 5 .. 0 0 1 1 .. -28dB -24dB -6dB -27dB .. -85dB
|
||||
{0x0011,-82}, // 6 .. 0 0 2 1 .. -28dB -24dB -3dB -27dB .. -82dB
|
||||
{0x0002,-81}, // 7 .. 0 0 0 2 .. -28dB -24dB -8dB -21dB .. -81dB
|
||||
{0x000A,-79}, // 8 .. 0 0 1 2 .. -28dB -24dB -6dB -21dB .. -79dB
|
||||
{0x0012,-76}, // 9 .. 0 0 2 2 .. -28dB -24dB -3dB -21dB .. -76dB
|
||||
{0x0003,-75}, // 10 .. 0 0 0 3 .. -28dB -24dB -8dB -15dB .. -75dB
|
||||
{0x000B,-73}, // 11 .. 0 0 1 3 .. -28dB -24dB -6dB -15dB .. -73dB
|
||||
{0x0013,-70}, // 12 .. 0 0 2 3 .. -28dB -24dB -3dB -15dB .. -70dB
|
||||
{0x0004,-69}, // 13 .. 0 0 0 4 .. -28dB -24dB -8dB -9dB .. -69dB
|
||||
{0x000C,-67}, // 14 .. 0 0 1 4 .. -28dB -24dB -6dB -9dB .. -67dB
|
||||
{0x000D,-64}, // 15 .. 0 0 1 5 .. -28dB -24dB -6dB -6dB .. -64dB
|
||||
{0x001C,-61}, // 16 .. 0 0 3 4 .. -28dB -24dB 0dB - 9dB .. -61dB
|
||||
{0x001D,-58}, // 17 .. 0 0 3 5 .. -28dB -24dB 0dB -6dB .. -58dB
|
||||
{0x001E,-55}, // 18 .. 0 0 3 6 .. -28dB -24dB 0dB -3dB .. -55dB
|
||||
{0x001F,-52}, // 19 .. 0 0 3 7 .. -28dB -24dB 0dB 0dB .. -52dB
|
||||
{0x003E,-50}, // 20 .. 0 1 3 6 .. -28dB -19dB 0dB -3dB .. -50dB
|
||||
{0x003F,-47}, // 21 .. 0 1 3 7 .. -28dB -19dB 0dB 0dB .. -47dB
|
||||
{0x005E,-45}, // 22 .. 0 2 3 6 .. -28dB -14dB 0dB -3dB .. -45dB
|
||||
{0x005F,-42}, // 23 .. 0 2 3 7 .. -28dB -14dB 0dB 0dB .. -42dB
|
||||
{0x007E,-40}, // 24 .. 0 3 3 6 .. -28dB -9dB 0dB -3dB .. -40dB
|
||||
{0x007F,-37}, // 25 .. 0 3 3 7 .. -28dB -9dB 0dB 0dB .. -37dB
|
||||
{0x009F,-34}, // 26 .. 0 4 3 7 .. -28dB -6dB 0dB 0dB .. -34dB
|
||||
{0x00BF,-32}, // 27 .. 0 5 3 7 .. -28dB -4dB 0dB 0dB .. -32dB
|
||||
{0x00DF,-30}, // 28 .. 0 6 3 7 .. -28dB -2dB 0dB 0dB .. -30dB
|
||||
{0x00FF,-28}, // 29 .. 0 7 3 7 .. -28dB 0dB 0dB 0dB .. -28dB
|
||||
{0x01DF,-26}, // 30 .. 1 6 3 7 .. -24dB -2dB 0dB 0dB .. -26dB
|
||||
{0x01FF,-24}, // 31 .. 1 7 3 7 .. -24dB 0dB 0dB 0dB .. -24dB
|
||||
{0x02BF,-23}, // 32 .. 2 5 3 7 .. -19dB -4dB 0dB 0dB .. -23dB
|
||||
{0x02DF,-21}, // 33 .. 2 6 3 7 .. -19dB -2dB 0dB -0dB .. -21dB
|
||||
{0x02FF,-19}, // 34 .. 2 7 3 7 .. -19dB 0dB 0dB 0dB .. -19dB
|
||||
{0x035E,-17}, // 35 .. 3 2 3 6 .. 0dB -14dB 0dB -3dB .. -17dB
|
||||
{0x035F,-14}, // 36 .. 3 2 3 7 .. 0dB -14dB 0dB 0dB .. -14dB
|
||||
{0x037E,-12}, // 37 .. 3 3 3 6 .. 0dB -9dB 0dB -3dB .. -12dB
|
||||
{0x037F,-9}, // 38 .. 3 3 3 7 .. 0dB -9dB 0dB 0dB .. -9dB
|
||||
{0x038F,-6}, // 39 .. 3 4 3 7 .. 0dB - 6dB 0dB 0dB .. -6dB
|
||||
{0x03BF,-4}, // 40 .. 3 5 3 7 .. 0dB -4dB 0dB 0dB .. -4dB
|
||||
{0x03DF,-2}, // 41 .. 3 6 3 7 .. 0dB - 2dB 0dB 0dB .. -2dB
|
||||
{0x03FF,0} // 42 .. 3 7 3 7 .. 0dB 0dB 0dB 0dB .. 0dB
|
||||
};
|
||||
|
||||
const uint8_t gain_table_size = ARRAY_SIZE(gain_table);
|
||||
#else
|
||||
|
||||
t_gain_table gain_table[100] = {{0x03BE, -7}}; //original
|
||||
uint8_t gain_table_size = 0;
|
||||
|
||||
void CreateTable()
|
||||
{
|
||||
typedef union {
|
||||
struct {
|
||||
uint8_t pgaIdx:3;
|
||||
uint8_t mixerIdx:2;
|
||||
uint8_t lnaIdx:3;
|
||||
uint8_t lnaSIdx:2;
|
||||
};
|
||||
uint16_t __raw;
|
||||
} GainData;
|
||||
|
||||
static const int8_t lna_short_dB[] = {-28, -24, -19, 0}; // corrected'ish
|
||||
static const int8_t lna_dB[] = {-24, -19, -14, -9, -6, -4, -2, 0};
|
||||
static const int8_t mixer_dB[] = { -8, -6, -3, 0};
|
||||
static const int8_t pga_dB[] = {-33, -27, -21, -15, -9, -6, -3, 0};
|
||||
|
||||
unsigned i;
|
||||
for (uint8_t lnaSIdx = 0; lnaSIdx < ARRAY_SIZE(lna_short_dB); lnaSIdx++) {
|
||||
for (uint8_t lnaIdx = 0; lnaIdx < ARRAY_SIZE(lna_dB); lnaIdx++) {
|
||||
for (uint8_t mixerIdx = 0; mixerIdx < ARRAY_SIZE(mixer_dB); mixerIdx++) {
|
||||
for (uint8_t pgaIdx = 0; pgaIdx < ARRAY_SIZE(pga_dB); pgaIdx++) {
|
||||
int16_t db = lna_short_dB[lnaSIdx] + lna_dB[lnaIdx] + mixer_dB[mixerIdx] + pga_dB[pgaIdx];
|
||||
GainData gainData = {{
|
||||
pgaIdx,
|
||||
mixerIdx,
|
||||
lnaIdx,
|
||||
lnaSIdx,
|
||||
}};
|
||||
|
||||
for (i = 1; i < ARRAY_SIZE(gain_table); i++) {
|
||||
t_gain_table * gain = &gain_table[i];
|
||||
if (db == gain->gain_dB)
|
||||
break;
|
||||
if (db > gain->gain_dB)
|
||||
continue;
|
||||
if (db < gain->gain_dB) {
|
||||
if(gain->gain_dB)
|
||||
memmove(gain + 1, gain, 100 - i);
|
||||
gain->gain_dB = db;
|
||||
gain->reg_val = gainData.__raw;
|
||||
break;
|
||||
}
|
||||
gain->gain_dB = db;
|
||||
gain->reg_val = gainData.__raw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gain_table_size = i+1;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
// display update rate
|
||||
static const unsigned int display_update_rate = 250 / 10; // max 250ms display update rate
|
||||
unsigned int counter = 0;
|
||||
#endif
|
||||
|
||||
unsigned int gain_table_index[2] = {0, 0};
|
||||
// used simply to detect a changed gain setting
|
||||
unsigned int gain_table_index_prev[2] = {0, 0};
|
||||
// holds the previous RSSI level .. we do an average of old + new RSSI reading
|
||||
int16_t prev_rssi[2] = {0, 0};
|
||||
// to help reduce gain hunting, peak hold count down tick
|
||||
unsigned int hold_counter[2] = {0, 0};
|
||||
// -89dBm, any higher and the AM demodulator starts to saturate/clip/distort
|
||||
const int16_t desired_rssi = (-89 + 160) * 2;
|
||||
|
||||
int8_t currentGainDiff;
|
||||
bool enabled = true;
|
||||
|
||||
void AM_fix_init(void)
|
||||
{ // called at boot-up
|
||||
for (int i = 0; i < 2; i++) {
|
||||
gain_table_index[i] = 0; // re-start with original QS setting
|
||||
}
|
||||
#if !LOOKUP_TABLE
|
||||
CreateTable();
|
||||
#endif
|
||||
}
|
||||
|
||||
void AM_fix_reset(const unsigned vfo)
|
||||
{ // reset the AM fixer upper
|
||||
if (vfo > 1)
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
counter = 0;
|
||||
#endif
|
||||
|
||||
prev_rssi[vfo] = 0;
|
||||
hold_counter[vfo] = 0;
|
||||
gain_table_index_prev[vfo] = 0;
|
||||
}
|
||||
|
||||
// adjust the RX gain to try and prevent the AM demodulator from
|
||||
// saturating/overloading/clipping (distorted AM audio)
|
||||
//
|
||||
// we're actually doing the BK4819's job for it here, but as the chip
|
||||
// won't/don't do it for itself, we're left to bodging it ourself by
|
||||
// playing with the RF front end gain setting
|
||||
//
|
||||
void AM_fix_10ms(const unsigned vfo)
|
||||
{
|
||||
if(!gSetting_AM_fix || !enabled || vfo > 1 )
|
||||
return;
|
||||
|
||||
if (gCurrentFunction != FUNCTION_FOREGROUND && !FUNCTION_IsRx()) {
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
counter = display_update_rate; // queue up a display update as soon as we switch to RX mode
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
if (counter > 0) {
|
||||
if (++counter >= display_update_rate) { // trigger a display update
|
||||
counter = 0;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static uint32_t lastFreq[2];
|
||||
if(gEeprom.VfoInfo[vfo].pRX->Frequency != lastFreq[vfo]) {
|
||||
lastFreq[vfo] = gEeprom.VfoInfo[vfo].pRX->Frequency;
|
||||
AM_fix_reset(vfo);
|
||||
}
|
||||
|
||||
int16_t rssi;
|
||||
{ // sample the current RSSI level
|
||||
// average it with the previous rssi (a bit of noise/spike immunity)
|
||||
const int16_t new_rssi = BK4819_GetRSSI();
|
||||
rssi = (prev_rssi[vfo] > 0) ? (prev_rssi[vfo] + new_rssi) / 2 : new_rssi;
|
||||
prev_rssi[vfo] = new_rssi;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
{
|
||||
static int16_t lastRssi;
|
||||
|
||||
if (lastRssi != rssi) { // rssi changed
|
||||
lastRssi = rssi;
|
||||
|
||||
if (counter == 0) {
|
||||
counter = 1;
|
||||
gUpdateDisplay = true; // trigger a display update
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// automatically adjust the RF RX gain
|
||||
|
||||
// update the gain hold counter
|
||||
if (hold_counter[vfo] > 0)
|
||||
hold_counter[vfo]--;
|
||||
|
||||
// dB difference between actual and desired RSSI level
|
||||
int16_t diff_dB = (rssi - desired_rssi) / 2;
|
||||
|
||||
if (diff_dB > 0) { // decrease gain
|
||||
unsigned int index = gain_table_index[vfo]; // current position we're at
|
||||
|
||||
if (diff_dB >= 10) { // jump immediately to a new gain setting
|
||||
// this greatly speeds up initial gain reduction (but reduces noise/spike immunity)
|
||||
|
||||
const int16_t desired_gain_dB = (int16_t)gain_table[index].gain_dB - diff_dB + 8; // get no closer than 8dB (bit of noise/spike immunity)
|
||||
|
||||
// scan the table to see what index to jump straight too
|
||||
while (index > 1)
|
||||
if (gain_table[--index].gain_dB <= desired_gain_dB)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{ // incrementally reduce the gain .. taking it slow improves noise/spike immunity
|
||||
if (index > 1)
|
||||
index--; // slow step-by-step gain reduction
|
||||
}
|
||||
|
||||
index = MAX(1u, index);
|
||||
|
||||
if (gain_table_index[vfo] != index)
|
||||
{
|
||||
gain_table_index[vfo] = index;
|
||||
hold_counter[vfo] = 30; // 300ms hold
|
||||
}
|
||||
}
|
||||
|
||||
if (diff_dB >= -6) // 6dB hysterisis (help reduce gain hunting)
|
||||
hold_counter[vfo] = 30; // 300ms hold
|
||||
|
||||
if (hold_counter[vfo] == 0)
|
||||
{ // hold has been released, we're free to increase gain
|
||||
const unsigned int index = gain_table_index[vfo] + 1; // move up to next gain index
|
||||
gain_table_index[vfo] = MIN(index, gain_table_size - 1u);
|
||||
}
|
||||
|
||||
|
||||
{ // apply the new settings to the front end registers
|
||||
const unsigned int index = gain_table_index[vfo];
|
||||
|
||||
// remember the new table index
|
||||
gain_table_index_prev[vfo] = index;
|
||||
currentGainDiff = gain_table[0].gain_dB - gain_table[index].gain_dB;
|
||||
BK4819_WriteRegister(BK4819_REG_13, gain_table[index].reg_val);
|
||||
#ifdef ENABLE_AGC_SHOW_DATA
|
||||
UI_MAIN_PrintAGC(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
if (counter == 0) {
|
||||
counter = 1;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
void AM_fix_print_data(const unsigned vfo, char *s) {
|
||||
if (s != NULL && vfo < ARRAY_SIZE(gain_table_index)) {
|
||||
const unsigned int index = gain_table_index[vfo];
|
||||
sprintf(s, "%2u %4ddB %3u", index, gain_table[index].gain_dB, prev_rssi[vfo]);
|
||||
counter = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int8_t AM_fix_get_gain_diff()
|
||||
{
|
||||
return currentGainDiff;
|
||||
}
|
||||
|
||||
void AM_fix_enable(bool on)
|
||||
{
|
||||
enabled = on;
|
||||
}
|
||||
#endif
|
||||
+388
-203
@@ -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,26 +103,72 @@ 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
|
||||
// [ACTION_OPT_MUTE] = &FUNCTION_NOP,
|
||||
//#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO
|
||||
[ACTION_OPT_RXA] = &ACTION_RxA,
|
||||
#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)
|
||||
{
|
||||
@@ -174,7 +215,7 @@ void ACTION_Monitor(void)
|
||||
|
||||
RADIO_SetupRegisters(true);
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode) {
|
||||
FM_Start();
|
||||
gRequestDisplayScreen = DISPLAY_FM;
|
||||
@@ -188,7 +229,7 @@ void ACTION_Scan(bool bRestart)
|
||||
{
|
||||
(void)bRestart;
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode) {
|
||||
ACTION_Scan_FM(bRestart);
|
||||
return;
|
||||
@@ -206,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();
|
||||
|
||||
@@ -230,15 +271,15 @@ void ACTION_Scan(bool bRestart)
|
||||
}
|
||||
|
||||
// channel mode. Keep scanning but toggle between scan lists
|
||||
gEeprom.SCAN_LIST_DEFAULT = (gEeprom.SCAN_LIST_DEFAULT + 1) % 6;
|
||||
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
|
||||
@@ -281,8 +322,127 @@ 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 = SIDEFUNCTION_COUNT - 1;
|
||||
}
|
||||
else if (++*selection >= SIDEFUNCTION_COUNT) {
|
||||
*selection = 1;
|
||||
}
|
||||
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
return true;
|
||||
|
||||
case KEY_MENU:
|
||||
if (!isPressed && !isHeld) {
|
||||
const uint8_t action = *selection;
|
||||
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();
|
||||
|
||||
if (gScreenToDisplay == DISPLAY_MAIN && gDTMF_InputMode){
|
||||
// entering DTMF code
|
||||
|
||||
@@ -312,51 +472,80 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
return;
|
||||
}
|
||||
|
||||
enum ACTION_OPT_t funcShort = ACTION_OPT_NONE;
|
||||
enum ACTION_OPT_t funcLong = ACTION_OPT_NONE;
|
||||
enum ACTION_OPT_t func = ACTION_OPT_NONE;
|
||||
switch(Key) {
|
||||
case KEY_SIDE1:
|
||||
funcShort = gEeprom.KEY_1_SHORT_PRESS_ACTION;
|
||||
funcLong = gEeprom.KEY_1_LONG_PRESS_ACTION;
|
||||
if (bKeyHeld)
|
||||
func = gEeprom.KEY_1_LONG_PRESS_ACTION;
|
||||
else
|
||||
func = gEeprom.KEY_1_SHORT_PRESS_ACTION;
|
||||
break;
|
||||
case KEY_SIDE2:
|
||||
funcShort = gEeprom.KEY_2_SHORT_PRESS_ACTION;
|
||||
funcLong = gEeprom.KEY_2_LONG_PRESS_ACTION;
|
||||
if (bKeyHeld)
|
||||
func = gEeprom.KEY_2_LONG_PRESS_ACTION;
|
||||
else
|
||||
func = gEeprom.KEY_2_SHORT_PRESS_ACTION;
|
||||
break;
|
||||
case KEY_MENU:
|
||||
funcLong = gEeprom.KEY_M_LONG_PRESS_ACTION;
|
||||
if (bKeyHeld)
|
||||
func = gEeprom.KEY_M_LONG_PRESS_ACTION;
|
||||
break;
|
||||
default:
|
||||
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
|
||||
|
||||
if(!(bKeyHeld && !bKeyPressed)) // don't beep on released after hold
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
|
||||
if (bKeyHeld || bKeyPressed) // held
|
||||
{
|
||||
funcShort = funcLong;
|
||||
|
||||
if (!bKeyPressed) //ignore release if held
|
||||
return;
|
||||
}
|
||||
|
||||
// held or released after short press beyond this point
|
||||
|
||||
action_opt_table[funcShort]();
|
||||
// held or released after short press
|
||||
ACTION_Execute(func);
|
||||
}
|
||||
|
||||
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) && !defined(ENABLE_FEAT_F4HWN_BEAM)
|
||||
static void ACTION_Beam(void)
|
||||
{
|
||||
if (APP_LaunchOverlayShortcut(APP_SHORTCUT_BEAM) != APP_OK)
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
}
|
||||
#endif
|
||||
|
||||
#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;
|
||||
@@ -372,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);
|
||||
|
||||
@@ -381,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())
|
||||
@@ -422,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
|
||||
@@ -472,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)
|
||||
{
|
||||
@@ -492,21 +669,51 @@ void ACTION_Update(void)
|
||||
gUpdateStatus = true;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_FULL_WATCH
|
||||
uint8_t ACTION_GetRxMode(void)
|
||||
{
|
||||
if (gEeprom.DUAL_WATCH == DUAL_WATCH_FULL)
|
||||
return gEeprom.CROSS_BAND_RX_TX == CROSS_BAND_OFF ? 4 : 5;
|
||||
|
||||
return (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF) +
|
||||
(gEeprom.CROSS_BAND_RX_TX != CROSS_BAND_OFF) * 2;
|
||||
}
|
||||
|
||||
void ACTION_SetRxMode(uint8_t mode)
|
||||
{
|
||||
const uint8_t selected = gEeprom.TX_VFO + 1u;
|
||||
bool crossBand;
|
||||
if (mode >= 4)
|
||||
{
|
||||
gEeprom.DUAL_WATCH = DUAL_WATCH_FULL;
|
||||
crossBand = mode == 5;
|
||||
}
|
||||
else
|
||||
{
|
||||
gEeprom.DUAL_WATCH = selected * (mode & 1);
|
||||
crossBand = (mode & 2) != 0;
|
||||
}
|
||||
gEeprom.CROSS_BAND_RX_TX = crossBand ? selected : CROSS_BAND_OFF;
|
||||
}
|
||||
#endif
|
||||
|
||||
void ACTION_RxMode(void)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_FULL_WATCH
|
||||
uint8_t mode = ACTION_GetRxMode() + 1;
|
||||
ACTION_SetRxMode(mode < 6 ? mode : 0);
|
||||
#else
|
||||
static bool cycle = 0;
|
||||
|
||||
switch(cycle) {
|
||||
case 0:
|
||||
gEeprom.DUAL_WATCH = !gEeprom.DUAL_WATCH;
|
||||
cycle = 1;
|
||||
break;
|
||||
case 1:
|
||||
gEeprom.CROSS_BAND_RX_TX = !gEeprom.CROSS_BAND_RX_TX;
|
||||
cycle = 0;
|
||||
break;
|
||||
if (cycle) {
|
||||
gEeprom.CROSS_BAND_RX_TX = !gEeprom.CROSS_BAND_RX_TX;
|
||||
} else {
|
||||
gEeprom.DUAL_WATCH = !gEeprom.DUAL_WATCH;
|
||||
}
|
||||
|
||||
cycle = !cycle;
|
||||
#endif
|
||||
|
||||
ACTION_Update();
|
||||
}
|
||||
|
||||
@@ -516,82 +723,63 @@ void ACTION_MainOnly(void)
|
||||
static uint8_t dw = 0;
|
||||
static uint8_t cb = 0;
|
||||
|
||||
switch(cycle) {
|
||||
case 0:
|
||||
dw = gEeprom.DUAL_WATCH;
|
||||
cb = gEeprom.CROSS_BAND_RX_TX;
|
||||
if (cycle) {
|
||||
gEeprom.DUAL_WATCH = dw;
|
||||
gEeprom.CROSS_BAND_RX_TX = cb;
|
||||
} else {
|
||||
dw = gEeprom.DUAL_WATCH;
|
||||
cb = gEeprom.CROSS_BAND_RX_TX;
|
||||
|
||||
gEeprom.DUAL_WATCH = 0;
|
||||
gEeprom.CROSS_BAND_RX_TX = 0;
|
||||
cycle = 1;
|
||||
break;
|
||||
case 1:
|
||||
gEeprom.DUAL_WATCH = dw;
|
||||
gEeprom.CROSS_BAND_RX_TX = cb;
|
||||
cycle = 0;
|
||||
break;
|
||||
gEeprom.DUAL_WATCH = 0;
|
||||
gEeprom.CROSS_BAND_RX_TX = 0;
|
||||
}
|
||||
|
||||
cycle = !cycle;
|
||||
ACTION_Update();
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO
|
||||
void ACTION_RxA(void)
|
||||
{
|
||||
if(gRxVfo->Modulation == MODULATION_AM)
|
||||
gSetting_set_audio_am = (gSetting_set_audio_am + 1) % 3;
|
||||
else if (gRxVfo->Modulation == MODULATION_FM)
|
||||
gSetting_set_audio_fm = (gSetting_set_audio_fm + 1) % 5;
|
||||
|
||||
RADIO_SetModulation(gRxVfo->Modulation);
|
||||
}
|
||||
#endif
|
||||
|
||||
void ACTION_Ptt(void)
|
||||
{
|
||||
gSetting_set_ptt_session = !gSetting_set_ptt_session;
|
||||
|
||||
ACTION_Update();
|
||||
}
|
||||
|
||||
void ACTION_Wn(void)
|
||||
{
|
||||
const bool isRx = FUNCTION_IsRx();
|
||||
VFO_Info_t *pVfo = isRx ? gRxVfo : gTxVfo;
|
||||
|
||||
pVfo->CHANNEL_BANDWIDTH = !pVfo->CHANNEL_BANDWIDTH;
|
||||
|
||||
if (pVfo->Modulation == MODULATION_AM)
|
||||
{
|
||||
BK4819_SetFilterBandwidth(RADIO_GetAMFilterBandwidth(pVfo), true);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t bw = pVfo->CHANNEL_BANDWIDTH;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_NARROWER
|
||||
bool narrower = 0;
|
||||
if (FUNCTION_IsRx())
|
||||
if (isRx && bw == BANDWIDTH_NARROW && gSetting_set_nfm == 1)
|
||||
{
|
||||
gRxVfo->CHANNEL_BANDWIDTH = (gRxVfo->CHANNEL_BANDWIDTH == 0) ? 1: 0;
|
||||
if(gRxVfo->CHANNEL_BANDWIDTH == BANDWIDTH_NARROW && gSetting_set_nfm == 1)
|
||||
{
|
||||
narrower = 1;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
BK4819_SetFilterBandwidth(gRxVfo->CHANNEL_BANDWIDTH + narrower, true);
|
||||
#else
|
||||
BK4819_SetFilterBandwidth(gRxVfo->CHANNEL_BANDWIDTH + narrower, false);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
gTxVfo->CHANNEL_BANDWIDTH = (gTxVfo->CHANNEL_BANDWIDTH == 0) ? 1: 0;
|
||||
if(gTxVfo->CHANNEL_BANDWIDTH == BANDWIDTH_NARROW && gSetting_set_nfm == 1)
|
||||
{
|
||||
narrower = 1;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
BK4819_SetFilterBandwidth(gTxVfo->CHANNEL_BANDWIDTH, true);
|
||||
#else
|
||||
BK4819_SetFilterBandwidth(gTxVfo->CHANNEL_BANDWIDTH, false);
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
if (FUNCTION_IsRx())
|
||||
{
|
||||
gRxVfo->CHANNEL_BANDWIDTH = (gRxVfo->CHANNEL_BANDWIDTH == 0) ? 1: 0;
|
||||
#ifdef ENABLE_AM_FIX
|
||||
BK4819_SetFilterBandwidth(gRxVfo->CHANNEL_BANDWIDTH, true);
|
||||
#else
|
||||
BK4819_SetFilterBandwidth(gRxVfo->CHANNEL_BANDWIDTH, false);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
gTxVfo->CHANNEL_BANDWIDTH = (gTxVfo->CHANNEL_BANDWIDTH == 0) ? 1: 0;
|
||||
#ifdef ENABLE_AM_FIX
|
||||
BK4819_SetFilterBandwidth(gTxVfo->CHANNEL_BANDWIDTH, true);
|
||||
#else
|
||||
BK4819_SetFilterBandwidth(gTxVfo->CHANNEL_BANDWIDTH, false);
|
||||
#endif
|
||||
bw++;
|
||||
}
|
||||
#endif
|
||||
|
||||
BK4819_SetFilterBandwidth(bw, false);
|
||||
}
|
||||
|
||||
void ACTION_BackLight(void)
|
||||
@@ -627,38 +815,35 @@ void ACTION_BackLightOnDemand(void)
|
||||
BACKLIGHT_TurnOn();
|
||||
}
|
||||
|
||||
//#if !defined(ENABLE_SPECTRUM) || !defined(ENABLE_FMRADIO)
|
||||
void ACTION_Mute(void)
|
||||
{
|
||||
// Toggle mute state
|
||||
gMute = !gMute;
|
||||
void ACTION_Mute(void)
|
||||
{
|
||||
// Toggle mute state
|
||||
gMute = !gMute;
|
||||
|
||||
// Update the registers
|
||||
#ifdef ENABLE_FMRADIO
|
||||
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)
|
||||
|
||||
gUpdateStatus = true;
|
||||
}
|
||||
//#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
void ACTION_Power_High(void)
|
||||
{
|
||||
gPowerHigh = !gPowerHigh;
|
||||
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
|
||||
}
|
||||
|
||||
void ACTION_Remove_Offset(void)
|
||||
{
|
||||
gRemoveOffset = !gRemoveOffset;
|
||||
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
|
||||
}
|
||||
// Update the registers
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
BK1080_WriteRegister(BK1080_REG_05_SYSTEM_CONFIGURATION2, gMute ? 0x0A10 : 0x0A1F);
|
||||
#endif
|
||||
#endif
|
||||
gEeprom.VOLUME_GAIN = gMute ? 0 : gEeprom.VOLUME_GAIN_BACKUP;
|
||||
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)
|
||||
{
|
||||
ACTION_ToggleVfoSetting(&gPowerHigh);
|
||||
}
|
||||
|
||||
void ACTION_Remove_Offset(void)
|
||||
{
|
||||
ACTION_ToggleVfoSetting(&gRemoveOffset);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
+21
-6
@@ -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,26 +34,38 @@ 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);
|
||||
#ifdef ENABLE_FEAT_F4HWN_FULL_WATCH
|
||||
uint8_t ACTION_GetRxMode(void);
|
||||
void ACTION_SetRxMode(uint8_t mode);
|
||||
#endif
|
||||
void ACTION_MainOnly(void);
|
||||
void ACTION_Ptt(void);
|
||||
void ACTION_Wn(void);
|
||||
void ACTION_BackLightOnDemand(void);
|
||||
void ACTION_BackLight(void);
|
||||
//#if !defined(ENABLE_SPECTRUM) || !defined(ENABLE_FMRADIO)
|
||||
void ACTION_Mute(void);
|
||||
//#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO
|
||||
void ACTION_RxA(void);
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
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
|
||||
+1101
-136
File diff suppressed because it is too large.
Load diff
+87
-9
@@ -21,26 +21,104 @@
|
||||
|
||||
#include "driver/keyboard.h"
|
||||
|
||||
enum AIRCOPY_State_t
|
||||
{
|
||||
// ============================================================================
|
||||
// General definitions
|
||||
// ============================================================================
|
||||
|
||||
#define AIRCOPY_BLOCK_SIZE 0x0040u // 64 bytes per AirCopy block
|
||||
#define AIRCOPY_BLOCK_WORDS (AIRCOPY_BLOCK_SIZE / 2u) // 32 FSK words per block
|
||||
|
||||
// Every forward frame stays 100 words long. HASH carries 24 per-block CRC32
|
||||
// values and the selected map; the receiver replies with an ACK carrying a
|
||||
// 24-bit difference mask or a rejection when the selections do not match.
|
||||
// DATA retains the proven [type][header][up to 3 blocks][CRC16][END] layout,
|
||||
// with unused blocks zero-padded. Only blocks selected by the mask are sent.
|
||||
// Reverse ACK frames are eight words long. A missing ACK triggers a retry.
|
||||
//
|
||||
// This wire format is not compatible with earlier AirCopy versions. Both radios
|
||||
// must run the same firmware.
|
||||
#define AIRCOPY_BLOCKS_PER_FRAME 3u
|
||||
#define AIRCOPY_DATA_HEADER_WORDS 2u
|
||||
#define AIRCOPY_DATA_WORDS (AIRCOPY_DATA_HEADER_WORDS + AIRCOPY_BLOCKS_PER_FRAME * AIRCOPY_BLOCK_WORDS + 2u)
|
||||
#define AIRCOPY_CTRL_WORDS 8u // multiple of the 4-word RX FIFO threshold
|
||||
#define AIRCOPY_FRAME_WORDS_MAX AIRCOPY_DATA_WORDS
|
||||
#ifdef ENABLE_AIRCOPY_UART
|
||||
// Enable exactly one of the validated cable rates below:
|
||||
// #define AIRCOPY_UART_BAUD_RATE 38400u
|
||||
// #define AIRCOPY_UART_BAUD_RATE 115200u
|
||||
// #define AIRCOPY_UART_BAUD_RATE 230400u
|
||||
#define AIRCOPY_UART_BAUD_RATE 460800u
|
||||
#define AIRCOPY_UART_DEFAULT_BAUD 38400u
|
||||
#endif
|
||||
|
||||
#if AIRCOPY_DATA_WORDS > 128u
|
||||
#error AirCopy DATA frame exceeds the radio TX FIFO
|
||||
#endif
|
||||
|
||||
#define AIRCOPY_CHANNELS_PER_BANK 128
|
||||
#define AIRCOPY_NUM_BANKS MR_CHANNELS_MAX / AIRCOPY_CHANNELS_PER_BANK
|
||||
#define AIRCOPY_NUM_MAPS (AIRCOPY_NUM_BANKS + 1u) // banks + one settings map
|
||||
#define AIRCOPY_ALL_INDEX AIRCOPY_NUM_MAPS // selection sentinel: send/receive everything
|
||||
#ifdef ENABLE_AIRCOPY_FLASH
|
||||
#define AIRCOPY_FLASH_INDEX (AIRCOPY_ALL_INDEX + 1u) // full external-flash clone
|
||||
#define AIRCOPY_FLASH_SECTORS 512u // 2 MiB / 4 KiB
|
||||
#endif
|
||||
#define AIRCOPY_BANK_BLOCKS 68u
|
||||
#define AIRCOPY_SETTINGS_BLOCKS 12u
|
||||
#define AIRCOPY_ALL_BLOCKS (AIRCOPY_NUM_BANKS * AIRCOPY_BANK_BLOCKS + AIRCOPY_SETTINGS_BLOCKS)
|
||||
#define AIRCOPY_BAR_WIDTH 120 // Visible width of the progress gauge
|
||||
|
||||
// ============================================================================
|
||||
// AirCopy state
|
||||
// ============================================================================
|
||||
|
||||
typedef enum {
|
||||
AIRCOPY_READY = 0,
|
||||
AIRCOPY_TRANSFER,
|
||||
AIRCOPY_COMPLETE
|
||||
};
|
||||
AIRCOPY_COMPLETE,
|
||||
AIRCOPY_FAILED
|
||||
} AIRCOPY_State_t;
|
||||
|
||||
typedef enum AIRCOPY_State_t AIRCOPY_State_t;
|
||||
typedef enum {
|
||||
AIRCOPY_TRANSPORT_AIR = 0,
|
||||
#ifdef ENABLE_AIRCOPY_UART
|
||||
AIRCOPY_TRANSPORT_UART,
|
||||
#endif
|
||||
} AIRCOPY_Transport_t;
|
||||
|
||||
// ============================================================================
|
||||
// Globals
|
||||
// ============================================================================
|
||||
|
||||
extern AIRCOPY_State_t gAircopyState;
|
||||
extern uint16_t gAirCopyBlockNumber;
|
||||
extern uint16_t gErrorsDuringAirCopy;
|
||||
extern uint8_t gAirCopyIsSendMode;
|
||||
extern bool gAirCopyIsSendMode;
|
||||
extern bool gAircopyAll; // All mode: banks + settings in one pass
|
||||
extern AIRCOPY_Transport_t gAircopyTransport;
|
||||
|
||||
extern uint16_t g_FSK_Buffer[36];
|
||||
extern uint16_t g_FSK_Buffer[AIRCOPY_FRAME_WORDS_MAX];
|
||||
extern uint8_t gFskRxExpectedWords; // frame length the current role expects on RX
|
||||
|
||||
// ============================================================================
|
||||
// API
|
||||
// ============================================================================
|
||||
|
||||
bool AIRCOPY_SendMessage(void);
|
||||
void AIRCOPY_StorePacket(void);
|
||||
#ifdef ENABLE_AIRCOPY_UART
|
||||
void AIRCOPY_StoreUartPacket(const void *data, uint8_t words);
|
||||
#endif
|
||||
void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
|
||||
uint16_t AIRCOPY_GetTotalBlocks(void);
|
||||
bool AIRCOPY_PixelWasCopied(uint8_t col);
|
||||
uint8_t AIRCOPY_CurrentSliceMap(void); // map index of the block in progress (All slice label)
|
||||
bool AIRCOPY_UsesUart(void);
|
||||
bool AIRCOPY_IsFlash(void);
|
||||
|
||||
#endif
|
||||
// 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
|
||||
#endif // ENABLE_AIRCOPY
|
||||
#endif // APP_AIRCOPY_H
|
||||
+908
-282
File diff suppressed because it is too large.
Load diff
+12
-1
@@ -24,12 +24,23 @@
|
||||
#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);
|
||||
#ifdef ENABLE_FEAT_F4HWN_FULL_WATCH
|
||||
// Ends any priority swap; the swap state is rebuilt on the next watch cycle.
|
||||
void APP_FullWatchReset(void);
|
||||
VFO_Info_t *APP_GetFullWatchDisplayVfo(uint8_t vfo);
|
||||
// Requires Full Watch mode; initializes the background VFOs on first access.
|
||||
VFO_Info_t *const *APP_GetFullWatchBackgroundVfos(uint8_t *count);
|
||||
uint8_t APP_GetFullWatchScrollPhase(void);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+314
@@ -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, 36); // BEAM uses a fixed 36-word frame
|
||||
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
|
||||
@@ -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
|
||||
+145
-259
@@ -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:
|
||||
@@ -401,50 +299,31 @@ static void OnKeyDown(uint8_t key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Key
|
||||
static KEY_Code_t GetKey()
|
||||
{
|
||||
KEY_Code_t btn = KEYBOARD_Poll();
|
||||
if (btn == KEY_INVALID && GPIO_IsPttPressed())
|
||||
{
|
||||
btn = KEY_PTT;
|
||||
}
|
||||
return btn;
|
||||
}
|
||||
|
||||
// HandleUserInput
|
||||
static bool HandleUserInput()
|
||||
{
|
||||
// Store previous key state
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
// Parse incoming packets on every tick so serial keys are never missed,
|
||||
// regardless of whether the screen needs redrawing.
|
||||
K5VIEWER_ParseInput();
|
||||
#endif
|
||||
|
||||
kbd.prev = kbd.current;
|
||||
|
||||
// Get the current key
|
||||
kbd.current = GetKey();
|
||||
|
||||
// Detect valid key press continuation (same key still pressed)
|
||||
if (kbd.current != KEY_INVALID && kbd.current == kbd.prev)
|
||||
{
|
||||
kbd.counter = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
kbd.counter = 0;
|
||||
}
|
||||
|
||||
// Process the key if counter indicates it should be handled
|
||||
if (kbd.counter == 1)
|
||||
{
|
||||
kbd.current = KEYBOARD_GetKey();
|
||||
|
||||
if (kbd.current == KEY_INVALID)
|
||||
return true;
|
||||
|
||||
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);
|
||||
|
||||
// Special handling for MENU key
|
||||
if(kbd.current == KEY_MENU)
|
||||
{
|
||||
kbd.counter = 0;
|
||||
SYSTEM_DelayMs(250);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -452,62 +331,69 @@ static bool HandleUserInput()
|
||||
static void Tick()
|
||||
{
|
||||
HandleUserInput();
|
||||
HandleUserInput();
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Init game
|
||||
UI_DisplayClear();
|
||||
reset();
|
||||
initWall();
|
||||
initRacket();
|
||||
initBall();
|
||||
memset(gStatusLine, 0, sizeof(gStatusLine));
|
||||
isInitialized = true;
|
||||
// Finish the brightness fade if it is in progress
|
||||
BACKLIGHT_UpdateTickless();
|
||||
|
||||
while(isInitialized)
|
||||
// Init game
|
||||
UI_DisplayClear();
|
||||
reset();
|
||||
initWall();
|
||||
initRacket();
|
||||
initBall();
|
||||
UI_StatusClear();
|
||||
isInitialized = true;
|
||||
|
||||
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;
|
||||
|
||||
// For screenshot
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
getScreenShot(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
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();
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
+829
-16
@@ -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,6 +67,655 @@ 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];
|
||||
SETTINGS_ApplyChannelScanDisplayInfo(&scanFastDisplayVfo, channel, &info);
|
||||
|
||||
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) {
|
||||
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
|
||||
|
||||
void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_direction)
|
||||
{
|
||||
@@ -44,15 +724,38 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi
|
||||
gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF;
|
||||
gScanKeepResult = false;
|
||||
}
|
||||
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_FULL_WATCH
|
||||
// Scanning retunes the VFO and clears the crossband flag the swap depends on,
|
||||
// so end the swap first, as a channel change does.
|
||||
APP_FullWatchReset();
|
||||
#endif
|
||||
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))
|
||||
{ // channel mode
|
||||
{
|
||||
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;
|
||||
lastFoundFrqOrChan = initialFrqOrChan;
|
||||
@@ -78,6 +781,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)
|
||||
{
|
||||
@@ -104,6 +817,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))
|
||||
{
|
||||
@@ -121,6 +846,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;
|
||||
@@ -193,6 +927,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;
|
||||
@@ -228,11 +965,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);
|
||||
@@ -249,12 +1021,12 @@ static void NextFreqChannel(void)
|
||||
|
||||
static void NextMemChannel(void)
|
||||
{
|
||||
static unsigned int prev_mr_chan = 0;
|
||||
const bool enabled = (gEeprom.SCAN_LIST_DEFAULT > 0 && gEeprom.SCAN_LIST_DEFAULT < 4) ? gEeprom.SCAN_LIST_ENABLED[gEeprom.SCAN_LIST_DEFAULT - 1] : true;
|
||||
const int chan1 = (gEeprom.SCAN_LIST_DEFAULT > 0 && gEeprom.SCAN_LIST_DEFAULT < 4) ? gEeprom.SCANLIST_PRIORITY_CH1[gEeprom.SCAN_LIST_DEFAULT - 1] : -1;
|
||||
const int chan2 = (gEeprom.SCAN_LIST_DEFAULT > 0 && gEeprom.SCAN_LIST_DEFAULT < 4) ? gEeprom.SCANLIST_PRIORITY_CH2[gEeprom.SCAN_LIST_DEFAULT - 1] : -1;
|
||||
const unsigned int prev_chan = gNextMrChannel;
|
||||
unsigned int chan = 0;
|
||||
static uint16_t prev_mr_chan = 0;
|
||||
const bool enabled = (gEeprom.SCAN_LIST_DEFAULT > 0 && gEeprom.SCAN_LIST_DEFAULT <= SCAN_LIST_MODE_MIX) ? gEeprom.SCAN_LIST_ENABLED : true;
|
||||
const int16_t chan1 = (gEeprom.SCAN_LIST_DEFAULT > 0 && gEeprom.SCAN_LIST_DEFAULT <= SCAN_LIST_MODE_MIX && gEeprom.SCANLIST_PRIORITY_CH[0] != MR_CHANNELS_MAX) ? gEeprom.SCANLIST_PRIORITY_CH[0] : -1;
|
||||
const int16_t chan2 = (gEeprom.SCAN_LIST_DEFAULT > 0 && gEeprom.SCAN_LIST_DEFAULT <= SCAN_LIST_MODE_MIX && gEeprom.SCANLIST_PRIORITY_CH[1] != MR_CHANNELS_MAX) ? gEeprom.SCANLIST_PRIORITY_CH[1] : -1;
|
||||
const uint16_t prev_chan = gNextMrChannel;
|
||||
uint16_t chan = 0;
|
||||
|
||||
//char str[64] = "";
|
||||
|
||||
@@ -327,29 +1099,66 @@ static void NextMemChannel(void)
|
||||
case SCAN_NEXT_CHAN_MR:
|
||||
currentScanList = SCAN_NEXT_CHAN_MR;
|
||||
gNextMrChannel = prev_mr_chan;
|
||||
chan = 0xff;
|
||||
chan = 0xFFFF;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabled || chan == 0xff)
|
||||
{
|
||||
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 == 0xFF)
|
||||
{ // no valid channel found
|
||||
if (chan == 0xFFFF)
|
||||
{ // 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);
|
||||
@@ -363,7 +1172,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
|
||||
}
|
||||
+26
-1
@@ -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,16 +15,39 @@ 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);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
extern uint32_t lastFoundFrqOrChan;
|
||||
extern uint32_t lastFoundFrqOrChanOld;
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif
|
||||
+19
-4
@@ -3,6 +3,7 @@
|
||||
#include "functions.h"
|
||||
#include "misc.h"
|
||||
#include "settings.h"
|
||||
#include "ui/inputbox.h"
|
||||
#include "ui/ui.h"
|
||||
|
||||
void COMMON_KeypadLockToggle()
|
||||
@@ -29,9 +30,18 @@ void COMMON_SwitchVFOs()
|
||||
#endif
|
||||
gEeprom.TX_VFO ^= 1;
|
||||
|
||||
if (gInputBoxIndex > 0) {
|
||||
gInputBoxIndex = 0;
|
||||
gHasVfoBackup = false;
|
||||
}
|
||||
|
||||
if (gEeprom.CROSS_BAND_RX_TX != CROSS_BAND_OFF)
|
||||
gEeprom.CROSS_BAND_RX_TX = gEeprom.TX_VFO + 1;
|
||||
if (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF)
|
||||
if (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF
|
||||
#ifdef ENABLE_FEAT_F4HWN_FULL_WATCH
|
||||
&& gEeprom.DUAL_WATCH != DUAL_WATCH_FULL
|
||||
#endif
|
||||
)
|
||||
gEeprom.DUAL_WATCH = gEeprom.TX_VFO + 1;
|
||||
|
||||
gRequestSaveSettings = 1;
|
||||
@@ -49,6 +59,11 @@ void COMMON_SwitchVFOMode()
|
||||
if (gEeprom.VFO_OPEN)
|
||||
#endif
|
||||
{
|
||||
if (gInputBoxIndex > 0) {
|
||||
gInputBoxIndex = 0;
|
||||
gHasVfoBackup = false;
|
||||
}
|
||||
|
||||
if (IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE))
|
||||
{ // swap to frequency mode
|
||||
gEeprom.ScreenChannel[gEeprom.TX_VFO] = gEeprom.FreqChannel[gEeprom.TX_VFO];
|
||||
@@ -60,8 +75,8 @@ void COMMON_SwitchVFOMode()
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t Channel = RADIO_FindNextChannel(gEeprom.MrChannel[gEeprom.TX_VFO], 1, false, 0);
|
||||
if (Channel != 0xFF)
|
||||
uint16_t Channel = RADIO_FindNextChannel(gEeprom.MrChannel[gEeprom.TX_VFO], 1, false, 0);
|
||||
if (Channel != 0xFFFF)
|
||||
{ // swap to channel mode
|
||||
gEeprom.ScreenChannel[gEeprom.TX_VFO] = Channel;
|
||||
#ifdef ENABLE_VOICE
|
||||
@@ -74,4 +89,4 @@ void COMMON_SwitchVFOMode()
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -18,7 +18,7 @@
|
||||
#include <stdio.h> // NULL
|
||||
|
||||
#include "app/chFrScanner.h"
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#include "app/scanner.h"
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
+31
-13
@@ -51,20 +51,38 @@ static inline void Flashlight_Toggle(){ GPIO_TogglePin(GPIO_PIN_FLASHLIGHT); }
|
||||
|
||||
void ACTION_FlashLight(void)
|
||||
{
|
||||
switch (gFlashLightState) {
|
||||
case FLASHLIGHT_OFF:
|
||||
gFlashLightState++;
|
||||
Flashlight_TurnOn();
|
||||
break;
|
||||
case FLASHLIGHT_ON:
|
||||
case FLASHLIGHT_BLINK:
|
||||
gFlashLightState++;
|
||||
break;
|
||||
case FLASHLIGHT_SOS:
|
||||
default:
|
||||
gFlashLightState = 0;
|
||||
Flashlight_TurnOff();
|
||||
static uint16_t lastButtonTick;
|
||||
const uint16_t now = gFlashLightBlinkCounter;
|
||||
|
||||
// switch (gFlashLightState) {
|
||||
// case FLASHLIGHT_OFF:
|
||||
// Flashlight_TurnOn();
|
||||
// break;
|
||||
// case FLASHLIGHT_ON:
|
||||
// case FLASHLIGHT_BLINK:
|
||||
// break;
|
||||
// case FLASHLIGHT_SOS:
|
||||
// default:
|
||||
// Flashlight_TurnOff();
|
||||
// }
|
||||
|
||||
if (gFlashLightState != FLASHLIGHT_OFF &&
|
||||
(uint16_t)(now - lastButtonTick) >= (2500u / 10u)) {
|
||||
Flashlight_TurnOff();
|
||||
gFlashLightState = FLASHLIGHT_OFF;
|
||||
return;
|
||||
}
|
||||
|
||||
lastButtonTick = now;
|
||||
|
||||
if(gFlashLightState == FLASHLIGHT_OFF) {
|
||||
Flashlight_TurnOn();
|
||||
}
|
||||
else if (gFlashLightState == FLASHLIGHT_SOS) {
|
||||
Flashlight_TurnOff();
|
||||
}
|
||||
|
||||
gFlashLightState = (gFlashLightState + 1) % 4;
|
||||
}
|
||||
#else
|
||||
void ACTION_FlashLight(void)
|
||||
|
||||
+139
-102
@@ -23,6 +23,7 @@
|
||||
#include "app/generic.h"
|
||||
#include "audio.h"
|
||||
#include "driver/bk1080.h"
|
||||
#include "driver/bk4819.h"
|
||||
#include "driver/py25q16.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "functions.h"
|
||||
@@ -31,11 +32,8 @@
|
||||
#include "ui/inputbox.h"
|
||||
#include "ui/ui.h"
|
||||
|
||||
#ifndef ARRAY_SIZE
|
||||
#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
|
||||
#endif
|
||||
|
||||
uint16_t gFM_Channels[20];
|
||||
uint16_t gFM_Channels[FM_CHANNELS_MAX];
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
bool gFmRadioMode;
|
||||
uint8_t gFmRadioCountdown_500ms;
|
||||
volatile uint16_t gFmPlayCountdown_10ms;
|
||||
@@ -43,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;
|
||||
|
||||
|
||||
@@ -56,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)
|
||||
{
|
||||
@@ -98,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;
|
||||
@@ -109,6 +112,10 @@ void FM_TurnOff(void)
|
||||
|
||||
BK1080_Init0();
|
||||
|
||||
// Enable relevant LNA based on VFO frequency
|
||||
BK4819_PickRXFilterPathBasedOnFrequency(gRxVfo->freq_config_RX.Frequency);
|
||||
|
||||
|
||||
gUpdateStatus = true;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
|
||||
@@ -119,10 +126,27 @@ void FM_TurnOff(void)
|
||||
|
||||
void FM_EraseChannels(void)
|
||||
{
|
||||
PY25Q16_SectorErase(0x003000);
|
||||
//PY25Q16_SectorErase(0x003000);
|
||||
|
||||
uint8_t clearBuf[128];
|
||||
memset(clearBuf, 0xFF, sizeof(clearBuf));
|
||||
PY25Q16_WriteBuffer(0x00A028, clearBuf, sizeof(clearBuf), false);
|
||||
|
||||
memset(gFM_Channels, 0xFF, sizeof(gFM_Channels));
|
||||
}
|
||||
|
||||
uint16_t FM_WrapFrequency(uint16_t Frequency) {
|
||||
const uint16_t freqLoLimit = BK1080_GetFreqLoLimit(gEeprom.FM_Band);
|
||||
const uint16_t freqHiLimit = BK1080_GetFreqHiLimit(gEeprom.FM_Band);
|
||||
|
||||
if (Frequency < freqLoLimit)
|
||||
return freqHiLimit;
|
||||
else if (Frequency > freqHiLimit)
|
||||
return freqLoLimit;
|
||||
|
||||
return Frequency;
|
||||
}
|
||||
|
||||
void FM_Tune(uint16_t Frequency, int8_t Step, bool bFlag)
|
||||
{
|
||||
AUDIO_AudioPathOff();
|
||||
@@ -139,17 +163,19 @@ void FM_Tune(uint16_t Frequency, int8_t Step, bool bFlag)
|
||||
|
||||
if (!bFlag) {
|
||||
Frequency += Step;
|
||||
if (Frequency < BK1080_GetFreqLoLimit(gEeprom.FM_Band))
|
||||
Frequency = BK1080_GetFreqHiLimit(gEeprom.FM_Band);
|
||||
else if (Frequency > BK1080_GetFreqHiLimit(gEeprom.FM_Band))
|
||||
Frequency = BK1080_GetFreqLoLimit(gEeprom.FM_Band);
|
||||
Frequency = FM_WrapFrequency(Frequency);
|
||||
|
||||
gEeprom.FM_FrequencyPlaying = Frequency;
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -162,81 +188,62 @@ 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)
|
||||
{
|
||||
int ret = -1;
|
||||
|
||||
const uint16_t Test2 = BK1080_ReadRegister(BK1080_REG_07);
|
||||
|
||||
// This is supposed to be a signed value, but above function is unsigned
|
||||
const uint16_t Test2 = BK1080_ReadRegister(BK1080_REG_07);
|
||||
const uint16_t Deviation = BK1080_REG_07_GET_FREQD(Test2);
|
||||
|
||||
if (BK1080_REG_07_GET_SNR(Test2) <= 2) {
|
||||
BK1080_FrequencyDeviation = Deviation;
|
||||
BK1080_BaseFrequency = Frequency;
|
||||
// Helper macro to update globals and return
|
||||
#define RETURN(val) \
|
||||
do { \
|
||||
BK1080_FrequencyDeviation = Deviation; \
|
||||
BK1080_BaseFrequency = Frequency; \
|
||||
return (val); \
|
||||
} while (0)
|
||||
|
||||
return ret;
|
||||
}
|
||||
if (BK1080_REG_07_GET_SNR(Test2) <= 2)
|
||||
RETURN(-1);
|
||||
|
||||
const uint16_t Status = BK1080_ReadRegister(BK1080_REG_10);
|
||||
if ((Status & BK1080_REG_10_MASK_AFCRL) != BK1080_REG_10_AFCRL_NOT_RAILED ||
|
||||
BK1080_REG_10_GET_RSSI(Status) < 10)
|
||||
RETURN(-1);
|
||||
|
||||
if ((Status & BK1080_REG_10_MASK_AFCRL) != BK1080_REG_10_AFCRL_NOT_RAILED || BK1080_REG_10_GET_RSSI(Status) < 10) {
|
||||
BK1080_FrequencyDeviation = Deviation;
|
||||
BK1080_BaseFrequency = Frequency;
|
||||
if (Deviation >= 280 && Deviation <= 3815)
|
||||
RETURN(-1);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
//if (Deviation > -281 && Deviation < 280)
|
||||
if (Deviation >= 280 && Deviation <= 3815) {
|
||||
BK1080_FrequencyDeviation = Deviation;
|
||||
BK1080_BaseFrequency = Frequency;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// not BLE(less than or equal)
|
||||
// Scanning upward: previous deviation was negative (bit 11 set) or near zero
|
||||
if (Frequency > LowerLimit && (Frequency - BK1080_BaseFrequency) == 1) {
|
||||
if (BK1080_FrequencyDeviation & 0x800 || (BK1080_FrequencyDeviation < 20))
|
||||
{
|
||||
BK1080_FrequencyDeviation = Deviation;
|
||||
BK1080_BaseFrequency = Frequency;
|
||||
|
||||
return ret;
|
||||
}
|
||||
if (BK1080_FrequencyDeviation & 0x800 || BK1080_FrequencyDeviation < 20)
|
||||
RETURN(-1);
|
||||
}
|
||||
|
||||
// not BLT(less than)
|
||||
|
||||
// Scanning downward: previous deviation was positive or saturated high
|
||||
if (Frequency >= LowerLimit && (BK1080_BaseFrequency - Frequency) == 1) {
|
||||
if ((BK1080_FrequencyDeviation & 0x800) == 0 || (BK1080_FrequencyDeviation > 4075))
|
||||
{
|
||||
BK1080_FrequencyDeviation = Deviation;
|
||||
BK1080_BaseFrequency = Frequency;
|
||||
|
||||
return ret;
|
||||
}
|
||||
if ((BK1080_FrequencyDeviation & 0x800) == 0 || BK1080_FrequencyDeviation > 4075)
|
||||
RETURN(-1);
|
||||
}
|
||||
|
||||
ret = 0;
|
||||
#undef RETURN
|
||||
|
||||
BK1080_FrequencyDeviation = Deviation;
|
||||
BK1080_BaseFrequency = Frequency;
|
||||
|
||||
return ret;
|
||||
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 };
|
||||
@@ -262,6 +269,7 @@ static void Key_DIGITS(KEY_Code_t Key, uint8_t state)
|
||||
}
|
||||
|
||||
INPUTBOX_Append(Key);
|
||||
gKeyInputCountdown = key_input_timeout_500ms;
|
||||
|
||||
gRequestDisplayScreen = DISPLAY_FM;
|
||||
|
||||
@@ -277,6 +285,8 @@ static void Key_DIGITS(KEY_Code_t Key, uint8_t state)
|
||||
uint32_t Frequency;
|
||||
|
||||
gInputBoxIndex = 0;
|
||||
gKeyInputCountdown = 1;
|
||||
|
||||
Frequency = StrToUL(INPUTBOX_GetAscii());
|
||||
|
||||
if (Frequency < BK1080_GetFreqLoLimit(gEeprom.FM_Band) || BK1080_GetFreqHiLimit(gEeprom.FM_Band) < Frequency) {
|
||||
@@ -290,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;
|
||||
}
|
||||
@@ -299,6 +309,8 @@ static void Key_DIGITS(KEY_Code_t Key, uint8_t state)
|
||||
uint8_t Channel;
|
||||
|
||||
gInputBoxIndex = 0;
|
||||
gKeyInputCountdown = 1;
|
||||
|
||||
Channel = ((gInputBox[0] * 10) + gInputBox[1]) - 1;
|
||||
|
||||
if (State == STATE_MR_MODE) {
|
||||
@@ -308,12 +320,12 @@ 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;
|
||||
}
|
||||
}
|
||||
else if (Channel < 20) {
|
||||
else if (Channel < FM_CHANNELS_MAX) {
|
||||
#ifdef ENABLE_VOICE
|
||||
gAnotherVoiceID = (VOICE_ID_t)Key;
|
||||
#endif
|
||||
@@ -341,8 +353,7 @@ static void Key_FUNC(KEY_Code_t Key, uint8_t state)
|
||||
bool autoScan = gWasFKeyPressed || (state == BUTTON_EVENT_HELD);
|
||||
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
gWasFKeyPressed = false;
|
||||
gUpdateStatus = true;
|
||||
HideFKeyIcon();
|
||||
gRequestDisplayScreen = DISPLAY_FM;
|
||||
|
||||
switch (Key) {
|
||||
@@ -364,13 +375,21 @@ 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
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
break;
|
||||
|
||||
case KEY_8:
|
||||
ACTION_BackLightOnDemand();
|
||||
break;
|
||||
|
||||
case KEY_9:
|
||||
ACTION_BackLight();
|
||||
break;
|
||||
|
||||
case KEY_STAR:
|
||||
ACTION_Scan(autoScan);
|
||||
break;
|
||||
@@ -384,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;
|
||||
|
||||
@@ -401,6 +426,7 @@ static void Key_EXIT(uint8_t state)
|
||||
}
|
||||
else {
|
||||
gInputBox[--gInputBoxIndex] = 10;
|
||||
gKeyInputCountdown = key_input_timeout_500ms;
|
||||
|
||||
if (gInputBoxIndex) {
|
||||
if (gInputBoxIndex != 1) {
|
||||
@@ -432,14 +458,25 @@ static void Key_EXIT(uint8_t state)
|
||||
|
||||
static void Key_MENU(uint8_t state)
|
||||
{
|
||||
if (state != BUTTON_EVENT_SHORT)
|
||||
if (state == BUTTON_EVENT_HELD) {
|
||||
ACTION_Handle(KEY_MENU, true, true);
|
||||
return;
|
||||
|
||||
}
|
||||
else if (state != BUTTON_EVENT_SHORT) {
|
||||
return;
|
||||
}
|
||||
|
||||
gRequestDisplayScreen = DISPLAY_FM;
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
|
||||
HideFKeyIcon();
|
||||
|
||||
if (gFM_ScanState == FM_SCAN_OFF) {
|
||||
if (gInputBoxIndex) {
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gEeprom.FM_IsMrMode) {
|
||||
if (gAskToSave) {
|
||||
gFM_Channels[gFM_ChannelPosition] = gEeprom.FM_FrequencyPlaying;
|
||||
@@ -452,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;
|
||||
}
|
||||
@@ -476,6 +513,8 @@ static void Key_MENU(uint8_t state)
|
||||
|
||||
static void Key_UP_DOWN(uint8_t state, int8_t Step)
|
||||
{
|
||||
HideFKeyIcon();
|
||||
|
||||
if (state == BUTTON_EVENT_PRESSED) {
|
||||
if (gInputBoxIndex) {
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
@@ -487,9 +526,13 @@ static void Key_UP_DOWN(uint8_t state, int8_t Step)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gEeprom.SET_NAV) {
|
||||
Step = -Step;
|
||||
}
|
||||
|
||||
if (gAskToSave) {
|
||||
gRequestDisplayScreen = DISPLAY_FM;
|
||||
gFM_ChannelPosition = NUMBER_AddWithWraparound(gFM_ChannelPosition, Step, 0, 19);
|
||||
gFM_ChannelPosition = NUMBER_AddWithWraparound(gFM_ChannelPosition, Step, 0, FM_CHANNELS_MAX - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -515,10 +558,7 @@ static void Key_UP_DOWN(uint8_t state, int8_t Step)
|
||||
else {
|
||||
uint16_t Frequency = gEeprom.FM_SelectedFrequency + Step;
|
||||
|
||||
if (Frequency < BK1080_GetFreqLoLimit(gEeprom.FM_Band))
|
||||
Frequency = BK1080_GetFreqHiLimit(gEeprom.FM_Band);
|
||||
else if (Frequency > BK1080_GetFreqHiLimit(gEeprom.FM_Band))
|
||||
Frequency = BK1080_GetFreqLoLimit(gEeprom.FM_Band);
|
||||
Frequency = FM_WrapFrequency(Frequency);
|
||||
|
||||
gEeprom.FM_FrequencyPlaying = Frequency;
|
||||
gEeprom.FM_SelectedFrequency = gEeprom.FM_FrequencyPlaying;
|
||||
@@ -527,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;
|
||||
}
|
||||
@@ -547,18 +587,8 @@ void FM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
Key_MENU(state);
|
||||
break;
|
||||
case KEY_UP:
|
||||
#ifdef ENABLE_NAVIG_LEFT_RIGHT
|
||||
Key_UP_DOWN(state, -1);
|
||||
#else
|
||||
Key_UP_DOWN(state, 1);
|
||||
#endif
|
||||
break;
|
||||
case KEY_DOWN:
|
||||
#ifdef ENABLE_NAVIG_LEFT_RIGHT
|
||||
Key_UP_DOWN(state, 1);
|
||||
#else
|
||||
Key_UP_DOWN(state, -1);
|
||||
#endif
|
||||
Key_UP_DOWN(state, Key == KEY_UP ? 1 : -1);
|
||||
break;
|
||||
case KEY_EXIT:
|
||||
Key_EXIT(state);
|
||||
@@ -569,9 +599,14 @@ void FM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
case KEY_PTT:
|
||||
GENERIC_Key_PTT(bKeyPressed);
|
||||
break;
|
||||
default:
|
||||
if (!bKeyHeld && bKeyPressed)
|
||||
case KEY_SIDE1:
|
||||
case KEY_SIDE2:
|
||||
if (state != BUTTON_EVENT_PRESSED) {
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
HideFKeyIcon();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -586,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 < 20)
|
||||
if (gFM_ChannelPosition < FM_CHANNELS_MAX)
|
||||
gFM_Channels[gFM_ChannelPosition++] = gEeprom.FM_FrequencyPlaying;
|
||||
|
||||
if (gFM_ChannelPosition >= 20) {
|
||||
|
||||
if (gFM_ChannelPosition >= FM_CHANNELS_MAX) {
|
||||
FM_PlayAndUpdate();
|
||||
GUI_SelectNextDisplay(DISPLAY_FM);
|
||||
return;
|
||||
|
||||
goto Display;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,6 +642,7 @@ void FM_Play(void)
|
||||
else
|
||||
FM_Tune(gEeprom.FM_FrequencyPlaying, gFM_ScanState, false);
|
||||
|
||||
Display:
|
||||
GUI_SelectNextDisplay(DISPLAY_FM);
|
||||
}
|
||||
|
||||
@@ -619,10 +654,11 @@ void FM_Start(void)
|
||||
gFM_RestoreCountdown_10ms = 0;
|
||||
|
||||
BK1080_Init(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
|
||||
// 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
|
||||
@@ -630,5 +666,6 @@ void FM_Start(void)
|
||||
SETTINGS_WriteCurrentState();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+14
-8
@@ -19,7 +19,11 @@
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
|
||||
#include "misc.h"
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "driver/keyboard.h"
|
||||
#endif
|
||||
|
||||
#define FM_CHANNEL_UP 0x01
|
||||
#define FM_CHANNEL_DOWN 0xFF
|
||||
@@ -28,33 +32,35 @@ enum {
|
||||
FM_SCAN_OFF = 0U,
|
||||
};
|
||||
|
||||
extern uint16_t gFM_Channels[20];
|
||||
extern uint16_t gFM_Channels[FM_CHANNELS_MAX];
|
||||
|
||||
bool FM_CheckValidChannel(uint8_t Channel);
|
||||
// returns first valid channel starting at Channel
|
||||
uint8_t FM_FindNextChannel(uint8_t Channel, uint8_t Direction);
|
||||
int FM_ConfigureChannelState(void);
|
||||
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit);
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
extern bool gFmRadioMode;
|
||||
extern uint8_t gFmRadioCountdown_500ms;
|
||||
extern volatile uint16_t gFmPlayCountdown_10ms;
|
||||
extern volatile int8_t gFM_ScanState;
|
||||
extern bool gFM_AutoScan;
|
||||
extern uint8_t gFM_ChannelPosition;
|
||||
// Doubts about whether this should be signed or not
|
||||
extern uint16_t gFM_FrequencyDeviation;
|
||||
extern bool gFM_FoundFrequency;
|
||||
extern uint16_t gFM_RestoreCountdown_10ms;
|
||||
|
||||
bool FM_CheckValidChannel(uint8_t Channel);
|
||||
// returns first valid channel starting at Channel
|
||||
uint8_t FM_FindNextChannel(uint8_t Channel, uint8_t Direction);
|
||||
int FM_ConfigureChannelState(void);
|
||||
void FM_TurnOff(void);
|
||||
void FM_EraseChannels(void);
|
||||
|
||||
void FM_Tune(uint16_t Frequency, int8_t Step, bool bFlag);
|
||||
void FM_PlayAndUpdate(void);
|
||||
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit);
|
||||
|
||||
void FM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
|
||||
|
||||
void FM_Play(void);
|
||||
void FM_Start(void);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
+1504
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,66 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef APP_FOXHUNT_H
|
||||
#define APP_FOXHUNT_H
|
||||
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
|
||||
|
||||
#include "keyboard_state.h"
|
||||
|
||||
#include "../bitmaps.h"
|
||||
#include "../board.h"
|
||||
#include "py32f0xx.h"
|
||||
#include "../driver/backlight.h"
|
||||
#include "../driver/bk4819-regs.h"
|
||||
#include "../driver/bk4819.h"
|
||||
#include "../driver/gpio.h"
|
||||
#include "../driver/keyboard.h"
|
||||
#include "../driver/py25q16.h"
|
||||
#include "../driver/st7565.h"
|
||||
#include "../driver/system.h"
|
||||
#include "../driver/systick.h"
|
||||
#include "../external/printf/printf.h"
|
||||
#include "../font.h"
|
||||
#include "../helper/battery.h"
|
||||
#include "../misc.h"
|
||||
#include "../radio.h"
|
||||
#include "../settings.h"
|
||||
#include "../ui/battery.h"
|
||||
#include "../ui/helper.h"
|
||||
#include "../ui/ui.h"
|
||||
#include "../audio.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
// Assignable resident/overlay launchers (see action.c).
|
||||
void ACTION_FoxHunt(void);
|
||||
void ACTION_Beacon(void);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
// Self-contained modal loop that owns the screen and keypad until EXIT.
|
||||
void APP_RunFoxHunt(void);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEACON
|
||||
void APP_RunBeacon(void);
|
||||
#endif
|
||||
|
||||
#endif // APP_FOXHUNT_H
|
||||
+10
-22
@@ -20,7 +20,7 @@
|
||||
#include "app/chFrScanner.h"
|
||||
#include "app/common.h"
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
void GENERIC_Key_F(bool bKeyPressed, bool bKeyHeld)
|
||||
{
|
||||
if (gInputBoxIndex > 0) {
|
||||
if (gInputBoxIndex > 0 || gScreenToDisplay == DISPLAY_MENU) {
|
||||
if (!bKeyHeld && bKeyPressed) // short pressed
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
+375
-253
@@ -20,19 +20,23 @@
|
||||
#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"
|
||||
#include "app/main.h"
|
||||
#include "app/menu.h"
|
||||
#include "app/scanner.h"
|
||||
|
||||
#ifdef ENABLE_SPECTRUM
|
||||
#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,9 +48,36 @@
|
||||
#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>
|
||||
|
||||
// Full VFO backup for restore on EXIT
|
||||
static VFO_Info_t gVfoBackup;
|
||||
static uint16_t gScreenChannelBackup = 0;
|
||||
static uint16_t gFreqChannelBackup = 0;
|
||||
|
||||
static void VFO_RestoreBackup(void) {
|
||||
if (gHasVfoBackup) {
|
||||
const uint8_t Vfo = gEeprom.TX_VFO;
|
||||
|
||||
// Restore indices
|
||||
gEeprom.ScreenChannel[Vfo] = gScreenChannelBackup;
|
||||
gEeprom.FreqChannel[Vfo] = gFreqChannelBackup;
|
||||
|
||||
// Restore full VFO
|
||||
memcpy(gTxVfo, &gVfoBackup, sizeof(VFO_Info_t));
|
||||
|
||||
// Save and apply
|
||||
SETTINGS_SaveVfoIndices();
|
||||
RADIO_ConfigureSquelchAndOutputPower(gTxVfo);
|
||||
RADIO_SetupRegisters(true);
|
||||
|
||||
gHasVfoBackup = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void toggle_chan_scanlist(void)
|
||||
{ // toggle the selected channels scanlist setting
|
||||
|
||||
@@ -55,31 +86,33 @@ static void toggle_chan_scanlist(void)
|
||||
|
||||
if(!IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
|
||||
#ifdef ENABLE_SCAN_RANGES
|
||||
gScanRangeStart = gScanRangeStart ? 0 : gTxVfo->pRX->Frequency;
|
||||
gScanRangeStop = gEeprom.VfoInfo[!gEeprom.TX_VFO].freq_config_RX.Frequency;
|
||||
if(gScanRangeStart > gScanRangeStop)
|
||||
SWAP(gScanRangeStart, gScanRangeStop);
|
||||
CHFRSCANNER_ScanRange();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
ChannelAttributes_t *att = MR_GetChannelAttributes(gTxVfo->CHANNEL_SAVE);
|
||||
|
||||
// Remove exclude
|
||||
if(gMR_ChannelExclude[gTxVfo->CHANNEL_SAVE] == true)
|
||||
if(att->exclude == true)
|
||||
{
|
||||
gMR_ChannelExclude[gTxVfo->CHANNEL_SAVE] = false;
|
||||
return;
|
||||
att->exclude = false;
|
||||
MR_SaveChannelAttributesToFlash(gTxVfo->CHANNEL_SAVE, att);
|
||||
}
|
||||
else
|
||||
{
|
||||
uint8_t scanlist = gTxVfo->SCANLIST_PARTICIPATION;
|
||||
|
||||
scanlist++;
|
||||
|
||||
if (scanlist > SCAN_LIST_MODE_ALL)
|
||||
scanlist = 0;
|
||||
|
||||
gTxVfo->SCANLIST_PARTICIPATION = scanlist;
|
||||
|
||||
SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true);
|
||||
}
|
||||
|
||||
uint8_t scanTmp = gTxVfo->SCANLIST1_PARTICIPATION | (gTxVfo->SCANLIST2_PARTICIPATION << 1) | (gTxVfo->SCANLIST3_PARTICIPATION << 2);
|
||||
|
||||
scanTmp = (scanTmp++ < 7) ? scanTmp: 0;
|
||||
|
||||
gTxVfo->SCANLIST1_PARTICIPATION = (scanTmp >> 0) & 0x01;
|
||||
gTxVfo->SCANLIST2_PARTICIPATION = (scanTmp >> 1) & 0x01;
|
||||
gTxVfo->SCANLIST3_PARTICIPATION = (scanTmp >> 2) & 0x01;
|
||||
|
||||
SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true, true, true);
|
||||
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
gFlagResetVfos = true;
|
||||
}
|
||||
@@ -88,16 +121,14 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
||||
{
|
||||
uint8_t Vfo = gEeprom.TX_VFO;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
if(gEeprom.MENU_LOCK == true) {
|
||||
if(Key == 2) { // Enable A/B only
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
COMMON_SwitchVFOs();
|
||||
if (beep)
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
}
|
||||
if (beep)
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
|
||||
return; // prevent F function if MENU LOCK is true
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
if(gEeprom.MENU_LOCK == true && Key != 2) {
|
||||
HideFKeyIcon();
|
||||
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -105,8 +136,6 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
}
|
||||
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
|
||||
switch (Key) {
|
||||
case KEY_0:
|
||||
@@ -117,9 +146,7 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
||||
|
||||
case KEY_1:
|
||||
if (!IS_FREQ_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
|
||||
gWasFKeyPressed = false;
|
||||
gUpdateStatus = true;
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
HideFKeyIcon();
|
||||
|
||||
#ifdef ENABLE_COPY_CHAN_TO_VFO
|
||||
if (!gEeprom.VFO_OPEN || gCssBackgroundScan) {
|
||||
@@ -183,9 +210,6 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
||||
|
||||
gRequestDisplayScreen = DISPLAY_MAIN;
|
||||
|
||||
if (beep)
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
|
||||
break;
|
||||
|
||||
case KEY_2:
|
||||
@@ -193,8 +217,6 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
#endif
|
||||
COMMON_SwitchVFOs();
|
||||
if (beep)
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
break;
|
||||
|
||||
case KEY_3:
|
||||
@@ -202,26 +224,27 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
#endif
|
||||
COMMON_SwitchVFOMode();
|
||||
if (beep)
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
|
||||
break;
|
||||
|
||||
case KEY_4:
|
||||
gWasFKeyPressed = false;
|
||||
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;
|
||||
gUpdateStatus = true;
|
||||
if (beep)
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF;
|
||||
|
||||
SCANNER_Start(false);
|
||||
gRequestDisplayScreen = DISPLAY_SCANNER;
|
||||
break;
|
||||
|
||||
case KEY_5:
|
||||
if(beep) {
|
||||
if(!beep) {
|
||||
#ifdef ENABLE_NOAA
|
||||
if (!IS_NOAA_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
|
||||
gEeprom.ScreenChannel[Vfo] = gEeprom.NoaaChannel[gEeprom.TX_VFO];
|
||||
@@ -250,79 +273,109 @@ 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
|
||||
case KEY_UP:
|
||||
gEeprom.SQUELCH_LEVEL = (gEeprom.SQUELCH_LEVEL < 9) ? gEeprom.SQUELCH_LEVEL + 1: 9;
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
gWasFKeyPressed = false;
|
||||
break;
|
||||
case KEY_DOWN:
|
||||
gEeprom.SQUELCH_LEVEL = (gEeprom.SQUELCH_LEVEL > 0) ? gEeprom.SQUELCH_LEVEL - 1: 0;
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
gWasFKeyPressed = false;
|
||||
break;
|
||||
{
|
||||
bool isKeyUp = (Key == KEY_UP);
|
||||
|
||||
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) {
|
||||
if (gEeprom.SQUELCH_LEVEL < 9) gEeprom.SQUELCH_LEVEL++;
|
||||
} else {
|
||||
if (gEeprom.SQUELCH_LEVEL > 0) gEeprom.SQUELCH_LEVEL--;
|
||||
}
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
}
|
||||
|
||||
gWasFKeyPressed = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case KEY_SIDE1:
|
||||
uint8_t a = FREQUENCY_GetSortedIdxFromStepIdx(gTxVfo->STEP_SETTING);
|
||||
if (a < STEP_N_ELEM - 1)
|
||||
{
|
||||
gTxVfo->STEP_SETTING = FREQUENCY_GetStepIdxFromSortedIdx(a + 1);
|
||||
}
|
||||
if (IS_FREQ_CHANNEL(gTxVfo->CHANNEL_SAVE))
|
||||
{
|
||||
gRequestSaveChannel = 1;
|
||||
}
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
gWasFKeyPressed = false;
|
||||
break;
|
||||
case KEY_SIDE2:
|
||||
uint8_t b = FREQUENCY_GetSortedIdxFromStepIdx(gTxVfo->STEP_SETTING);
|
||||
if (b > 0)
|
||||
{
|
||||
gTxVfo->STEP_SETTING = FREQUENCY_GetStepIdxFromSortedIdx(b - 1);
|
||||
bool isKeySide1 = (Key == KEY_SIDE1);
|
||||
uint8_t idx = FREQUENCY_GetSortedIdxFromStepIdx(gTxVfo->STEP_SETTING);
|
||||
|
||||
if ((isKeySide1 && idx < STEP_N_ELEM - 1) || (!isKeySide1 && idx > 0))
|
||||
{
|
||||
gTxVfo->STEP_SETTING = FREQUENCY_GetStepIdxFromSortedIdx(idx + (isKeySide1 ? 1 : -1));
|
||||
|
||||
if (IS_FREQ_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
|
||||
gRequestSaveChannel = 1;
|
||||
}
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
}
|
||||
|
||||
gWasFKeyPressed = false;
|
||||
|
||||
break;
|
||||
}
|
||||
if (IS_FREQ_CHANNEL(gTxVfo->CHANNEL_SAVE))
|
||||
{
|
||||
gRequestSaveChannel = 1;
|
||||
}
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
gWasFKeyPressed = false;
|
||||
break;
|
||||
#endif
|
||||
|
||||
default:
|
||||
gUpdateStatus = true;
|
||||
gWasFKeyPressed = false;
|
||||
HideFKeyIcon();
|
||||
|
||||
if (beep)
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
@@ -348,8 +401,8 @@ void channelMove(uint16_t Channel)
|
||||
gAnotherVoiceID = (VOICE_ID_t)Key;
|
||||
#endif
|
||||
|
||||
gEeprom.MrChannel[Vfo] = (uint8_t)Channel;
|
||||
gEeprom.ScreenChannel[Vfo] = (uint8_t)Channel;
|
||||
gEeprom.MrChannel[Vfo] = (uint16_t)Channel;
|
||||
gEeprom.ScreenChannel[Vfo] = (uint16_t)Channel;
|
||||
//gRequestSaveVFO = true;
|
||||
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
|
||||
|
||||
@@ -359,6 +412,8 @@ void channelMove(uint16_t Channel)
|
||||
#endif
|
||||
|
||||
RADIO_ConfigureChannel(gEeprom.TX_VFO, gVfoConfigureMode);
|
||||
|
||||
SETTINGS_SaveVfoIndices();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -386,21 +441,17 @@ void channelMoveSwitch(void) {
|
||||
Channel = (Channel * 10) + gInputBox[i];
|
||||
}
|
||||
|
||||
if ((Channel == 0) && (gInputBoxIndex != 3)) {
|
||||
if ((Channel == 0) && (gInputBoxIndex != 4)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gInputBoxIndex == 3) {
|
||||
if (gInputBoxIndex == 4) {
|
||||
gInputBoxIndex = 0;
|
||||
gKeyInputCountdown = 1;
|
||||
|
||||
channelMove(Channel - 1);
|
||||
SETTINGS_SaveVfoIndices();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
channelMove(Channel - 1);
|
||||
SETTINGS_SaveVfoIndices();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,10 +465,19 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
gRequestDisplayScreen = DISPLAY_MAIN;
|
||||
}
|
||||
|
||||
gWasFKeyPressed = false;
|
||||
gUpdateStatus = true;
|
||||
#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
|
||||
|
||||
processFKeyFunction(Key, false);
|
||||
HideFKeyIcon();
|
||||
|
||||
processFKeyFunction(Key, true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -432,20 +492,78 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
if (!gWasFKeyPressed) { // F-key wasn't pressed
|
||||
|
||||
if (gScanStateDir != SCAN_OFF){
|
||||
switch(Key) {
|
||||
case KEY_0...KEY_5:
|
||||
gEeprom.SCAN_LIST_DEFAULT = Key;
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
|
||||
SETTINGS_WriteCurrentState();
|
||||
#endif
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
INPUTBOX_Append(Key);
|
||||
|
||||
/* Wait until exactly two digits are entered */
|
||||
if (gInputBoxIndex < 2)
|
||||
return;
|
||||
|
||||
/* Two digits entered */
|
||||
gInputBoxIndex = 0;
|
||||
|
||||
uint8_t value = (gInputBox[0] * 10) + gInputBox[1];
|
||||
|
||||
/* 00 = ALL scan lists */
|
||||
if (value == 0)
|
||||
{
|
||||
gEeprom.SCAN_LIST_DEFAULT = SCAN_LIST_MODE_ALL;
|
||||
UI_MAIN_NotifyScanListChanged();
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
|
||||
SETTINGS_WriteCurrentState();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
/* 25 = saved MIX selection */
|
||||
if (value == SCAN_LIST_MIX_SHORTCUT)
|
||||
{
|
||||
gEeprom.SCAN_LIST_DEFAULT = SCAN_LIST_MODE_MIX;
|
||||
|
||||
if (!RADIO_CheckValidList(SCAN_LIST_MODE_MIX))
|
||||
{
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
RADIO_NextValidList(1);
|
||||
}
|
||||
UI_MAIN_NotifyScanListChanged();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
|
||||
SETTINGS_WriteCurrentState();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
/* 01 .. MR_CHANNELS_LIST */
|
||||
if (value <= MR_CHANNELS_LIST)
|
||||
{
|
||||
gEeprom.SCAN_LIST_DEFAULT = value;
|
||||
|
||||
if (!RADIO_CheckValidList(value))
|
||||
{
|
||||
/* Requested scan list is empty or invalid:
|
||||
jump to the next valid scan list */
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
RADIO_NextValidList(1);
|
||||
}
|
||||
UI_MAIN_NotifyScanListChanged();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
|
||||
SETTINGS_WriteCurrentState();
|
||||
#endif
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t Vfo = gEeprom.TX_VFO;
|
||||
|
||||
// Save full VFO state BEFORE first digit
|
||||
if (gInputBoxIndex == 0 && IS_FREQ_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
|
||||
memcpy(&gVfoBackup, gTxVfo, sizeof(VFO_Info_t));
|
||||
gScreenChannelBackup = gEeprom.ScreenChannel[Vfo];
|
||||
gFreqChannelBackup = gEeprom.FreqChannel[Vfo];
|
||||
gHasVfoBackup = true;
|
||||
}
|
||||
|
||||
INPUTBOX_Append(Key);
|
||||
gKeyInputCountdown = key_input_timeout_500ms;
|
||||
|
||||
@@ -483,7 +601,13 @@ 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;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const char *inputStr = INPUTBOX_GetAscii();
|
||||
uint8_t inputLength = gInputBoxIndex;
|
||||
@@ -491,11 +615,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;
|
||||
}
|
||||
|
||||
@@ -552,6 +673,7 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
}
|
||||
|
||||
gInputBoxIndex = 0;
|
||||
gHasVfoBackup = false;
|
||||
|
||||
uint8_t Channel = (gInputBox[0] * 10) + gInputBox[1];
|
||||
if (Channel >= 1 && Channel <= ARRAY_SIZE(NoaaFrequencyTable)) {
|
||||
@@ -573,95 +695,77 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
return;
|
||||
}
|
||||
|
||||
gWasFKeyPressed = false;
|
||||
gUpdateStatus = true;
|
||||
HideFKeyIcon();
|
||||
|
||||
if(Key == 8)
|
||||
{
|
||||
ACTION_BackLightOnDemand();
|
||||
return;
|
||||
}
|
||||
else if(Key == 9)
|
||||
{
|
||||
ACTION_BackLight();
|
||||
return;
|
||||
}
|
||||
#ifdef ENABLE_FEAT_F4HWN_GAME
|
||||
else if(Key == 7)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
if(gEeprom.MENU_LOCK == true) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
APP_RunBreakout();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
processFKeyFunction(Key, true);
|
||||
processFKeyFunction(Key, false);
|
||||
}
|
||||
|
||||
static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
|
||||
{
|
||||
if (!bKeyHeld && bKeyPressed) { // exit key pressed
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; // beep when key is pressed
|
||||
return; // don't use the key till it's released
|
||||
}
|
||||
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
if (gDTMF_CallState != DTMF_CALL_STATE_NONE && gCurrentFunction != FUNCTION_TRANSMIT)
|
||||
{ // clear CALL mode being displayed
|
||||
gDTMF_CallState = DTMF_CALL_STATE_NONE;
|
||||
gUpdateDisplay = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
if (!gFmRadioMode)
|
||||
#endif
|
||||
{
|
||||
if (gScanStateDir == SCAN_OFF) {
|
||||
if (gInputBoxIndex == 0)
|
||||
return;
|
||||
gInputBox[--gInputBoxIndex] = 10;
|
||||
|
||||
gKeyInputCountdown = key_input_timeout_500ms;
|
||||
|
||||
#ifdef ENABLE_VOICE
|
||||
if (gInputBoxIndex == 0)
|
||||
gAnotherVoiceID = VOICE_ID_CANCEL;
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
gScanKeepResult = false;
|
||||
CHFRSCANNER_Stop();
|
||||
|
||||
#ifdef ENABLE_VOICE
|
||||
gAnotherVoiceID = VOICE_ID_SCANNING_STOP;
|
||||
#endif
|
||||
}
|
||||
if (bKeyHeld) { // exit key held down
|
||||
if (bKeyPressed) {
|
||||
// Restore full VFO state on long press EXIT
|
||||
VFO_RestoreBackup();
|
||||
|
||||
gRequestDisplayScreen = DISPLAY_MAIN;
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
ACTION_FM();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (bKeyHeld && bKeyPressed) { // exit key held down
|
||||
if (gInputBoxIndex > 0 || gDTMF_InputBox_Index > 0 || gDTMF_InputMode)
|
||||
{ // cancel key input mode (channel/frequency entry)
|
||||
gDTMF_InputMode = false;
|
||||
gDTMF_InputBox_Index = 0;
|
||||
memset(gDTMF_String, 0, sizeof(gDTMF_String));
|
||||
gInputBoxIndex = 0;
|
||||
gRequestDisplayScreen = DISPLAY_MAIN;
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
}
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
if (gDTMF_CallState != DTMF_CALL_STATE_NONE && gCurrentFunction != FUNCTION_TRANSMIT)
|
||||
{ // clear CALL mode being displayed
|
||||
gDTMF_CallState = DTMF_CALL_STATE_NONE;
|
||||
gUpdateDisplay = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (!gFmRadioMode)
|
||||
#endif
|
||||
{
|
||||
if (gScanStateDir == SCAN_OFF) {
|
||||
if (gInputBoxIndex == 0)
|
||||
return;
|
||||
gInputBox[--gInputBoxIndex] = 10;
|
||||
|
||||
// Restore full VFO state when back to 0
|
||||
if (gInputBoxIndex == 0)
|
||||
VFO_RestoreBackup();
|
||||
|
||||
gKeyInputCountdown = key_input_timeout_500ms;
|
||||
channelMoveSwitch();
|
||||
|
||||
#ifdef ENABLE_VOICE
|
||||
if (gInputBoxIndex == 0)
|
||||
gAnotherVoiceID = VOICE_ID_CANCEL;
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
gScanKeepResult = false;
|
||||
gInputBoxIndex = 0;
|
||||
CHFRSCANNER_Stop();
|
||||
|
||||
#ifdef ENABLE_VOICE
|
||||
gAnotherVoiceID = VOICE_ID_SCANNING_STOP;
|
||||
#endif
|
||||
}
|
||||
|
||||
gRequestDisplayScreen = DISPLAY_MAIN;
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
ACTION_FM();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld)
|
||||
@@ -673,12 +777,30 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld)
|
||||
if (bKeyPressed) { // long press MENU key
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
// Exclude work with list 1, 2, 3 or all list
|
||||
// Exclude current scan entry
|
||||
if(gScanStateDir != SCAN_OFF)
|
||||
{
|
||||
if(FUNCTION_IsRx() || gScanPauseDelayIn_10ms > 9)
|
||||
{
|
||||
gMR_ChannelExclude[gTxVfo->CHANNEL_SAVE] = true;
|
||||
#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;
|
||||
@@ -700,7 +822,6 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld)
|
||||
gRequestDisplayScreen = DISPLAY_MAIN;
|
||||
}
|
||||
|
||||
gWasFKeyPressed = false;
|
||||
gUpdateStatus = true;
|
||||
|
||||
ACTION_Handle(KEY_MENU, bKeyPressed, bKeyHeld);
|
||||
@@ -711,6 +832,9 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld)
|
||||
}
|
||||
|
||||
if (!bKeyPressed && !gDTMF_InputMode) { // menu key released
|
||||
gKeyInputCountdown = 1;
|
||||
channelMoveSwitch();
|
||||
|
||||
const bool bFlag = !gInputBoxIndex;
|
||||
gInputBoxIndex = 0;
|
||||
|
||||
@@ -721,18 +845,24 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld)
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
if(gEeprom.MENU_LOCK == false) {
|
||||
if(gEeprom.MENU_LOCK == true) {
|
||||
HideFKeyIcon();
|
||||
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
gFlagRefreshSetting = true;
|
||||
gScanMixEditorActive = false;
|
||||
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
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
gRequestDisplayScreen = DISPLAY_MAIN;
|
||||
@@ -742,13 +872,6 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld)
|
||||
|
||||
static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld)
|
||||
{
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
if(gEeprom.MENU_LOCK == true) {
|
||||
return; // prevent F function if MENU LOCK is true
|
||||
}
|
||||
#endif
|
||||
|
||||
if (gCurrentFunction == FUNCTION_TRANSMIT)
|
||||
return;
|
||||
|
||||
@@ -758,6 +881,19 @@ static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bKeyHeld && bKeyPressed) { // star key pressed
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; // beep when key is pressed
|
||||
return; // don't use the key till it's released
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
if(gEeprom.MENU_LOCK == true) {
|
||||
HideFKeyIcon();
|
||||
|
||||
return; // prevent F function if MENU LOCK is true
|
||||
}
|
||||
#endif
|
||||
|
||||
if (bKeyHeld && !gWasFKeyPressed){ // long press
|
||||
if (!bKeyPressed) // released
|
||||
return;
|
||||
@@ -780,12 +916,6 @@ static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld)
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (bKeyPressed) { // just pressed
|
||||
return;
|
||||
}
|
||||
|
||||
// just released
|
||||
|
||||
if (!gWasFKeyPressed) // pressed without the F-key
|
||||
{
|
||||
@@ -798,7 +928,6 @@ static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld)
|
||||
#endif
|
||||
)
|
||||
{ // start entering a DTMF string
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
memcpy(gDTMF_InputBox, gDTMF_String, MIN(sizeof(gDTMF_InputBox), sizeof(gDTMF_String) - 1));
|
||||
gDTMF_InputBox_Index = 0;
|
||||
gDTMF_InputMode = true;
|
||||
@@ -820,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;
|
||||
@@ -834,18 +969,13 @@ static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld)
|
||||
|
||||
static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
|
||||
{
|
||||
if (!gEeprom.SET_NAV) {
|
||||
Direction = -Direction;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN // Set Squelch F + UP or Down
|
||||
if(gWasFKeyPressed) {
|
||||
switch(Direction)
|
||||
{
|
||||
case 1:
|
||||
processFKeyFunction(KEY_UP, false);
|
||||
break;
|
||||
case -1:
|
||||
processFKeyFunction(KEY_DOWN, false);
|
||||
break;
|
||||
}
|
||||
processFKeyFunction(Direction == 1 ? KEY_UP : KEY_DOWN, true);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -855,12 +985,14 @@ static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
|
||||
gPowerHigh = false;
|
||||
#endif
|
||||
|
||||
uint8_t Channel = gEeprom.ScreenChannel[gEeprom.TX_VFO];
|
||||
uint16_t Channel = gEeprom.ScreenChannel[gEeprom.TX_VFO];
|
||||
|
||||
if (gInputBoxIndex > 0) {
|
||||
gInputBoxIndex = 0;
|
||||
gHasVfoBackup = false;
|
||||
}
|
||||
|
||||
if (bKeyHeld || !bKeyPressed) { // key held or released
|
||||
if (gInputBoxIndex > 0)
|
||||
return; // leave if input box active
|
||||
|
||||
if (!bKeyPressed) {
|
||||
if (!bKeyHeld || IS_FREQ_CHANNEL(Channel))
|
||||
return;
|
||||
@@ -873,19 +1005,21 @@ static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
|
||||
}
|
||||
}
|
||||
else { // short pressed
|
||||
if (gInputBoxIndex > 0) {
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
}
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
}
|
||||
|
||||
if (gScanStateDir == SCAN_OFF) {
|
||||
#ifdef ENABLE_FEAT_F4HWN_FULL_WATCH
|
||||
// Navigate from the displayed priority; the normal reload ends the swap.
|
||||
const VFO_Info_t *displayVfo = APP_GetFullWatchDisplayVfo(gEeprom.TX_VFO);
|
||||
if (displayVfo != NULL)
|
||||
Channel = displayVfo->CHANNEL_SAVE;
|
||||
#endif
|
||||
#ifdef ENABLE_NOAA
|
||||
if (!IS_NOAA_CHANNEL(Channel))
|
||||
#endif
|
||||
{
|
||||
uint8_t Next;
|
||||
uint16_t Next;
|
||||
if (IS_FREQ_CHANNEL(Channel)) { // step/down in frequency
|
||||
const uint32_t frequency = APP_SetFrequencyByStep(gTxVfo, Direction);
|
||||
|
||||
@@ -901,9 +1035,9 @@ static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
|
||||
}
|
||||
|
||||
Next = RADIO_FindNextChannel(Channel + Direction, Direction, false, 0);
|
||||
if (Next == 0xFF)
|
||||
if (Next == 0xFFFF)
|
||||
return;
|
||||
if (Channel == Next)
|
||||
if (Channel == Next && gEeprom.ScreenChannel[gEeprom.TX_VFO] == Next)
|
||||
return;
|
||||
gEeprom.MrChannel[gEeprom.TX_VFO] = Next;
|
||||
gEeprom.ScreenChannel[gEeprom.TX_VFO] = Next;
|
||||
@@ -929,16 +1063,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;
|
||||
@@ -977,18 +1109,8 @@ void MAIN_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
MAIN_Key_MENU(bKeyPressed, bKeyHeld);
|
||||
break;
|
||||
case KEY_UP:
|
||||
#ifdef ENABLE_NAVIG_LEFT_RIGHT
|
||||
MAIN_Key_UP_DOWN(bKeyPressed, bKeyHeld, -1);
|
||||
#else
|
||||
MAIN_Key_UP_DOWN(bKeyPressed, bKeyHeld, 1);
|
||||
#endif
|
||||
break;
|
||||
case KEY_DOWN:
|
||||
#ifdef ENABLE_NAVIG_LEFT_RIGHT
|
||||
MAIN_Key_UP_DOWN(bKeyPressed, bKeyHeld, 1);
|
||||
#else
|
||||
MAIN_Key_UP_DOWN(bKeyPressed, bKeyHeld, -1);
|
||||
#endif
|
||||
MAIN_Key_UP_DOWN(bKeyPressed, bKeyHeld, Key == KEY_UP ? 1 : -1);
|
||||
break;
|
||||
case KEY_EXIT:
|
||||
MAIN_Key_EXIT(bKeyPressed, bKeyHeld);
|
||||
|
||||
+780
-512
File diff suppressed because it is too large.
Load diff
+3
-1
@@ -24,6 +24,9 @@
|
||||
#endif
|
||||
|
||||
extern uint8_t gUnlockAllTxConfCnt;
|
||||
extern bool gScanMixEditorActive;
|
||||
extern uint8_t gScanMixEditorCursor;
|
||||
extern uint32_t gScanMixEditorMask;
|
||||
|
||||
int MENU_GetLimits(uint8_t menu_id, int32_t *pMin, int32_t *pMax);
|
||||
void MENU_AcceptSetting(void);
|
||||
@@ -35,4 +38,3 @@ void MENU_StopCssScan(void);
|
||||
void MENU_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/* Simple persistent byte settings. No callbacks or channel/VFO writes belong here.
|
||||
* MENU_SETTING(id, lvalue, minimum, maximum) is supplied by the includer.
|
||||
* Intentionally no include guard: used for validation and table generation.
|
||||
*/
|
||||
|
||||
MENU_SETTING(MENU_SAVE, gEeprom.BATTERY_SAVE, 0, 5)
|
||||
MENU_SETTING(MENU_BEEP, gEeprom.BEEP_CONTROL, 0, ARRAY_SIZE(gSubMenu_OFF_ON) - 1)
|
||||
MENU_SETTING(MENU_TOT, gEeprom.TX_TIMEOUT_TIMER, 5, 179)
|
||||
MENU_SETTING(MENU_SC_REV, gEeprom.SCAN_RESUME_MODE, 0, 104)
|
||||
MENU_SETTING(MENU_MDF, gEeprom.CHANNEL_DISPLAY_MODE, 0, ARRAY_SIZE(gSubMenu_MDF) - 1)
|
||||
MENU_SETTING(MENU_STE, gEeprom.TAIL_TONE_ELIMINATION, 0, ARRAY_SIZE(gSubMenu_OFF_ON) - 1)
|
||||
MENU_SETTING(MENU_RP_STE, gEeprom.REPEATER_TAIL_TONE_ELIMINATION, 0, 10)
|
||||
MENU_SETTING(MENU_S_LIST, gEeprom.SCAN_LIST_DEFAULT, 1, SCAN_LIST_MODE_MIX)
|
||||
MENU_SETTING(MENU_D_ST, gEeprom.DTMF_SIDE_TONE, 0, ARRAY_SIZE(gSubMenu_OFF_ON) - 1)
|
||||
MENU_SETTING(MENU_BATTYP, gEeprom.BATTERY_TYPE, 0, 4)
|
||||
MENU_SETTING(MENU_BAT_TXT, gSetting_battery_text, 0, ARRAY_SIZE(gSubMenu_BAT_TXT) - 1)
|
||||
#if defined(ENABLE_AUDIO_BAR)
|
||||
MENU_SETTING(MENU_MIC_BAR, gSetting_mic_bar, 0, ARRAY_SIZE(gSubMenu_OFF_ON) - 1)
|
||||
#endif
|
||||
#if defined(ENABLE_DTMF_CALLING)
|
||||
MENU_SETTING(MENU_D_RSP, gEeprom.DTMF_DECODE_RESPONSE, 0, ARRAY_SIZE(gSubMenu_D_RSP) - 1)
|
||||
#endif
|
||||
#if defined(ENABLE_DTMF_CALLING)
|
||||
MENU_SETTING(MENU_D_HOLD, gEeprom.DTMF_auto_reset_time, 5, 60)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN)
|
||||
MENU_SETTING(MENU_SET_NAV, gEeprom.SET_NAV, 0, 1)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN_SLEEP)
|
||||
MENU_SETTING(MENU_SET_OFF, gSetting_set_off, 0, 120)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN)
|
||||
MENU_SETTING(MENU_SET_TOT, gSetting_set_tot, 0, ARRAY_SIZE(gSubMenu_SET_TOT) - 1)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN)
|
||||
MENU_SETTING(MENU_SET_EOT, gSetting_set_eot, 0, ARRAY_SIZE(gSubMenu_SET_TOT) - 1)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN) && defined(ENABLE_FEAT_F4HWN_CTR)
|
||||
MENU_SETTING(MENU_SET_CTR, gSetting_set_ctr, 1, 15)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN)
|
||||
MENU_SETTING(MENU_SET_LCK, gSetting_set_lck, 0, SET_LCK_LEN - 1)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN)
|
||||
MENU_SETTING(MENU_SET_MET, gSetting_set_met, 0, ARRAY_SIZE(gSubMenu_SET_MET) - 1)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN)
|
||||
MENU_SETTING(MENU_SET_GUI, gSetting_set_gui, 0, ARRAY_SIZE(gSubMenu_SET_MET) - 1)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN) && defined(ENABLE_FEAT_F4HWN_SCAN_FASTER)
|
||||
MENU_SETTING(MENU_SET_SCN, gSetting_set_scn, 0, ARRAY_SIZE(gSubMenu_SET_SCN) - 1)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN) && defined(ENABLE_FEAT_F4HWN_VOL)
|
||||
MENU_SETTING(MENU_SET_VOL, gEeprom.VOLUME_GAIN, 0, 63)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN)
|
||||
MENU_SETTING(MENU_SET_TMR, gSetting_set_tmr, 0, ARRAY_SIZE(gSubMenu_OFF_ON) - 1)
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN) && defined(ENABLE_FEAT_F4HWN_LOGO_SAV)
|
||||
MENU_SETTING(MENU_SET_SAV, gSetting_set_sav, 0, SET_SAV_LEN - 1)
|
||||
#endif
|
||||
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
/*
|
||||
* REGA Alam and Test
|
||||
* ==================
|
||||
*
|
||||
* This bit of code is to implement the REGA test and alarm functions.
|
||||
* This function sends out a fixed ZVEI tone sequence for testing or alarm purposes.
|
||||
* Further information: https://www.rega.ch/en/our-missions/sites-and-infrastructure/emergency-radio
|
||||
*
|
||||
* There are two REGA ACTIONS: Test and Alarm.
|
||||
* The Test action sends out a fixed ZVEI tone sequence for testing purposes.
|
||||
* The Alarm action sends out a fixed ZVEI tone sequence for alarm purposes.
|
||||
*
|
||||
* These actions can be assigned to a key in the settings menu.
|
||||
*
|
||||
* The Test/Alarm function will perform the following actions:
|
||||
* - Set the radio to transmit mode
|
||||
* - Wait 100ms to allow the radio to switch to transmit mode and stabilize the tramsmitter
|
||||
* - Send out the ZVEI tone sequence
|
||||
* - Wait 100ms to allow the radio to finish transmitting
|
||||
* - Set the radio back to receive mode
|
||||
*
|
||||
* The ZVEI tone squence for alarm is: 21414
|
||||
* The ZVEI tone squence for test is: 21301
|
||||
*
|
||||
* To save space the two tone sequences are stored as precalculated register values.
|
||||
* This avoids the need to calculate the register values at runtime.
|
||||
* They are hardcoded as array of 32bit integers in the rega.h file.
|
||||
*
|
||||
* Copyright 2025 Markus Bärtschi
|
||||
* https://github.com/markusb
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "rega.h"
|
||||
#include "radio.h"
|
||||
#include "action.h"
|
||||
#include "misc.h"
|
||||
#include "settings.h"
|
||||
#include "functions.h"
|
||||
#include "driver/bk4819.h"
|
||||
#include "driver/st7565.h"
|
||||
#include "driver/system.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "bsp/dp32g030/gpio.h"
|
||||
#include "ui/helper.h"
|
||||
#include "ui/ui.h"
|
||||
|
||||
|
||||
const uint16_t rega_alarm_tones[5] = {
|
||||
1160, // 2 1160Hz
|
||||
1060, // 1 1060Hz
|
||||
1400, // 4 1400Hz
|
||||
1060, // 1 1060Hz
|
||||
1400, // 4 1400Hz
|
||||
};
|
||||
const uint16_t rega_test_tones[5] = {
|
||||
1160, // 2 1160Hz
|
||||
1060, // 1 1060Hz
|
||||
1260, // 3 1260Hz
|
||||
2400, // 0 2400Hz
|
||||
1060, // 1 1060Hz
|
||||
};
|
||||
|
||||
// Global variable to pass the message to the display
|
||||
char rega_message[16];
|
||||
|
||||
// Transmit the ZVEI tone sequence for alarm
|
||||
void ACTION_RegaAlarm()
|
||||
{
|
||||
const char message[16] = "REGA Alarm";
|
||||
REGA_TransmitZvei(rega_alarm_tones,message);
|
||||
}
|
||||
|
||||
// Tranmit the ZVEI tone sequence for test
|
||||
void ACTION_RegaTest()
|
||||
{
|
||||
const char message[16] = "REGA Test";
|
||||
REGA_TransmitZvei(rega_test_tones,message);
|
||||
}
|
||||
|
||||
// Display the REGA message on the screen
|
||||
void UI_DisplayREGA()
|
||||
{
|
||||
UI_DisplayClear();
|
||||
UI_DisplayPopup(rega_message);
|
||||
ST7565_BlitFullScreen();
|
||||
}
|
||||
|
||||
// Transmit a ZVEI tone sequence on the REGA frequency
|
||||
// Configures the radio on VFO A with the required parameters
|
||||
// tones: array of 5 ZVEI tones
|
||||
// message: message to display on the screen
|
||||
void REGA_TransmitZvei(const uint16_t tones[],const char message[])
|
||||
{
|
||||
// Copy the message text
|
||||
strncpy(rega_message,message,16);
|
||||
// Trigger the display
|
||||
gScreenToDisplay = DISPLAY_REGA;
|
||||
|
||||
// Flash the green LED twice
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
|
||||
SYSTEM_DelayMs(70);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||
SYSTEM_DelayMs(50);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
|
||||
SYSTEM_DelayMs(70);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||
|
||||
// Select VFO A
|
||||
gEeprom.TX_VFO = 0;
|
||||
gEeprom.ScreenChannel[gEeprom.TX_VFO] = gEeprom.FreqChannel[gEeprom.TX_VFO];
|
||||
gRxVfo = gTxVfo;
|
||||
gRequestSaveVFO = true;
|
||||
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
|
||||
|
||||
// Set the REGA frequency, no offset
|
||||
gTxVfo->freq_config_RX.Frequency = REGA_FREQUENCY;
|
||||
gTxVfo->freq_config_TX = gTxVfo->freq_config_RX;
|
||||
gTxVfo->TX_OFFSET_FREQUENCY = 0;
|
||||
|
||||
// Set the modulation to FM narrow
|
||||
gTxVfo->Modulation = MODULATION_FM;
|
||||
gTxVfo->CHANNEL_BANDWIDTH = BK4819_FILTER_BW_NARROW;
|
||||
|
||||
// Set Tx Squelch Tone
|
||||
gTxVfo->freq_config_TX.CodeType = CODE_TYPE_CONTINUOUS_TONE;
|
||||
gTxVfo->freq_config_TX.Code = REGA_CTCSS_FREQ_INDEX;
|
||||
BK4819_SetCTCSSFrequency(CTCSS_Options[gTxVfo->freq_config_TX.Code]);
|
||||
// Turn Rx squelch tone off
|
||||
gTxVfo->freq_config_RX.CodeType = CODE_TYPE_OFF;
|
||||
|
||||
// Set the transmit power to high
|
||||
gTxVfo->OUTPUT_POWER = OUTPUT_POWER_HIGH;
|
||||
|
||||
// Lock the keyboard
|
||||
gEeprom.KEY_LOCK = true;
|
||||
|
||||
gRequestSaveChannel = 1;
|
||||
gRequestSaveSettings = true;
|
||||
|
||||
// Configure the receiver
|
||||
BK4819_RX_TurnOn();
|
||||
|
||||
// Set the radio to transmit mode
|
||||
RADIO_PrepareCssTX();
|
||||
FUNCTION_Select(FUNCTION_TRANSMIT);
|
||||
|
||||
// Wait to allow the radio to switch to transmit mode and stabilize the transmitter
|
||||
SYSTEM_DelayMs(ZVEI_PRE_LENGTH_MS);
|
||||
|
||||
// Send out the ZVEI2 tone sequence
|
||||
for (int i = 0; i < ZVEI_NUM_TONES; i++)
|
||||
{
|
||||
BK4819_PlaySingleTone(tones[i], ZVEI_TONE_LENGTH_MS, 100, true);
|
||||
SYSTEM_DelayMs(ZVEI_PAUSE_LENGTH_MS);
|
||||
}
|
||||
|
||||
// Wait to allow the radio to finish transmitting
|
||||
SYSTEM_DelayMs(ZVEI_POST_LENGTH_MS);
|
||||
|
||||
// Flash the green LED twice
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
|
||||
SYSTEM_DelayMs(70);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||
SYSTEM_DelayMs(70);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
|
||||
SYSTEM_DelayMs(70);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||
|
||||
// Display the normal screen
|
||||
gRequestDisplayScreen = DISPLAY_MAIN;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* REGA Alam and Test
|
||||
* ==================
|
||||
*
|
||||
* This function sends out a fixed ZVEI tone sequence for testing or alarm purposes for the Alpine REGA alarm channel.
|
||||
* Further information: https://www.rega.ch/en/our-missions/sites-and-infrastructure/emergency-radio
|
||||
*
|
||||
* There are two REGA ACTIONS: Test and Alarm.
|
||||
* The Test action sends out a fixed ZVEI tone sequence for testing purposes.
|
||||
* The Alarm action sends out a fixed ZVEI tone sequence for alarm purposes.
|
||||
*
|
||||
* These actions can be assigned to a key in the settings menu.
|
||||
*
|
||||
* The Test/Alarm function will perform the following actions:
|
||||
* - Set the radio configuration to the REGA frequency, CTCSS, FM, full power etc.
|
||||
* - Start transmitting
|
||||
* - Wait 100ms to allow the radio to switch to transmit mode and stabilize the transmitter
|
||||
* - Send out the ZVEI tone sequence
|
||||
* - Wait 100ms to allow the radio to finish transmitting
|
||||
* - Set the radio back to receive mode
|
||||
*
|
||||
* The REGA frequency is 161.300 Mhz
|
||||
*
|
||||
* The ZVEI tone squence for alarm is: 21414
|
||||
* The ZVEI tone squence for test is: 21301
|
||||
*
|
||||
* The two tone sequences are hardcoded in two arrays
|
||||
*
|
||||
* Copyright 2025 Markus Bärtschi
|
||||
*
|
||||
* https://github.com/markusb
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef REGA_H
|
||||
#define REGA_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define ZVEI_NUM_TONES 5
|
||||
#define ZVEI_TONE_LENGTH_MS 70 // 70
|
||||
#define ZVEI_PAUSE_LENGTH_MS 10 // 10
|
||||
#define ZVEI_PRE_LENGTH_MS 300
|
||||
#define ZVEI_POST_LENGTH_MS 100
|
||||
#define REGA_CTCSS_FREQ_INDEX 18 // dcs.c: CTCSS_Options[18] = 1230
|
||||
#define REGA_FREQUENCY 16130000 // 43370000 16130000
|
||||
|
||||
void ACTION_RegaAlarm(void);
|
||||
void ACTION_RegaTest(void);
|
||||
void UI_DisplayREGA(void);
|
||||
void REGA_TransmitZvei(const uint16_t[], const char[]);
|
||||
|
||||
#endif
|
||||
+1430
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,91 @@
|
||||
/* 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_Suspend(void);
|
||||
void RXTX_LOG_Resume(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
|
||||
+132
-36
@@ -32,7 +32,7 @@ DCS_CodeType_t gScanCssResultType;
|
||||
uint8_t gScanCssResultCode;
|
||||
bool gScanSingleFrequency; // scan CTCSS/DCS codes for current frequency
|
||||
SCAN_SaveState_t gScannerSaveState;
|
||||
uint8_t gScanChannel;
|
||||
uint16_t gScanChannel;
|
||||
uint32_t gScanFrequency;
|
||||
SCAN_CssState_t gScanCssState;
|
||||
uint8_t gScanProgressIndicator;
|
||||
@@ -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)
|
||||
@@ -52,7 +147,7 @@ static void SCANNER_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
|
||||
gRequestDisplayScreen = DISPLAY_SCANNER;
|
||||
|
||||
if (gInputBoxIndex < 3) {
|
||||
if (gInputBoxIndex < 4) {
|
||||
#ifdef ENABLE_VOICE
|
||||
gAnotherVoiceID = (VOICE_ID_t)Key;
|
||||
#endif
|
||||
@@ -61,13 +156,14 @@ 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] * 100) + (gInputBox[1] * 10) + gInputBox[2]) - 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;
|
||||
#endif
|
||||
gShowChPrefix = RADIO_CheckValidChannel(chan, false, 0);
|
||||
gScanChannel = (uint8_t)chan;
|
||||
gScanChannel = chan;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -207,7 +303,7 @@ static void SCANNER_Key_MENU(bool bKeyPressed, bool bKeyHeld)
|
||||
gTxVfo->freq_config_TX.Code = gScanCssResultCode;
|
||||
}
|
||||
|
||||
uint8_t chan;
|
||||
uint16_t chan;
|
||||
if (IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
|
||||
chan = gScanChannel;
|
||||
gEeprom.MrChannel[gEeprom.TX_VFO] = chan;
|
||||
@@ -242,26 +338,27 @@ 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;
|
||||
}
|
||||
|
||||
if (!gEeprom.SET_NAV) {
|
||||
Direction = -Direction;
|
||||
}
|
||||
|
||||
if (gScannerSaveState == SCAN_SAVE_CHAN_SEL) {
|
||||
gScanChannel = NUMBER_AddWithWraparound(gScanChannel, Direction, 0, MR_CHANNEL_LAST);
|
||||
gShowChPrefix = RADIO_CheckValidChannel(gScanChannel, false, 0);
|
||||
gRequestDisplayScreen = DISPLAY_SCANNER;
|
||||
}
|
||||
else
|
||||
else if (!bKeyHeld)
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
}
|
||||
|
||||
@@ -275,10 +372,8 @@ void SCANNER_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
SCANNER_Key_MENU(bKeyPressed, bKeyHeld);
|
||||
break;
|
||||
case KEY_UP:
|
||||
SCANNER_Key_UP_DOWN(bKeyPressed, bKeyHeld, 1);
|
||||
break;
|
||||
case KEY_DOWN:
|
||||
SCANNER_Key_UP_DOWN(bKeyPressed, bKeyHeld, -1);
|
||||
SCANNER_Key_UP_DOWN(bKeyPressed, bKeyHeld, Key == KEY_UP ? 1 : -1);
|
||||
break;
|
||||
case KEY_EXIT:
|
||||
SCANNER_Key_EXIT(bKeyPressed, bKeyHeld);
|
||||
@@ -341,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;
|
||||
}
|
||||
@@ -366,6 +462,7 @@ void SCANNER_Start(bool singleFreq)
|
||||
g_SquelchLost = false;
|
||||
gScannerSaveState = SCAN_SAVE_NO_PROMPT;
|
||||
gScanProgressIndicator = 0;
|
||||
scanFreqVerifyState = SCAN_FREQ_VERIFY_OFF;
|
||||
}
|
||||
|
||||
void SCANNER_Stop(void)
|
||||
@@ -377,6 +474,7 @@ void SCANNER_Stop(void)
|
||||
gUpdateStatus = true;
|
||||
gCssBackgroundScan = false;
|
||||
gScanUseCssResult = false;
|
||||
scanFreqVerifyState = SCAN_FREQ_VERIFY_OFF;
|
||||
#ifdef ENABLE_VOICE
|
||||
gAnotherVoiceID = VOICE_ID_CANCEL;
|
||||
#endif
|
||||
@@ -398,6 +496,10 @@ void SCANNER_TimeSlice10ms(void)
|
||||
return;
|
||||
}
|
||||
|
||||
if (SCANNER_HandleFrequencyVerification()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (gScanCssState) {
|
||||
case SCAN_CSS_STATE_OFF: {
|
||||
// must be RF frequency scanning if we're here ?
|
||||
@@ -415,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;
|
||||
}
|
||||
@@ -506,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;
|
||||
@@ -525,4 +621,4 @@ void SCANNER_TimeSlice500ms(void)
|
||||
bool SCANNER_IsScanning(void)
|
||||
{
|
||||
return gCssBackgroundScan || (gScreenToDisplay == DISPLAY_SCANNER);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -40,7 +40,7 @@ extern DCS_CodeType_t gScanCssResultType;
|
||||
extern uint8_t gScanCssResultCode;
|
||||
extern bool gScanSingleFrequency;
|
||||
extern SCAN_SaveState_t gScannerSaveState;
|
||||
extern uint8_t gScanChannel;
|
||||
extern uint16_t gScanChannel;
|
||||
extern uint32_t gScanFrequency;
|
||||
extern SCAN_CssState_t gScanCssState;
|
||||
extern uint8_t gScanProgressIndicator;
|
||||
|
||||
+1170
-397
File diff suppressed because it is too large.
Load diff
+20
-13
@@ -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
|
||||
};
|
||||
|
||||
|
||||
+602
-31
@@ -20,10 +20,13 @@
|
||||
#if !defined(ENABLE_OVERLAY)
|
||||
#include "py32f0xx.h"
|
||||
#endif
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#include "app/uart.h"
|
||||
#ifdef ENABLE_AIRCOPY_UART
|
||||
#include "app/aircopy.h"
|
||||
#endif
|
||||
#include "board.h"
|
||||
#include "py32f071_ll_dma.h"
|
||||
#include "driver/backlight.h"
|
||||
@@ -45,6 +48,17 @@
|
||||
#include "settings.h"
|
||||
#include "version.h"
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
#include "driver/mb_flash.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_EXT_FLASH_RW
|
||||
#include "driver/py25q16.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||
#include "apps/app_overlay.h"
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_OVERLAY)
|
||||
#include "sram-overlay.h"
|
||||
#endif
|
||||
@@ -60,6 +74,10 @@
|
||||
// !! Make sure this is correct!
|
||||
#define MAX_REPLY_SIZE 144
|
||||
|
||||
#ifdef ENABLE_AIRCOPY_UART
|
||||
#define UART_CMD_AIRCOPY 0x0740u
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint16_t ID;
|
||||
uint16_t Size;
|
||||
@@ -193,7 +211,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 +220,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 +234,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 +274,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 +305,61 @@ static void SendReply(uint32_t Port, void *pReply, uint16_t Size)
|
||||
Footer.ID = 0xBADC;
|
||||
|
||||
UART_Send(&Footer, sizeof(Footer));
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AIRCOPY_UART
|
||||
void UART_SendAircopy(const uint16_t *data, uint8_t words)
|
||||
{
|
||||
static union {
|
||||
uint8_t Bytes[sizeof(Header_t) + 2u +
|
||||
AIRCOPY_FRAME_WORDS_MAX * sizeof(uint16_t) + sizeof(uint16_t)];
|
||||
struct __attribute__((packed, aligned(4))) {
|
||||
Header_t Header;
|
||||
uint8_t Words;
|
||||
uint8_t Reserved;
|
||||
uint16_t Data[AIRCOPY_FRAME_WORDS_MAX];
|
||||
} Packet;
|
||||
} Frame;
|
||||
Header_t transportHeader;
|
||||
const uint16_t transportFooter = 0xBADCu;
|
||||
|
||||
if (words == 0u || words > AIRCOPY_FRAME_WORDS_MAX)
|
||||
return;
|
||||
|
||||
Frame.Packet.Header.ID = UART_CMD_AIRCOPY;
|
||||
Frame.Packet.Header.Size = (uint16_t)(2u + words * sizeof(Frame.Packet.Data[0]));
|
||||
Frame.Packet.Words = words;
|
||||
Frame.Packet.Reserved = 0;
|
||||
memcpy(Frame.Packet.Data, data, words * sizeof(Frame.Packet.Data[0]));
|
||||
|
||||
const uint16_t bodySize = (uint16_t)(sizeof(Frame.Packet.Header) +
|
||||
Frame.Packet.Header.Size);
|
||||
const uint16_t crc = CRC_Calculate(Frame.Bytes, bodySize);
|
||||
Frame.Bytes[bodySize] = (uint8_t)crc;
|
||||
Frame.Bytes[bodySize + 1u] = (uint8_t)(crc >> 8);
|
||||
|
||||
// Peer radios feed this packet back through UART_IsCommandAvailable(), so
|
||||
// unlike a PC reply it must carry a real CRC instead of the 0xFFFF marker.
|
||||
for (uint16_t i = 0; i < bodySize + sizeof(crc); i++)
|
||||
Frame.Bytes[i] ^= Obfuscation[i % 16u];
|
||||
|
||||
transportHeader.ID = 0xCDABu;
|
||||
transportHeader.Size = bodySize;
|
||||
UART_Send(&transportHeader, sizeof(transportHeader));
|
||||
UART_Send(Frame.Bytes, bodySize + sizeof(crc));
|
||||
UART_Send(&transportFooter, sizeof(transportFooter));
|
||||
}
|
||||
#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 +414,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 +458,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 +479,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 +493,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 +526,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,12 +549,16 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
if (bReloadEeprom)
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_HOT_CFG
|
||||
SETTINGS_InitEEPROM(false);
|
||||
#else
|
||||
SETTINGS_InitEEPROM();
|
||||
#endif
|
||||
}
|
||||
|
||||
SendReply(Port, &Reply, sizeof(Reply));
|
||||
@@ -512,7 +600,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 +631,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 +682,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);
|
||||
}
|
||||
@@ -677,14 +766,26 @@ bool UART_IsCommandAvailable(uint32_t Port)
|
||||
return false;
|
||||
}
|
||||
|
||||
while (1)
|
||||
// Limit iterations to prevent long loops when buffer is full of non-command data
|
||||
uint16_t maxIterations = ReadBufSize + 1;
|
||||
|
||||
while (maxIterations--)
|
||||
{
|
||||
if ((*pReadPointer) == DmaLength)
|
||||
return false;
|
||||
|
||||
while ((*pReadPointer) != DmaLength && ReadBuf[*pReadPointer] != 0xABU)
|
||||
// Find 0xAB with iteration limit
|
||||
uint16_t searchLimit = ReadBufSize;
|
||||
while ((*pReadPointer) != DmaLength && ReadBuf[*pReadPointer] != 0xABU && searchLimit--)
|
||||
*pReadPointer = DMA_INDEX((*pReadPointer), 1, ReadBufSize);
|
||||
|
||||
if (searchLimit == 0)
|
||||
{
|
||||
// Too many bytes without finding 0xAB - sync to current position and exit
|
||||
*pReadPointer = DmaLength;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((*pReadPointer) == DmaLength)
|
||||
return false;
|
||||
|
||||
@@ -702,6 +803,13 @@ bool UART_IsCommandAvailable(uint32_t Port)
|
||||
*pReadPointer = DMA_INDEX(*pReadPointer, 1, ReadBufSize);
|
||||
}
|
||||
|
||||
if (maxIterations == 0)
|
||||
{
|
||||
// Safety: too many outer loop iterations
|
||||
*pReadPointer = DmaLength;
|
||||
return false;
|
||||
}
|
||||
|
||||
Index = DMA_INDEX(*pReadPointer, 2, ReadBufSize);
|
||||
Size = (ReadBuf[DMA_INDEX(Index, 1, ReadBufSize)] << 8) | ReadBuf[Index];
|
||||
|
||||
@@ -760,9 +868,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;
|
||||
@@ -787,6 +915,22 @@ void UART_HandleCommand(uint32_t Port)
|
||||
|
||||
switch (pUART_Command->Header.ID)
|
||||
{
|
||||
#ifdef ENABLE_AIRCOPY_UART
|
||||
case UART_CMD_AIRCOPY:
|
||||
{
|
||||
const uint8_t words = pUART_Command->Data[0];
|
||||
const uint16_t payloadSize = (uint16_t)(2u + words * sizeof(uint16_t));
|
||||
|
||||
if (Port == UART_PORT_UART &&
|
||||
words > 0u && words <= AIRCOPY_FRAME_WORDS_MAX &&
|
||||
pUART_Command->Header.Size == payloadSize)
|
||||
{
|
||||
AIRCOPY_StoreUartPacket(&pUART_Command->Data[2], words);
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
case 0x0514:
|
||||
CMD_0514(Port, pUART_Command->Buffer);
|
||||
break;
|
||||
@@ -833,6 +977,420 @@ 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)
|
||||
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;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_EXT_FLASH_RW
|
||||
// ---- full external-flash dump / restore (host: UV Studio) ----------
|
||||
// Raw access by physical address, bypassing the config-bank mapping;
|
||||
// all four commands are timestamp-authenticated like the slot ops.
|
||||
// Restore flow: erase each unprotected 4 KiB sector (0x073A), then
|
||||
// program it in chunks (0x073C). The calibration sector is rejected.
|
||||
case 0x0738: // read raw external flash by physical address (full-chip dump)
|
||||
{
|
||||
if (pUART_Command->Header.Size != 10u) break; // addr(4) + len(2) + timestamp(4)
|
||||
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||
uint32_t addr = (uint32_t)pUART_Command->Data[0]
|
||||
| ((uint32_t)pUART_Command->Data[1] << 8)
|
||||
| ((uint32_t)pUART_Command->Data[2] << 16)
|
||||
| ((uint32_t)pUART_Command->Data[3] << 24);
|
||||
uint16_t len = (uint16_t)(pUART_Command->Data[4]
|
||||
| ((uint16_t)pUART_Command->Data[5] << 8));
|
||||
uint32_t ts = (uint32_t)pUART_Command->Data[6]
|
||||
| ((uint32_t)pUART_Command->Data[7] << 8)
|
||||
| ((uint32_t)pUART_Command->Data[8] << 16)
|
||||
| ((uint32_t)pUART_Command->Data[9] << 24);
|
||||
struct __attribute__((packed)) {
|
||||
Header_t Header;
|
||||
uint32_t Address; // echoes the requested physical address
|
||||
uint16_t Size; // bytes returned (0 on error)
|
||||
uint8_t Status; // MB_OK or an MB_ERR_* code
|
||||
uint8_t Data[PY25Q16_RAW_CHUNK_SIZE]; // capped to fit MAX_REPLY_SIZE
|
||||
} Reply;
|
||||
memset(&Reply, 0, sizeof(Reply));
|
||||
uint8_t status;
|
||||
if (ts != mb_port_timestamp(Port))
|
||||
status = MB_ERR_AUTH;
|
||||
else if (len == 0u || len > sizeof(Reply.Data))
|
||||
status = MB_ERR_SIZE;
|
||||
else if (addr > PY25Q16_TOTAL_SIZE || len > PY25Q16_TOTAL_SIZE - addr)
|
||||
status = MB_ERR_SIZE;
|
||||
else
|
||||
{
|
||||
PY25Q16_ReadBufferPhysical(addr, Reply.Data, len);
|
||||
status = MB_OK;
|
||||
}
|
||||
Reply.Header.ID = 0x0739;
|
||||
Reply.Address = addr;
|
||||
Reply.Size = (status == MB_OK) ? len : 0;
|
||||
Reply.Status = status;
|
||||
Reply.Header.Size = 7 + Reply.Size; // Address(4)+Size(2)+Status(1)+Data
|
||||
SendReply(Port, &Reply, 11 + Reply.Size); // + Header(4)
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x073A: // erase one 4 KiB external-flash sector by physical address
|
||||
{
|
||||
if (pUART_Command->Header.Size != 8u) break; // addr(4) + timestamp(4)
|
||||
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||
uint32_t addr = (uint32_t)pUART_Command->Data[0]
|
||||
| ((uint32_t)pUART_Command->Data[1] << 8)
|
||||
| ((uint32_t)pUART_Command->Data[2] << 16)
|
||||
| ((uint32_t)pUART_Command->Data[3] << 24);
|
||||
uint32_t ts = (uint32_t)pUART_Command->Data[4]
|
||||
| ((uint32_t)pUART_Command->Data[5] << 8)
|
||||
| ((uint32_t)pUART_Command->Data[6] << 16)
|
||||
| ((uint32_t)pUART_Command->Data[7] << 24);
|
||||
uint8_t status;
|
||||
if (ts != mb_port_timestamp(Port))
|
||||
status = MB_ERR_AUTH;
|
||||
else if (addr >= PY25Q16_TOTAL_SIZE ||
|
||||
(addr % PY25Q16_SECTOR_SIZE) != 0u)
|
||||
status = MB_ERR_SIZE;
|
||||
else if (addr == PY25Q16_CALIBRATION_SECTOR_BASE)
|
||||
status = MB_ERR_PROTECTED; // device-specific data is never restorable
|
||||
else
|
||||
{
|
||||
PY25Q16_SectorErasePhysical(addr);
|
||||
status = MB_OK;
|
||||
}
|
||||
struct __attribute__((packed)) {
|
||||
Header_t Header;
|
||||
uint32_t Address; // echoes the requested physical address
|
||||
uint8_t Status; // MB_OK or an MB_ERR_* code
|
||||
} Reply;
|
||||
Reply.Header.ID = 0x073B;
|
||||
Reply.Address = addr;
|
||||
Reply.Status = status;
|
||||
Reply.Header.Size = 5; // Address(4)+Status(1)
|
||||
SendReply(Port, &Reply, 9); // + Header(4)
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x073C: // program bytes into external flash by physical address (sector pre-erased)
|
||||
{
|
||||
if (pUART_Command->Header.Size < 10u) break; // fixed fields + data
|
||||
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||
uint32_t addr = (uint32_t)pUART_Command->Data[0]
|
||||
| ((uint32_t)pUART_Command->Data[1] << 8)
|
||||
| ((uint32_t)pUART_Command->Data[2] << 16)
|
||||
| ((uint32_t)pUART_Command->Data[3] << 24);
|
||||
uint16_t len = (uint16_t)(pUART_Command->Data[4]
|
||||
| ((uint16_t)pUART_Command->Data[5] << 8));
|
||||
uint32_t ts = (uint32_t)pUART_Command->Data[6]
|
||||
| ((uint32_t)pUART_Command->Data[7] << 8)
|
||||
| ((uint32_t)pUART_Command->Data[8] << 16)
|
||||
| ((uint32_t)pUART_Command->Data[9] << 24);
|
||||
uint8_t status;
|
||||
if (ts != mb_port_timestamp(Port))
|
||||
status = MB_ERR_AUTH;
|
||||
else if (len == 0u || len > PY25Q16_RAW_CHUNK_SIZE ||
|
||||
len != pUART_Command->Header.Size - 10u)
|
||||
status = MB_ERR_SIZE;
|
||||
else if (addr > PY25Q16_TOTAL_SIZE || len > PY25Q16_TOTAL_SIZE - addr)
|
||||
status = MB_ERR_SIZE;
|
||||
else if (addr < PY25Q16_CALIBRATION_SECTOR_BASE + PY25Q16_SECTOR_SIZE &&
|
||||
addr + len > PY25Q16_CALIBRATION_SECTOR_BASE)
|
||||
status = MB_ERR_PROTECTED; // device-specific data is never restorable
|
||||
else
|
||||
{
|
||||
PY25Q16_WriteBufferPhysical(addr, &pUART_Command->Data[10], len);
|
||||
status = MB_OK;
|
||||
}
|
||||
struct __attribute__((packed)) {
|
||||
Header_t Header;
|
||||
uint32_t Address; // echoes the requested physical address
|
||||
uint16_t Size; // bytes written (0 on error)
|
||||
uint8_t Status; // MB_OK or an MB_ERR_* code
|
||||
} Reply;
|
||||
Reply.Header.ID = 0x073D;
|
||||
Reply.Address = addr;
|
||||
Reply.Size = (status == MB_OK) ? len : 0;
|
||||
Reply.Status = status;
|
||||
Reply.Header.Size = 7; // Address(4)+Size(2)+Status(1)
|
||||
SendReply(Port, &Reply, 11); // + Header(4)
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x073E: // CRC-32 of a physical external-flash range
|
||||
{
|
||||
if (pUART_Command->Header.Size != 12u) break; // addr(4) + len(4) + timestamp(4)
|
||||
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||
uint32_t addr = (uint32_t)pUART_Command->Data[0]
|
||||
| ((uint32_t)pUART_Command->Data[1] << 8)
|
||||
| ((uint32_t)pUART_Command->Data[2] << 16)
|
||||
| ((uint32_t)pUART_Command->Data[3] << 24);
|
||||
uint32_t len = (uint32_t)pUART_Command->Data[4]
|
||||
| ((uint32_t)pUART_Command->Data[5] << 8)
|
||||
| ((uint32_t)pUART_Command->Data[6] << 16)
|
||||
| ((uint32_t)pUART_Command->Data[7] << 24);
|
||||
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);
|
||||
uint32_t crc = 0;
|
||||
uint8_t status;
|
||||
if (ts != mb_port_timestamp(Port))
|
||||
status = MB_ERR_AUTH;
|
||||
else if (len == 0u || len > PY25Q16_SECTOR_SIZE)
|
||||
status = MB_ERR_SIZE;
|
||||
else
|
||||
status = MB_ExternalFlashCrc32(addr, len, &crc);
|
||||
struct __attribute__((packed)) {
|
||||
Header_t Header;
|
||||
uint32_t Address; // echoes the requested physical address
|
||||
uint32_t Size; // bytes covered by the CRC
|
||||
uint32_t Crc32; // zlib-compatible CRC-32
|
||||
uint8_t Status; // MB_OK or an MB_ERR_* code
|
||||
} Reply;
|
||||
Reply.Header.ID = 0x073F;
|
||||
Reply.Address = addr;
|
||||
Reply.Size = (status == MB_OK) ? len : 0;
|
||||
Reply.Crc32 = (status == MB_OK) ? crc : 0;
|
||||
Reply.Status = status;
|
||||
Reply.Header.Size = 13; // Address(4)+Size(4)+CRC32(4)+Status(1)
|
||||
SendReply(Port, &Reply, sizeof(Reply));
|
||||
break;
|
||||
}
|
||||
#endif // ENABLE_FEAT_F4HWN_EXT_FLASH_RW
|
||||
#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 8 KiB slot region, but the app's tagged settings (see APP_SlotErase)
|
||||
{
|
||||
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);
|
||||
if (status == APP_OK)
|
||||
APP_NotifySlotChanged();
|
||||
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);
|
||||
@@ -844,7 +1402,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
|
||||
}
|
||||
+5
-1
@@ -19,6 +19,7 @@
|
||||
#define APP_UART_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum
|
||||
{
|
||||
@@ -32,6 +33,9 @@ enum
|
||||
|
||||
bool UART_IsCommandAvailable(uint32_t Port);
|
||||
void UART_HandleCommand(uint32_t Port);
|
||||
|
||||
void UART_ServiceCommands(void);
|
||||
#ifdef ENABLE_AIRCOPY_UART
|
||||
void UART_SendAircopy(const uint16_t *data, uint8_t words);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,300 @@
|
||||
/* 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>
|
||||
|
||||
/* API levels within ABI major 1, one per published release (what a release
|
||||
* ships is frozen; services added before the next release join its level):
|
||||
* 1 v6.0.0 baseline: every service up to and including beam_draw
|
||||
* 2 ticks_ms, rand32, asset_read (+ app_header_t asset_size / asset_crc),
|
||||
* idivmod, uidivmod (the resident division helpers), Labs system info,
|
||||
* current and minimum-since-boot free stack/RAM margin */
|
||||
#define APP_ABI_MAJOR 1u
|
||||
#define APP_API_LEVEL 2u
|
||||
|
||||
/* Minimum API level of an app that ships read-only assets (pack_app.py). */
|
||||
#define APP_API_ASSETS 2u
|
||||
|
||||
/* 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;
|
||||
|
||||
/* 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);
|
||||
|
||||
/* ---- BEAM channel transfer ----
|
||||
* APP_CAP_BEAM (v6.0.0, retired): every service below, FSK included.
|
||||
* APP_CAP_BEAM2: only beam_prepare (tunes the channel, no FSK setup),
|
||||
* beam_get, beam_save and beam_draw; the app drives the FSK modem through
|
||||
* bk_read/bk_write, and beam_leave/send/rx/rx_poll are NULL. */
|
||||
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 */
|
||||
|
||||
/* ---- API level 2: time, randomness, read-only assets ---- */
|
||||
/* Free-running millisecond clock with 10 ms resolution (SysTick). Compare
|
||||
* with unsigned subtraction: (api->ticks_ms() - start) >= period. */
|
||||
uint32_t (*ticks_ms)(void);
|
||||
/* xorshift32 PRNG, never 0. The resident state persists across launches
|
||||
* and is re-mixed with RSSI noise and SysTick jitter at each launch, so apps
|
||||
* need no seed of their own. */
|
||||
uint32_t (*rand32)(void);
|
||||
/* Copy len bytes of this app's assets, starting at offset, into buf. The
|
||||
* read is clamped to the packed asset size; returns the byte count copied
|
||||
* (0 past the end or when the app has no assets). The loader verified the
|
||||
* assets' CRC before launch. buf may live in the overlay (.bss) or on the
|
||||
* stack. */
|
||||
uint16_t (*asset_read)(uint16_t offset, void *buf, uint16_t len);
|
||||
|
||||
/* ---- API level 2: integer division ---- */
|
||||
/* The resident run-time helpers (the Cortex-M0+ has no divide
|
||||
* instruction), so an app needs no libgcc division of its own: its
|
||||
* __aeabi_idivmod / __aeabi_uidivmod (and the __aeabi_idiv / __aeabi_uidiv
|
||||
* aliases) just forward here. C division (truncated toward zero), the
|
||||
* quotient in the low word (r0) and the remainder in the high word (r1), as
|
||||
* the AEABI helpers return them; x / 0 gives 0, remainder x. */
|
||||
uint64_t (*idivmod)(int32_t n, int32_t d);
|
||||
uint64_t (*uidivmod)(uint32_t n, uint32_t d);
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO
|
||||
/* ---- API level 2: zero-code Labs system information ---- */
|
||||
const char *sys_edition;
|
||||
const char *sys_version;
|
||||
const char *sys_build_date;
|
||||
const char *sys_build_time;
|
||||
const char *sys_build_commit;
|
||||
const void *sys_flash_end;
|
||||
const void *sys_ram_end;
|
||||
const uint16_t *sys_battery_voltage;
|
||||
const void *sys_battery_type;
|
||||
unsigned int (*sys_battery_percent)(unsigned int voltage_10mV);
|
||||
void (*sys_storage_read)(uint32_t address, void *buffer, uint32_t size);
|
||||
|
||||
/* ---- API level 2: stack watermark diagnostics ---- */
|
||||
uint32_t (*sys_stack_free_now)(void);
|
||||
uint32_t (*sys_stack_free_min)(void);
|
||||
#endif
|
||||
} app_api_t;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO
|
||||
_Static_assert(sizeof(app_api_t) == 328u,
|
||||
"Labs system information must add exactly 52 API bytes");
|
||||
#endif
|
||||
|
||||
/* BK4819 AF modes for set_af (mirror driver/bk4819.h values). */
|
||||
enum { APP_AF_MUTE = 0, APP_AF_FM = 1, APP_AF_AM = 7 };
|
||||
|
||||
/* Register ids the apps use (mirror driver/bk4819-regs.h). */
|
||||
enum { APP_BK_REG_13 = 0x13 };
|
||||
|
||||
/* Entry point every app blob exports, placed at blob offset 0. */
|
||||
typedef void (*app_entry_t)(const app_api_t *api);
|
||||
|
||||
#endif /* APPS_APP_API_H */
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
# Shared artwork helpers for overlay-app title screens (used by gen_assets.py).
|
||||
#
|
||||
# Canvas is a 128x64 1-bit picture. Canvas.pages() packs it the way the LCD
|
||||
# buffers are laid out: 8 pages of 128 column bytes, LSB = top row; page 0 is
|
||||
# the status line, pages 1..7 the frame buffer. An app streams the result into
|
||||
# A->status_line and A->fb with two asset_read calls.
|
||||
#
|
||||
# The logo font draws letters as stroke skeletons: polylines on a 14-row grid
|
||||
# (x right, y down), scaled, drawn with a square pen and sheared into italics.
|
||||
# Corners are cut by diagonal segments (chamfered arcade look), and each word
|
||||
# is outlined by a one-pixel ring drawn one blank pixel away from the letters.
|
||||
W, H = 128, 64
|
||||
PROMPT_TOP = 56 # page 7 stays blank: the app blinks its prompt there
|
||||
|
||||
LOGO = {
|
||||
"A": (9, [[(0, 13), (0, 3), (3, 0), (6, 0), (9, 3), (9, 13)], [(0, 8), (9, 8)]]),
|
||||
"B": (9, [[(0, 13), (0, 0), (7, 0), (9, 2), (9, 4), (7, 6), (0, 6)],
|
||||
[(7, 6), (9, 8), (9, 11), (7, 13), (0, 13)]]),
|
||||
"C": (9, [[(9, 0), (3, 0), (0, 3), (0, 10), (3, 13), (9, 13)]]),
|
||||
"D": (9, [[(0, 0), (6, 0), (9, 3), (9, 10), (6, 13), (0, 13), (0, 0)]]),
|
||||
"E": (9, [[(9, 0), (0, 0), (0, 13), (9, 13)], [(0, 6), (6, 6)]]),
|
||||
"I": (6, [[(0, 0), (6, 0)], [(3, 0), (3, 13)], [(0, 13), (6, 13)]]),
|
||||
"K": (9, [[(0, 0), (0, 13)], [(9, 0), (3, 6), (0, 6)], [(3, 6), (9, 12), (9, 13)]]),
|
||||
"L": (8, [[(0, 0), (0, 13), (8, 13)]]),
|
||||
"M": (12, [[(0, 13), (0, 0), (6, 6), (12, 0), (12, 13)]]),
|
||||
"N": (9, [[(0, 13), (0, 0), (9, 13), (9, 0)]]),
|
||||
"O": (9, [[(3, 0), (6, 0), (9, 3), (9, 10), (6, 13), (3, 13), (0, 10), (0, 3), (3, 0)]]),
|
||||
"P": (9, [[(0, 13), (0, 0), (7, 0), (9, 2), (9, 5), (7, 7), (0, 7)]]),
|
||||
"R": (9, [[(0, 13), (0, 0), (7, 0), (9, 2), (9, 5), (7, 7), (0, 7)], [(5, 7), (9, 11), (9, 13)]]),
|
||||
"S": (9, [[(9, 0), (2, 0), (0, 2), (0, 4), (2, 6), (7, 6), (9, 8), (9, 11), (7, 13), (0, 13)]]),
|
||||
"T": (10, [[(0, 0), (10, 0)], [(5, 0), (5, 13)]]),
|
||||
"U": (9, [[(0, 0), (0, 10), (3, 13), (6, 13), (9, 10), (9, 0)]]),
|
||||
"W": (12, [[(0, 0), (0, 13), (6, 7), (12, 13), (12, 0)]]),
|
||||
}
|
||||
LOGO_KX, LOGO_KY = 1.0, 1.1 # default skeleton scale
|
||||
LOGO_BRUSH = 3 # stroke thickness in pixels
|
||||
LOGO_GAP = 3 # pixels between letters
|
||||
LOGO_SHEAR = 4 # one pixel of slant every LOGO_SHEAR rows
|
||||
LOGO_RING = 2 # outline distance from the letters (0 = none)
|
||||
|
||||
def line(pts, x0, y0, x1, y1):
|
||||
"""Bresenham segment into the pts set."""
|
||||
dx, dy = abs(x1 - x0), -abs(y1 - y0)
|
||||
sx, sy = (1 if x0 < x1 else -1), (1 if y0 < y1 else -1)
|
||||
err = dx + dy
|
||||
while True:
|
||||
pts.add((x0, y0))
|
||||
if x0 == x1 and y0 == y1:
|
||||
return
|
||||
e2 = 2 * err
|
||||
if e2 >= dy:
|
||||
err += dy
|
||||
x0 += sx
|
||||
if e2 <= dx:
|
||||
err += dx
|
||||
y0 += sy
|
||||
|
||||
def logo_glyph(ch, kx, ky):
|
||||
"""(advance width, pixel set) of one logo letter, before shearing."""
|
||||
width, polylines = LOGO[ch]
|
||||
skeleton = set()
|
||||
for poly in polylines:
|
||||
pts = [(round(x * kx), round(y * ky)) for x, y in poly]
|
||||
for a, b in zip(pts, pts[1:]):
|
||||
line(skeleton, *a, *b)
|
||||
pixels = {(x + dx, y + dy) for x, y in skeleton
|
||||
for dx in range(LOGO_BRUSH) for dy in range(LOGO_BRUSH)}
|
||||
return round(width * kx) + LOGO_BRUSH, pixels
|
||||
|
||||
class Canvas:
|
||||
def __init__(self):
|
||||
self.px = [[0] * W for _ in range(H)]
|
||||
|
||||
def set(self, x, y, on=1):
|
||||
if 0 <= x < W and 0 <= y < H:
|
||||
self.px[y][x] = on
|
||||
|
||||
def rect(self, x0, y0, x1, y1):
|
||||
for y in range(y0, y1 + 1):
|
||||
for x in range(x0, x1 + 1):
|
||||
self.set(x, y)
|
||||
|
||||
def sprite(self, x, y, cols, scale=1):
|
||||
"""Column-major sprite (LSB = top row), as the apps store them."""
|
||||
for i, c in enumerate(cols):
|
||||
for bit in range(8):
|
||||
if c >> bit & 1:
|
||||
for dx in range(scale):
|
||||
for dy in range(scale):
|
||||
self.set(x + i * scale + dx, y + bit * scale + dy)
|
||||
|
||||
def word(self, text, y, kx=LOGO_KX, ky=LOGO_KY):
|
||||
"""Centered, outlined italic logo word; returns its letter height."""
|
||||
height = round(13 * ky) + LOGO_BRUSH
|
||||
glyphs = [logo_glyph(ch, kx, ky) for ch in text]
|
||||
total = sum(w for w, _ in glyphs) + LOGO_GAP * (len(glyphs) - 1) + (height - 1) // LOGO_SHEAR
|
||||
x = (W - total) // 2
|
||||
solid = set()
|
||||
for width, pixels in glyphs:
|
||||
for gx, gy in pixels:
|
||||
solid.add((x + gx + (height - 1 - gy) // LOGO_SHEAR, y + gy))
|
||||
x += width + LOGO_GAP
|
||||
grown = [solid] # solid dilated by 0, 1, ... pixels
|
||||
for _ in range(LOGO_RING):
|
||||
grown.append({(px + dx, py + dy) for px, py in grown[-1]
|
||||
for dx in (-1, 0, 1) for dy in (-1, 0, 1)})
|
||||
ring = grown[-1] - grown[-2] if LOGO_RING else set()
|
||||
for px, py in solid | ring:
|
||||
self.set(px, py)
|
||||
return height
|
||||
|
||||
def two_words(self, first, second):
|
||||
"""Logo on two lines from the top of the screen; returns the bottom row."""
|
||||
top = LOGO_RING # keep the outline on screen
|
||||
height = self.word(first, top)
|
||||
second_top = top + height + 2 * LOGO_RING + 1
|
||||
return second_top + self.word(second, second_top) + LOGO_RING
|
||||
|
||||
def pages(self):
|
||||
"""Title-screen payload: status page then the 7 frame-buffer pages."""
|
||||
if any(self.px[y][x] for y in range(PROMPT_TOP, H) for x in range(W)):
|
||||
raise SystemExit("title: the bottom page is reserved for the prompt")
|
||||
out = bytearray()
|
||||
for page in range(H // 8):
|
||||
for x in range(W):
|
||||
b = 0
|
||||
for bit in range(8):
|
||||
if self.px[page * 8 + bit][x]:
|
||||
b |= 1 << bit
|
||||
out.append(b)
|
||||
return bytes(out)
|
||||
|
||||
def preview(self, path, scale=4):
|
||||
"""Binary PGM, LCD-like colours (dark pixels on a pale panel)."""
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P5 %d %d 255\n" % (W * scale, H * scale))
|
||||
for y in range(H):
|
||||
row = bytes((40 if self.px[y][x] else 200)
|
||||
for x in range(W) for _ in range(scale))
|
||||
f.write(row * scale)
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
# Shared builder for overlay-app read-only assets (served by api->asset_read).
|
||||
#
|
||||
# Each app has a gen_assets.py that declares its texts and data with this
|
||||
# module, then calls Assets.main(): it writes the asset blob packed into the
|
||||
# .app by pack_app.py, and a C header of offsets the app includes. The header
|
||||
# is generated before compiling, so code and data can never drift apart.
|
||||
#
|
||||
# a = Assets("SNAKE")
|
||||
# a.text("T_PAUSE", "PAUSE") # NUL-terminated string
|
||||
# a.table("T_STATE", ["TX", "RX"]) # fixed-stride strings (+ _STRIDE)
|
||||
# a.u16("SPR_HEAD", [0x6566, 0x0FD2]) # packed little-endian data
|
||||
# a.main() # argv: <out.bin> <out.h>
|
||||
#
|
||||
# Texts come first, so their offsets stay small (cheap immediates in Thumb
|
||||
# code). TEXT_MAX is the longest text including its NUL: the size of the
|
||||
# app's shared text buffer, read in one asset_read by T().
|
||||
import os, re, struct, sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def _cdefine(header, name):
|
||||
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")
|
||||
|
||||
ASSET_MAX = _cdefine("app_overlay.h", "APP_ASSET_MAX")
|
||||
|
||||
class Assets:
|
||||
def __init__(self, guard):
|
||||
self.guard = guard
|
||||
self.texts = [] # (name, bytes)
|
||||
self.data = [] # (name, bytes)
|
||||
self.extra = [] # (name, value) plain constants
|
||||
self.names = set()
|
||||
|
||||
def _name(self, name):
|
||||
if name in self.names or not re.fullmatch(r"[A-Z][A-Z0-9_]*", name):
|
||||
sys.exit(f"asset name {name!r} is invalid or duplicated")
|
||||
self.names.add(name)
|
||||
|
||||
def text(self, name, s):
|
||||
self._name(name)
|
||||
self.texts.append((name, s.encode("ascii") + b"\x00"))
|
||||
|
||||
def table(self, name, strings, stride=None):
|
||||
"""Fixed-stride string table: entry i is at name + i * name_STRIDE."""
|
||||
self._name(name)
|
||||
width = stride or max(len(s) for s in strings) + 1
|
||||
if any(len(s) + 1 > width for s in strings):
|
||||
sys.exit(f"{name}: an entry does not fit the {width} B stride")
|
||||
blob = b"".join(s.encode("ascii").ljust(width, b"\x00") for s in strings)
|
||||
self.texts.append((name, blob))
|
||||
self.const(name + "_STRIDE", width)
|
||||
|
||||
def const(self, name, value):
|
||||
self._name(name)
|
||||
self.extra.append((name, value))
|
||||
|
||||
def raw(self, name, payload):
|
||||
self._name(name)
|
||||
self.data.append((name, bytes(payload)))
|
||||
|
||||
def u8(self, name, values): self.raw(name, struct.pack(f"<{len(values)}B", *values))
|
||||
def i8(self, name, values): self.raw(name, struct.pack(f"<{len(values)}b", *values))
|
||||
def u16(self, name, values): self.raw(name, struct.pack(f"<{len(values)}H", *values))
|
||||
def i16(self, name, values): self.raw(name, struct.pack(f"<{len(values)}h", *values))
|
||||
def u32(self, name, values): self.raw(name, struct.pack(f"<{len(values)}I", *values))
|
||||
|
||||
def build(self):
|
||||
blob = bytearray()
|
||||
lines = []
|
||||
text_max = 1
|
||||
strides = dict(self.extra)
|
||||
for name, payload in self.texts:
|
||||
lines.append((name, len(blob), len(payload)))
|
||||
text_max = max(text_max, strides.get(name + "_STRIDE", len(payload)))
|
||||
blob += payload
|
||||
for name, payload in self.data: # asset_read copies bytes: no alignment needed
|
||||
lines.append((name, len(blob), len(payload)))
|
||||
blob += payload
|
||||
if len(blob) > ASSET_MAX:
|
||||
sys.exit(f"assets {len(blob)} B exceed the {ASSET_MAX} B asset area")
|
||||
return bytes(blob), lines, text_max
|
||||
|
||||
def main(self):
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit("usage: gen_assets.py <out.bin> <out.h>")
|
||||
blob, lines, text_max = self.build()
|
||||
with open(sys.argv[1], "wb") as f:
|
||||
f.write(blob)
|
||||
guard = self.guard.upper() + "_ASSETS_H"
|
||||
out = [f"/* Generated by gen_assets.py - do not edit ({len(blob)} B of assets). */",
|
||||
f"#ifndef {guard}", f"#define {guard}", "",
|
||||
f"#define TEXT_MAX {text_max}u /* longest text incl. NUL */"]
|
||||
for name, off, size in lines:
|
||||
out.append(f"#define {name} {off}u")
|
||||
out.append(f"#define {name}_LEN {size}u")
|
||||
for name, value in self.extra:
|
||||
out.append(f"#define {name} {value}u")
|
||||
out += ["", f"#endif /* {guard} */", ""]
|
||||
with open(sys.argv[2], "w") as f:
|
||||
f.write("\n".join(out))
|
||||
@@ -0,0 +1,351 @@
|
||||
/* 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 "app/uart.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;
|
||||
}
|
||||
|
||||
/* 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 19u
|
||||
#define APP_NAME_BOX_END 102u
|
||||
#define APP_NAME_TEXT_X 21u
|
||||
|
||||
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)
|
||||
{
|
||||
#if defined(ENABLE_UART) || defined(ENABLE_USB)
|
||||
const uint8_t slot_revision = APP_SlotRevision();
|
||||
#endif
|
||||
|
||||
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
|
||||
#if defined(ENABLE_UART) || defined(ENABLE_USB)
|
||||
UART_ServiceCommands();
|
||||
if (APP_SlotRevision() != slot_revision)
|
||||
return KEY_INVALID;
|
||||
#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 APP_SLOT_COUNT
|
||||
|
||||
_Static_assert(APP_MENU_SLOT_COUNT <= APP_SLOT_COUNT,
|
||||
"APP_MENU_SLOT_COUNT exceeds the physical app slot count");
|
||||
|
||||
static uint16_t app_scan_slots(void)
|
||||
{
|
||||
uint16_t installed = 0u;
|
||||
for (uint8_t slot = 0; slot < APP_MENU_SLOT_COUNT; slot++)
|
||||
{
|
||||
app_header_t hdr;
|
||||
if (APP_SlotInfo(slot, &hdr) == APP_OK &&
|
||||
(hdr.flags & APP_FLAG_COMMITTED))
|
||||
installed |= (uint16_t)((uint16_t)1u << slot);
|
||||
}
|
||||
return installed;
|
||||
}
|
||||
|
||||
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. */
|
||||
uint8_t slot_revision = APP_SlotRevision();
|
||||
uint16_t installed = app_scan_slots();
|
||||
|
||||
/* 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 (;;)
|
||||
{
|
||||
const uint8_t current_revision = APP_SlotRevision();
|
||||
if (current_revision != slot_revision)
|
||||
{
|
||||
installed = app_scan_slots();
|
||||
slot_revision = current_revision;
|
||||
}
|
||||
|
||||
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[3];
|
||||
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 / 10u);
|
||||
number[1] = (char)('0' + visible_number % 10u);
|
||||
number[2] = '\0';
|
||||
|
||||
UI_PrintStringSmallNormal(number, 2u, 0, fbLine);
|
||||
if (installed & (uint16_t)((uint16_t)1u << slot))
|
||||
{
|
||||
app_header_t hdr;
|
||||
APP_SlotInfo(slot, &hdr);
|
||||
app_copy(name, sizeof(name), hdr.name, APP_NAME_LEN);
|
||||
app_format_size(size, hdr.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);
|
||||
}
|
||||
|
||||
UI_DrawMenuKeyHints("RUN", "QUIT");
|
||||
|
||||
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 & (uint16_t)((uint16_t)1u << sel)))
|
||||
break;
|
||||
|
||||
app_header_t hdr;
|
||||
APP_SlotInfo(sel, &hdr);
|
||||
const uint8_t rc = APP_LaunchOverlay(sel); /* runs until the app exits */
|
||||
if (rc != APP_OK)
|
||||
app_show_error(&hdr, rc); /* no longer silent */
|
||||
app_wait_release();
|
||||
/* Radio apps (BEAM, ...) hand back straight to the radio screen. */
|
||||
if (rc == APP_OK && (hdr.flags & APP_FLAG_EXIT_TO_MAIN))
|
||||
return;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* ENABLE_FEAT_F4HWN_OVERLAY_APPS */
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
/* Copyright 2023 OneOfEleven
|
||||
* https://github.com/DualTachyon
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,21 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef AM_FIXH
|
||||
#ifndef APPS_APP_MENU_H
|
||||
#define APPS_APP_MENU_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
/* Blocking "Apps" selector: scans the overlay-app slots, lists the committed
|
||||
* ones by name, and launches the chosen one. UP/DOWN move, MENU launches, EXIT
|
||||
* closes. Returns when the user leaves. */
|
||||
void APP_MenuOpen(void);
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
void AM_fix_init(void);
|
||||
void AM_fix_reset(const unsigned vfo);
|
||||
void AM_fix_10ms(const unsigned vfo);
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
void AM_fix_print_data(const unsigned vfo, char *s);
|
||||
#endif
|
||||
int8_t AM_fix_get_gain_diff();
|
||||
void AM_fix_enable(bool on);
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif /* APPS_APP_MENU_H */
|
||||
@@ -0,0 +1,933 @@
|
||||
/* 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 <stddef.h> /* offsetof */
|
||||
#include "py32f0xx.h"
|
||||
|
||||
#include "driver/bk4819.h"
|
||||
#include "driver/bk4819-regs.h"
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#include "driver/bk1080.h"
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#include "driver/keyboard.h"
|
||||
#include "driver/mb_flash.h"
|
||||
#include "driver/py25q16.h"
|
||||
#include "driver/st7565.h"
|
||||
#include "driver/system.h"
|
||||
#include "driver/backlight.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
#include "k5viewer.h"
|
||||
#endif
|
||||
#include "app/app.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
#include "app/rxtx_log.h"
|
||||
#endif
|
||||
#include "ui/helper.h"
|
||||
#include "ui/main.h"
|
||||
#include "ui/status.h"
|
||||
#include "board.h"
|
||||
#include "audio.h"
|
||||
#include "dcs.h"
|
||||
#include "functions.h"
|
||||
#include "frequencies.h"
|
||||
#include "radio.h"
|
||||
#include "scheduler.h"
|
||||
#include "helper/battery.h"
|
||||
#include "settings.h"
|
||||
#include "misc.h" /* dBmCorrTable */
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO
|
||||
#include "version.h"
|
||||
#include "stack_usage.h"
|
||||
#endif
|
||||
|
||||
_Static_assert(sizeof(app_header_t) == 64u && _Alignof(app_header_t) == 4u,
|
||||
"app_header_t must stay 64 bytes with word alignment");
|
||||
_Static_assert(offsetof(app_header_t, magic) == 0u &&
|
||||
offsetof(app_header_t, hdr_version) == 4u &&
|
||||
offsetof(app_header_t, abi_major) == 6u &&
|
||||
offsetof(app_header_t, api_min) == 7u &&
|
||||
offsetof(app_header_t, code_size) == 8u &&
|
||||
offsetof(app_header_t, code_crc32) == 12u &&
|
||||
offsetof(app_header_t, entry_off) == 16u &&
|
||||
offsetof(app_header_t, flags) == 18u &&
|
||||
offsetof(app_header_t, name) == 20u &&
|
||||
offsetof(app_header_t, version) == 36u &&
|
||||
offsetof(app_header_t, link_vma) == 52u &&
|
||||
offsetof(app_header_t, required_caps) == 56u &&
|
||||
offsetof(app_header_t, asset_size) == 60u &&
|
||||
offsetof(app_header_t, asset_crc) == 62u,
|
||||
"FAP1 field offsets must match existing apps and host tools");
|
||||
_Static_assert(sizeof(app_api_t) <= UINT16_MAX, "app_api_t size field overflow");
|
||||
_Static_assert(APP_ASSET_OFFSET + APP_ASSET_MAX == APP_CODE_OFFSET,
|
||||
"assets must end where the code sector starts");
|
||||
_Static_assert(APP_ASSET_MAX <= APP_OVERLAY_MAX,
|
||||
"assets are CRC-checked through the overlay buffer");
|
||||
|
||||
enum {
|
||||
APP_AVAILABLE_CAPS = 0u
|
||||
#ifdef ENABLE_FMRADIO
|
||||
| APP_CAP_FM
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM
|
||||
| APP_CAP_BEAM2
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO
|
||||
| APP_CAP_SYSINFO
|
||||
#endif
|
||||
};
|
||||
|
||||
/* Keep the small zero-initialized state together so callbacks can address it
|
||||
* from one base. The nonzero RNG seed stays separate to keep this in .bss. */
|
||||
static struct {
|
||||
uint16_t asset_size;
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM
|
||||
uint16_t beam_pending_channel;
|
||||
#endif
|
||||
uint8_t run_slot;
|
||||
uint8_t cfg_len; /* staged length; 0 = nothing to commit */
|
||||
bool allow_screen_saver;
|
||||
bool screen_saver_wake;
|
||||
#ifdef ENABLE_FMRADIO
|
||||
bool fm_dirty;
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM
|
||||
bool beam_dirty;
|
||||
#endif
|
||||
bool shortcuts_cached;
|
||||
uint8_t shortcut_mask;
|
||||
uint8_t shortcut_slots[4];
|
||||
uint8_t slot_revision;
|
||||
uint8_t cfg_buf[16];
|
||||
} app_state;
|
||||
|
||||
/* ---- ABI wrappers: the few resident calls that are not a direct signature match ---- */
|
||||
|
||||
static void app_backlight_on(void)
|
||||
{
|
||||
APP_ModalScreenSaverExit();
|
||||
BACKLIGHT_TurnOn();
|
||||
}
|
||||
|
||||
static void app_backlight_update(void)
|
||||
{
|
||||
APP_ModalBacklightTick(app_state.allow_screen_saver);
|
||||
}
|
||||
|
||||
static uint8_t app_get_key(void)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
/* Overlay apps run synchronously outside APP_Update(). Keep serial key
|
||||
* injection alive while an app owns the foreground loop. */
|
||||
K5VIEWER_ParseInput();
|
||||
#endif
|
||||
const KEY_Code_t key = KEYBOARD_GetKey();
|
||||
|
||||
if (app_state.screen_saver_wake) {
|
||||
if (key == KEY_INVALID)
|
||||
app_state.screen_saver_wake = false;
|
||||
return APP_KEY_INVALID;
|
||||
}
|
||||
|
||||
if (!APP_IsScreenSaverDisplayed()) {
|
||||
if (key != KEY_INVALID)
|
||||
BACKLIGHT_TurnOn(); /* re-arm BLTime on activity, mirrors ProcessKey():
|
||||
overlay apps bypass the resident key handler, so
|
||||
the saver would otherwise fire mid-use. */
|
||||
return (uint8_t)key;
|
||||
}
|
||||
|
||||
if (key == KEY_INVALID)
|
||||
return APP_KEY_SAVER;
|
||||
|
||||
app_backlight_on();
|
||||
if (key == KEY_PTT)
|
||||
return APP_KEY_PTT;
|
||||
|
||||
app_state.screen_saver_wake = true;
|
||||
return APP_KEY_WAKE;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
static void app_blit_full(void)
|
||||
{
|
||||
ST7565_BlitFullScreen();
|
||||
/* The normal loop mirrors completed frames after drawing. Overlay apps
|
||||
* bypass that loop, so publish the frame from this ABI wrapper. */
|
||||
K5VIEWER_Update(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
static int8_t app_nav_dir(uint8_t key)
|
||||
{
|
||||
int8_t direction;
|
||||
|
||||
if (key == KEY_UP)
|
||||
direction = 1;
|
||||
else if (key == KEY_DOWN)
|
||||
direction = -1;
|
||||
else
|
||||
return 0;
|
||||
|
||||
return gEeprom.SET_NAV ? direction : -direction;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO
|
||||
extern uint8_t _eflash_used;
|
||||
extern uint8_t _ebss;
|
||||
#endif
|
||||
static void app_led(bool on) { BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, on); }
|
||||
|
||||
static void app_play_tone(uint16_t tone, uint16_t ms)
|
||||
{
|
||||
BK4819_PrepareToPlayTone(true);
|
||||
AUDIO_AudioPathOn();
|
||||
BK4819_PlayToneRaw(tone, ms);
|
||||
AUDIO_AudioPathOff();
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM
|
||||
/* ---- optional BEAM radio/channel bridge -----------------------------------
|
||||
* The modal app owns the packet format, CRC, UI, state machine and the FSK
|
||||
* modem (plain BK4819 register sequences through bk_read/bk_write). Resident
|
||||
* code only tunes the fixed channel and translates the stable ABI channel
|
||||
* structure, the parts which depend on VFO_Info_t. */
|
||||
static VFO_Info_t app_beam_vfo;
|
||||
static app_beam_channel_t app_beam_pending;
|
||||
|
||||
static void app_beam_prepare(void)
|
||||
{
|
||||
const uint16_t channel = FREQ_CHANNEL_FIRST + BAND6_400MHz;
|
||||
RADIO_InitInfo(&app_beam_vfo, channel, DEFAULT_FREQ);
|
||||
app_beam_vfo.CHANNEL_BANDWIDTH = BANDWIDTH_NARROW;
|
||||
app_beam_vfo.OUTPUT_POWER = OUTPUT_POWER_LOW1;
|
||||
RADIO_ConfigureSquelchAndOutputPower(&app_beam_vfo);
|
||||
|
||||
gRxVfo = &app_beam_vfo;
|
||||
gTxVfo = &app_beam_vfo;
|
||||
gCurrentVfo = &app_beam_vfo;
|
||||
RADIO_SetupRegisters(true);
|
||||
}
|
||||
|
||||
/* Wire<->VFO fields that are a plain one-byte copy in BOTH directions. Fields
|
||||
* that differ in width (frequency, offset, band) or are enum-typed on the VFO
|
||||
* side (modulation, code types, PTT-id) are excluded: enums are int-sized here
|
||||
* (no -fshort-enums), so a byte copy would truncate them. Those stay as the
|
||||
* explicit width-converting assignments below. Driving the byte fields from one
|
||||
* table collapses two near-identical copy blocks into a single shared loop. */
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
#define APP_BEAM_BYTE_FIELDS_DTMF(F) F(dtmf_decoding_enable, DTMF_DECODING_ENABLE)
|
||||
#else
|
||||
#define APP_BEAM_BYTE_FIELDS_DTMF(F)
|
||||
#endif
|
||||
#define APP_BEAM_BYTE_FIELDS(F) \
|
||||
F(rx_code, freq_config_RX.Code) \
|
||||
F(tx_code, freq_config_TX.Code) \
|
||||
F(tx_offset_direction, TX_OFFSET_FREQUENCY_DIRECTION) \
|
||||
F(tx_lock, TX_LOCK) \
|
||||
F(busy_channel_lock, BUSY_CHANNEL_LOCK) \
|
||||
F(output_power, OUTPUT_POWER) \
|
||||
F(channel_bandwidth, CHANNEL_BANDWIDTH) \
|
||||
F(scanlist, SCANLIST_PARTICIPATION) \
|
||||
F(compander, Compander) \
|
||||
APP_BEAM_BYTE_FIELDS_DTMF(F)
|
||||
|
||||
typedef struct { uint8_t wire_off, vfo_off; } app_beam_byte_map_t;
|
||||
|
||||
#define APP_BEAM_MAP_ROW(w, v) { offsetof(app_beam_channel_t, w), offsetof(VFO_Info_t, v) },
|
||||
static const app_beam_byte_map_t app_beam_byte_map[] = {
|
||||
APP_BEAM_BYTE_FIELDS(APP_BEAM_MAP_ROW)
|
||||
};
|
||||
#undef APP_BEAM_MAP_ROW
|
||||
|
||||
/* Widening either side of a mapped field must fail to compile here rather than
|
||||
* silently truncate through the byte copy. */
|
||||
#define APP_BEAM_MAP_CHECK(w, v) \
|
||||
_Static_assert(sizeof(((app_beam_channel_t *)0)->w) == 1u, #w); \
|
||||
_Static_assert(sizeof(((VFO_Info_t *)0)->v) == 1u, #v);
|
||||
APP_BEAM_BYTE_FIELDS(APP_BEAM_MAP_CHECK)
|
||||
#undef APP_BEAM_MAP_CHECK
|
||||
|
||||
_Static_assert(sizeof(VFO_Info_t) <= 256u && sizeof(app_beam_channel_t) <= 256u,
|
||||
"app_beam_byte_map offsets must fit in uint8_t");
|
||||
|
||||
/* Copy every mapped byte field in one direction (to_vfo = save, else export). */
|
||||
static void app_beam_copy_bytes(app_beam_channel_t *wire, VFO_Info_t *vfo, bool to_vfo)
|
||||
{
|
||||
for (unsigned i = 0; i < sizeof(app_beam_byte_map) / sizeof(app_beam_byte_map[0]); i++) {
|
||||
uint8_t *w = (uint8_t *)wire + app_beam_byte_map[i].wire_off;
|
||||
uint8_t *v = (uint8_t *)vfo + app_beam_byte_map[i].vfo_off;
|
||||
if (to_vfo) *v = *w;
|
||||
else *w = *v;
|
||||
}
|
||||
}
|
||||
|
||||
static void app_beam_get(app_beam_channel_t *out)
|
||||
{
|
||||
if (out == NULL)
|
||||
return;
|
||||
memset(out, 0, sizeof(*out));
|
||||
VFO_Info_t *vfo = &gEeprom.VfoInfo[gEeprom.TX_VFO];
|
||||
app_beam_copy_bytes(out, vfo, false); /* plain one-byte fields */
|
||||
out->rx_frequency = vfo->freq_config_RX.Frequency;
|
||||
out->tx_offset_frequency = vfo->TX_OFFSET_FREQUENCY;
|
||||
out->rx_codetype = vfo->freq_config_RX.CodeType;
|
||||
out->tx_codetype = vfo->freq_config_TX.CodeType;
|
||||
out->modulation = vfo->Modulation;
|
||||
out->frequency_reverse = vfo->FrequencyReverse;
|
||||
out->dtmf_ptt_id_mode = vfo->DTMF_PTT_ID_TX_MODE;
|
||||
out->step_setting = vfo->STEP_SETTING;
|
||||
out->band = vfo->Band;
|
||||
if (IS_MR_CHANNEL(vfo->CHANNEL_SAVE))
|
||||
SETTINGS_FetchChannelName(out->name, vfo->CHANNEL_SAVE);
|
||||
else
|
||||
memcpy(out->name, vfo->Name, sizeof(out->name));
|
||||
}
|
||||
|
||||
static uint16_t app_beam_save(const app_beam_channel_t *in)
|
||||
{
|
||||
if (in == NULL)
|
||||
return 0xFFFFu;
|
||||
|
||||
/* Only one external-flash write can be deferred per app run. Preserve the
|
||||
first successfully received channel if an older app tries to queue more. */
|
||||
if (app_state.beam_dirty)
|
||||
return 0xFFFFu;
|
||||
|
||||
uint16_t channel = MR_CHANNEL_FIRST;
|
||||
while (IS_MR_CHANNEL(channel) && RADIO_CheckValidChannel(channel, false, 0))
|
||||
channel++;
|
||||
if (!IS_MR_CHANNEL(channel))
|
||||
return 0xFFFFu;
|
||||
|
||||
/* External flash cannot be written while the overlay executes from its
|
||||
sector-cache RAM. Keep the pointer-free payload separate from the radio
|
||||
VFO: app_beam_prepare() may reuse that VFO before the app returns. */
|
||||
memcpy(&app_beam_pending, in, sizeof(app_beam_pending));
|
||||
app_state.beam_pending_channel = channel;
|
||||
app_state.beam_dirty = true;
|
||||
return channel;
|
||||
}
|
||||
|
||||
/* Called only after the overlay has returned and its code no longer executes
|
||||
* from the PY25Q16 sector cache. */
|
||||
static void app_beam_commit(void)
|
||||
{
|
||||
if (!app_state.beam_dirty)
|
||||
return;
|
||||
app_state.beam_dirty = false;
|
||||
|
||||
const uint16_t channel = app_state.beam_pending_channel;
|
||||
|
||||
/* The overlay has returned, so the temporary radio VFO is now free to
|
||||
become the channel-save staging object. */
|
||||
RADIO_InitInfo(&app_beam_vfo, channel, app_beam_pending.rx_frequency);
|
||||
app_beam_copy_bytes(&app_beam_pending, &app_beam_vfo, true);
|
||||
app_beam_vfo.TX_OFFSET_FREQUENCY = app_beam_pending.tx_offset_frequency;
|
||||
app_beam_vfo.freq_config_RX.CodeType = app_beam_pending.rx_codetype;
|
||||
app_beam_vfo.freq_config_TX.CodeType = app_beam_pending.tx_codetype;
|
||||
app_beam_vfo.Modulation = app_beam_pending.modulation;
|
||||
app_beam_vfo.FrequencyReverse = app_beam_pending.frequency_reverse;
|
||||
app_beam_vfo.DTMF_PTT_ID_TX_MODE = app_beam_pending.dtmf_ptt_id_mode;
|
||||
app_beam_vfo.STEP_SETTING = app_beam_pending.step_setting < STEP_N_ELEM
|
||||
? app_beam_pending.step_setting : STEP_12_5kHz;
|
||||
app_beam_vfo.StepFrequency = gStepFrequencyTable[app_beam_vfo.STEP_SETTING];
|
||||
memcpy(app_beam_vfo.Name, app_beam_pending.name, sizeof(app_beam_vfo.Name));
|
||||
app_beam_vfo.Name[sizeof(app_beam_vfo.Name) - 1u] = '\0';
|
||||
|
||||
SETTINGS_SaveChannel(channel, gEeprom.TX_VFO, &app_beam_vfo, 3);
|
||||
#ifndef ENABLE_KEEP_MEM_NAME
|
||||
SETTINGS_SaveChannelName(channel, app_beam_vfo.Name);
|
||||
#endif
|
||||
|
||||
gEeprom.MrChannel[gEeprom.TX_VFO] = channel;
|
||||
gEeprom.ScreenChannel[gEeprom.TX_VFO] = channel;
|
||||
RADIO_ConfigureChannel(gEeprom.TX_VFO, VFO_CONFIGURE_RELOAD);
|
||||
RADIO_SelectVfos();
|
||||
RADIO_SetupRegisters(true);
|
||||
PY25Q16_InvalidateCache();
|
||||
}
|
||||
|
||||
static void app_beam_draw(const char *status)
|
||||
{
|
||||
UI_DisplayStatus();
|
||||
UI_DisplayMain();
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
const uint8_t line = (gEeprom.DUAL_WATCH == DUAL_WATCH_OFF &&
|
||||
gEeprom.CROSS_BAND_RX_TX == CROSS_BAND_OFF) ? 5u : 3u;
|
||||
#else
|
||||
const uint8_t line = 3u;
|
||||
#endif
|
||||
memset(gFrameBuffer[line], 0, LCD_WIDTH);
|
||||
UI_PrintStringSmallBold(status, 2, LCD_WIDTH - 1u, line);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ---- radio wrappers ---- */
|
||||
static int16_t app_rssi_dbm(void) { return BK4819_GetRSSI_dBm() + dBmCorrTable[gRxVfo->Band]; }
|
||||
static uint16_t app_bk_read(uint8_t r) { return BK4819_ReadRegister((BK4819_REGISTER_t)r); }
|
||||
static void app_bk_write(uint8_t r, uint16_t v) { BK4819_WriteRegister((BK4819_REGISTER_t)r, v); }
|
||||
static void app_set_af(uint8_t m) { BK4819_SetAF((BK4819_AF_Type_t)m); }
|
||||
static void app_audio_path(bool on){ if (on) AUDIO_AudioPathOn(); else AUDIO_AudioPathOff(); }
|
||||
static void app_prepare_tone(void) { BK4819_PrepareToPlayTone(true); }
|
||||
static void app_play_tone_raw(uint16_t hz, uint16_t ms) { BK4819_PlayToneRaw(hz, ms); }
|
||||
static uint32_t app_rx_freq(void) { return gRxVfo->pRX->Frequency; }
|
||||
|
||||
/* ---- v2 config (deferred, flash-backed) ----
|
||||
* Stored per app slot in the header sector, just after the 64-byte header. cfg_load
|
||||
* reads flash at launch (ReadBuffer bypasses the overlay cache). cfg_save only stages
|
||||
* into RAM - the app runs from the sector cache, so it cannot write flash itself; the
|
||||
* loader commits the staged bytes to flash after the app returns (RMW preserves the
|
||||
* slot header), tagged with the app's name. Updating an app erases its slot: the
|
||||
* tagged config is kept across that erase and handed back to the app of that name
|
||||
* only, so an update keeps the app's settings while another app installed in the
|
||||
* slot starts from its own defaults. */
|
||||
#define APP_CFG_OFFSET 0x40u /* config area within the header sector */
|
||||
#define APP_CFG_OWNER 0x50u /* name of the app the config belongs to */
|
||||
|
||||
static void app_cfg_load(uint8_t *buf, uint8_t len)
|
||||
{
|
||||
if (len > sizeof(app_state.cfg_buf)) len = sizeof(app_state.cfg_buf);
|
||||
const uint32_t base = APP_SLOT_BASE(app_state.run_slot);
|
||||
char name[APP_NAME_LEN], owner[APP_NAME_LEN];
|
||||
PY25Q16_ReadBuffer(base + offsetof(app_header_t, name), name, sizeof(name));
|
||||
PY25Q16_ReadBuffer(base + APP_CFG_OWNER, owner, sizeof(owner));
|
||||
/* This app's config, or an untagged one saved before the tag existed.
|
||||
* Another app's reads as erased flash, i.e. the app's defaults. */
|
||||
if ((uint8_t)owner[0] == 0xFFu || !memcmp(owner, name, sizeof(name)))
|
||||
PY25Q16_ReadBuffer(base + APP_CFG_OFFSET, buf, len);
|
||||
else
|
||||
memset(buf, 0xFF, len);
|
||||
}
|
||||
static void app_cfg_save(const uint8_t *buf, uint8_t len)
|
||||
{
|
||||
if (len > sizeof(app_state.cfg_buf)) len = sizeof(app_state.cfg_buf);
|
||||
memcpy(app_state.cfg_buf, buf, len);
|
||||
app_state.cfg_len = len; /* mark dirty; the loader commits after the app returns */
|
||||
}
|
||||
|
||||
_Static_assert(APP_CFG_OFFSET + sizeof(app_state.cfg_buf) <= APP_CFG_OWNER &&
|
||||
APP_CFG_OWNER + APP_NAME_LEN <= APP_ASSET_OFFSET,
|
||||
"config area overlaps its owner tag or the assets");
|
||||
|
||||
/* ---- API level 2: time, randomness, read-only assets ---- */
|
||||
static uint32_t app_rng_state = 0x2545F491u;
|
||||
|
||||
static uint32_t app_ticks_ms(void)
|
||||
{
|
||||
return SCHEDULER_GetTick10ms() * 10u;
|
||||
}
|
||||
|
||||
static uint32_t app_rand32(void)
|
||||
{
|
||||
uint32_t x = app_rng_state;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
app_rng_state = x;
|
||||
return x;
|
||||
}
|
||||
|
||||
/* Fold fresh entropy into the persistent state: RSSI noise LSBs, the SysTick
|
||||
* phase of the key press that launched the app, and the 10 ms counter. */
|
||||
static void app_rng_mix(void)
|
||||
{
|
||||
app_rng_state ^= ((uint32_t)BK4819_ReadRegister(BK4819_REG_67) << 16) ^
|
||||
SysTick->VAL ^ SCHEDULER_GetTick10ms();
|
||||
if (app_rng_state == 0u)
|
||||
app_rng_state = 0x2545F491u;
|
||||
app_rand32();
|
||||
}
|
||||
|
||||
static uint16_t app_asset_read(uint16_t offset, void *buf, uint16_t len)
|
||||
{
|
||||
if (offset >= app_state.asset_size)
|
||||
return 0;
|
||||
if (len > app_state.asset_size - offset)
|
||||
len = app_state.asset_size - offset;
|
||||
PY25Q16_ReadBuffer(APP_SLOT_BASE(app_state.run_slot) + APP_ASSET_OFFSET + offset, buf, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
/* ---- API level 2: integer division ----
|
||||
* The run-time helpers the firmware links anyway (App/compact_div.S over
|
||||
* libgcc's unsigned division), served as they are: their {quotient, remainder}
|
||||
* register pair is declared as one 64-bit return value (low word = r0). */
|
||||
extern uint64_t __aeabi_idivmod(int32_t n, int32_t d);
|
||||
extern uint64_t __aeabi_uidivmod(uint32_t n, uint32_t d);
|
||||
|
||||
/* ---- v2 battery / backlight ---- */
|
||||
static void app_draw_battery(void)
|
||||
{
|
||||
char t[8];
|
||||
UI_DrawStatusBattery(gStatusLine, t);
|
||||
}
|
||||
static void app_battery_sample(void)
|
||||
{
|
||||
BATTERY_Sample(false);
|
||||
}
|
||||
|
||||
/* ---- v2 TX (beacon) ---- */
|
||||
static uint8_t app_tx_state(void)
|
||||
{
|
||||
if (TX_freq_check(gTxVfo->pTX->Frequency) != 0 && gTxVfo->TX_LOCK) return 1; /* TX disable */
|
||||
if (gBatteryDisplayLevel == 0) return 2; /* battery low */
|
||||
if (gBatteryDisplayLevel > 6) return 3; /* voltage high */
|
||||
if (gTxVfo->Modulation != MODULATION_FM) return 1;
|
||||
return 0;
|
||||
}
|
||||
static void app_tx_tone(uint16_t hz) { BK4819_TransmitTone(false, hz); }
|
||||
static void app_tx_mute(bool on) { if (on) BK4819_EnterTxMute(); else BK4819_ExitTxMute(); }
|
||||
static void app_tx_end(void) { BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); RADIO_SetupRegisters(true); }
|
||||
static void app_tx_carrier(bool on) { BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, on); }
|
||||
static uint32_t app_tx_freq(void) { return gTxVfo->pTX->Frequency; }
|
||||
static void app_boot_callsign(char *buf, uint8_t len)
|
||||
{
|
||||
char raw[12]; uint8_t n = 0;
|
||||
PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, raw, sizeof(raw));
|
||||
for (uint8_t i = 0; i < sizeof(raw) && (uint8_t)(n + 1) < len; i++) {
|
||||
char c = raw[i];
|
||||
if (c == '\0' || (uint8_t)c == 0xFFu) break;
|
||||
if (c >= 'a' && c <= 'z') c -= 32;
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/') buf[n++] = c;
|
||||
}
|
||||
buf[n] = '\0';
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
/* ---- v2 broadcast FM (BK1080), sovereign (no BK4819 dual-watch) ---- */
|
||||
static void app_fm_enter(uint16_t f, uint8_t b)
|
||||
{
|
||||
BK1080_Init(f, b);
|
||||
BK4819_PickRXFilterPathBasedOnFrequency(10320000); /* FM band antenna filter */
|
||||
AUDIO_AudioPathOn();
|
||||
gEnableSpeaker = true;
|
||||
}
|
||||
static void app_fm_exit(void)
|
||||
{
|
||||
AUDIO_AudioPathOff();
|
||||
gEnableSpeaker = false;
|
||||
BK1080_Init0();
|
||||
BK4819_PickRXFilterPathBasedOnFrequency(gRxVfo->pRX->Frequency); /* restore RX filter */
|
||||
}
|
||||
static int8_t app_fm_valid(uint16_t f, uint16_t lo) { return (int8_t)FM_CheckFrequencyLock(f, lo); }
|
||||
|
||||
static void app_fm_state(app_fm_state_t *s, bool write)
|
||||
{
|
||||
if (write) {
|
||||
gEeprom.FM_FrequencyPlaying = s->freq_playing;
|
||||
gEeprom.FM_SelectedFrequency = s->sel_freq;
|
||||
gEeprom.FM_Band = s->band & 3u;
|
||||
gEeprom.FM_IsMrMode = s->is_mr ? true : false;
|
||||
gEeprom.FM_SelectedChannel = s->sel_ch;
|
||||
} else {
|
||||
s->freq_playing = gEeprom.FM_FrequencyPlaying;
|
||||
s->sel_freq = gEeprom.FM_SelectedFrequency;
|
||||
s->band = gEeprom.FM_Band;
|
||||
s->is_mr = gEeprom.FM_IsMrMode;
|
||||
s->sel_ch = gEeprom.FM_SelectedChannel;
|
||||
}
|
||||
}
|
||||
static void app_fm_commit(void) { app_state.fm_dirty = true; }
|
||||
#endif
|
||||
|
||||
/* Internal callers discard the header on failure, so read directly into their
|
||||
* aligned object without another 64-byte stack object and copy. */
|
||||
static uint8_t app_read_validated_header(uint8_t slot, app_header_t *h)
|
||||
{
|
||||
if (slot >= APP_SLOT_COUNT)
|
||||
return APP_ERR_SLOT;
|
||||
|
||||
PY25Q16_ReadBuffer(APP_SLOT_BASE(slot), h, sizeof(*h));
|
||||
|
||||
if (h->magic != APP_MAGIC) return APP_ERR_MAGIC;
|
||||
if (h->hdr_version != APP_HDR_VERSION) return APP_ERR_MAGIC;
|
||||
if (h->abi_major != APP_ABI_MAJOR || h->api_min == 0u ||
|
||||
h->api_min > APP_API_LEVEL) return APP_ERR_ABI;
|
||||
if (!(h->flags & APP_FLAG_COMMITTED)) return APP_ERR_NOT_COMMITTED;
|
||||
if (h->required_caps & ~APP_AVAILABLE_CAPS) return APP_ERR_CAP;
|
||||
if (h->code_size < 2u || h->code_size > APP_OVERLAY_MAX ||
|
||||
(uint32_t)h->entry_off > h->code_size - 2u || /* leave room for a 2-byte Thumb insn */
|
||||
(h->entry_off & 1u) != 0u || /* entry must be Thumb-aligned (even) */
|
||||
h->asset_size > APP_ASSET_MAX)
|
||||
return APP_ERR_SIZE;
|
||||
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header)
|
||||
{
|
||||
app_header_t h;
|
||||
const uint8_t rc = app_read_validated_header(slot, &h);
|
||||
/* Preserve the public API: a failed validation leaves out_header intact. */
|
||||
if (rc == APP_OK && out_header)
|
||||
*out_header = h;
|
||||
return rc;
|
||||
}
|
||||
|
||||
uint8_t APP_SlotRevision(void)
|
||||
{
|
||||
return app_state.slot_revision;
|
||||
}
|
||||
|
||||
void APP_NotifySlotChanged(void)
|
||||
{
|
||||
app_state.shortcuts_cached = false;
|
||||
app_state.slot_revision++;
|
||||
}
|
||||
|
||||
static int8_t app_shortcut_index(uint8_t shortcut)
|
||||
{
|
||||
if (shortcut == APP_SHORTCUT_FM) return 0;
|
||||
if (shortcut == APP_SHORTCUT_FOXHUNT) return 1;
|
||||
if (shortcut == APP_SHORTCUT_BEACON) return 2;
|
||||
if (shortcut == APP_SHORTCUT_BEAM) return 3;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void app_cache_shortcuts(void)
|
||||
{
|
||||
if (app_state.shortcuts_cached)
|
||||
return;
|
||||
|
||||
app_state.shortcut_mask = 0;
|
||||
const uint32_t overlay_vma = (uint32_t)PY25Q16_OverlayBuffer();
|
||||
|
||||
for (uint8_t slot = 0; slot < APP_SLOT_COUNT; slot++) {
|
||||
app_header_t h;
|
||||
if (app_read_validated_header(slot, &h) != APP_OK || h.link_vma != overlay_vma)
|
||||
continue;
|
||||
|
||||
const uint8_t shortcut = (uint8_t)((h.flags & APP_FLAG_SHORTCUT_MASK) >>
|
||||
APP_FLAG_SHORTCUT_SHIFT);
|
||||
const int8_t index = app_shortcut_index(shortcut);
|
||||
if (index >= 0) {
|
||||
if (!(app_state.shortcut_mask & shortcut)) {
|
||||
app_state.shortcut_mask |= shortcut;
|
||||
app_state.shortcut_slots[index] = slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app_state.shortcuts_cached = true;
|
||||
}
|
||||
|
||||
uint8_t APP_OverlayShortcutMask(void)
|
||||
{
|
||||
app_cache_shortcuts();
|
||||
return app_state.shortcut_mask;
|
||||
}
|
||||
|
||||
uint8_t APP_LaunchOverlayShortcut(uint8_t shortcut)
|
||||
{
|
||||
const int8_t index = app_shortcut_index(shortcut);
|
||||
if (index < 0)
|
||||
return APP_ERR_MAGIC;
|
||||
|
||||
app_cache_shortcuts();
|
||||
return (app_state.shortcut_mask & shortcut)
|
||||
? APP_LaunchOverlay(app_state.shortcut_slots[index])
|
||||
: APP_ERR_MAGIC;
|
||||
}
|
||||
|
||||
uint8_t APP_SlotInfo(uint8_t slot, app_header_t *out_header)
|
||||
{
|
||||
if (slot >= APP_SLOT_COUNT)
|
||||
return APP_ERR_SLOT;
|
||||
app_header_t local;
|
||||
app_header_t *h = out_header ? out_header : &local;
|
||||
/* Slot info returns the raw header even when its magic is invalid. */
|
||||
PY25Q16_ReadBuffer(APP_SLOT_BASE(slot), h, sizeof(*h));
|
||||
return (h->magic == APP_MAGIC) ? APP_OK : APP_ERR_MAGIC;
|
||||
}
|
||||
|
||||
/* All services are immutable. Keeping the table in flash avoids rebuilding a
|
||||
* roughly quarter-kilobyte automatic object on every launch and removes that
|
||||
* object from the launcher's stack frame. Callbacks must also obey the ABI's
|
||||
* no-external-flash-write rule while entry() is running. */
|
||||
static const app_api_t app_api = {
|
||||
.abi_major = APP_ABI_MAJOR,
|
||||
.api_level = APP_API_LEVEL,
|
||||
.api_size = sizeof(app_api_t),
|
||||
.fb = gFrameBuffer,
|
||||
.display_clear = UI_DisplayClear,
|
||||
.status_clear = UI_StatusClear,
|
||||
.draw_line = UI_DrawLineBuffer,
|
||||
.draw_rect = UI_DrawRectangleBuffer,
|
||||
.print_bold = UI_PrintStringSmallBold,
|
||||
.print_tiny = GUI_DisplaySmallest,
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
.blit_full = app_blit_full,
|
||||
#else
|
||||
.blit_full = ST7565_BlitFullScreen,
|
||||
#endif
|
||||
.blit_line = ST7565_BlitLine,
|
||||
.blit_status = ST7565_BlitStatusLine,
|
||||
.get_key = app_get_key,
|
||||
.delay_ms = SYSTEM_DelayMs,
|
||||
.play_tone = app_play_tone,
|
||||
.led = app_led,
|
||||
.print_normal = UI_PrintStringSmallNormal,
|
||||
.print_inverse = GUI_DisplaySmallestInverse,
|
||||
.display_freq = UI_DisplayFrequency,
|
||||
.rssi_dbm = app_rssi_dbm,
|
||||
.bk_read = app_bk_read,
|
||||
.bk_write = app_bk_write,
|
||||
.set_agc = BK4819_SetAGC,
|
||||
.set_af = app_set_af,
|
||||
.audio_path = app_audio_path,
|
||||
.prepare_tone = app_prepare_tone,
|
||||
.play_tone_raw = app_play_tone_raw,
|
||||
.tones_off_rx = BK4819_TurnsOffTones_TurnsOnRX,
|
||||
.rx_freq = app_rx_freq,
|
||||
.cfg_load = app_cfg_load,
|
||||
.cfg_save = app_cfg_save,
|
||||
.draw_battery = app_draw_battery,
|
||||
.battery_sample = app_battery_sample,
|
||||
.backlight_on = app_backlight_on,
|
||||
.backlight_update = app_backlight_update,
|
||||
.audio_scope = UI_DisplayAudioScopeOverlay,
|
||||
.status_line = gStatusLine,
|
||||
.tx_state = app_tx_state,
|
||||
.tx_set_params = RADIO_SetTxParameters,
|
||||
.tx_tone = app_tx_tone,
|
||||
.tx_mute = app_tx_mute,
|
||||
.tx_end = app_tx_end,
|
||||
.tx_carrier = app_tx_carrier,
|
||||
.tx_freq = app_tx_freq,
|
||||
.boot_callsign = app_boot_callsign,
|
||||
.print_string = UI_PrintString,
|
||||
#ifdef ENABLE_FMRADIO
|
||||
.fm_enter = app_fm_enter,
|
||||
.fm_exit = app_fm_exit,
|
||||
.fm_set_freq = BK1080_SetFrequency,
|
||||
.fm_lo = BK1080_GetFreqLoLimit,
|
||||
.fm_hi = BK1080_GetFreqHiLimit,
|
||||
.fm_mute = BK1080_Mute,
|
||||
.fm_valid = app_fm_valid,
|
||||
.fm_channels = gFM_Channels,
|
||||
.fm_state = app_fm_state,
|
||||
.fm_commit = app_fm_commit,
|
||||
#endif
|
||||
.nav_dir = app_nav_dir,
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM
|
||||
.beam_prepare = app_beam_prepare,
|
||||
/* APP_CAP_BEAM2: beam_leave, beam_send, beam_rx and beam_rx_poll stay
|
||||
* NULL, the app drives the FSK modem through bk_read/bk_write. */
|
||||
.beam_get = app_beam_get,
|
||||
.beam_save = app_beam_save,
|
||||
.beam_draw = app_beam_draw,
|
||||
#endif
|
||||
.ticks_ms = app_ticks_ms,
|
||||
.rand32 = app_rand32,
|
||||
.asset_read = app_asset_read,
|
||||
.idivmod = __aeabi_idivmod,
|
||||
.uidivmod = __aeabi_uidivmod,
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO
|
||||
.sys_edition = Edition,
|
||||
.sys_version = DisplayVersion,
|
||||
.sys_build_date = BuildDate,
|
||||
.sys_build_time = BuildTime,
|
||||
.sys_build_commit = BuildCommit,
|
||||
.sys_flash_end = &_eflash_used,
|
||||
.sys_ram_end = &_ebss,
|
||||
.sys_battery_voltage = &gBatteryVoltageAverage,
|
||||
.sys_battery_type = &gEeprom.BATTERY_TYPE,
|
||||
.sys_battery_percent = BATTERY_VoltsToPercent,
|
||||
.sys_storage_read = PY25Q16_ReadBuffer,
|
||||
.sys_stack_free_now = STACK_FreeNow,
|
||||
.sys_stack_free_min = STACK_FreeMinimum,
|
||||
#endif
|
||||
};
|
||||
|
||||
uint8_t APP_LaunchOverlay(uint8_t slot)
|
||||
{
|
||||
app_header_t h;
|
||||
uint8_t rc = app_read_validated_header(slot, &h);
|
||||
if (rc != APP_OK)
|
||||
return rc;
|
||||
|
||||
/* The app's absolute data references only resolve if it runs at the exact
|
||||
* VMA it was linked for. The overlay VMA varies with the firmware's RAM
|
||||
* layout (per preset/features), so the app records its link VMA and we
|
||||
* refuse a mismatch cleanly instead of jumping into misaddressed code. */
|
||||
uint8_t *ws = PY25Q16_OverlayBuffer();
|
||||
if (h.link_vma != (uint32_t)ws)
|
||||
return APP_ERR_VMA;
|
||||
|
||||
/* Flush and suspend RF logging before the sector cache becomes executable
|
||||
* app code. Both a pending RX and an app-owned TX could otherwise write a
|
||||
* log entry through the same 4 KiB buffer and overwrite the running app. */
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
RXTX_LOG_Suspend();
|
||||
#endif
|
||||
|
||||
/* Repurpose the sector cache: drop any cached config sector, load the code
|
||||
* straight in (ReadBuffer bypasses the cache), and verify it in RAM before
|
||||
* trusting it. Zeroing first leaves the app's .bss clean. The assets are
|
||||
* verified first through the same buffer: a slot written by a host that
|
||||
* does not know the asset area (older UV Studio) is refused here instead of
|
||||
* handing the app unprogrammed flash. */
|
||||
PY25Q16_InvalidateCache();
|
||||
bool assets_ok = true;
|
||||
if (h.asset_size) {
|
||||
PY25Q16_ReadBuffer(APP_SLOT_BASE(slot) + APP_ASSET_OFFSET, ws, h.asset_size);
|
||||
assets_ok = (uint16_t)MB_Crc32Bytes(ws, h.asset_size) == h.asset_crc;
|
||||
}
|
||||
memset(ws, 0, APP_OVERLAY_MAX);
|
||||
PY25Q16_ReadBuffer(APP_SLOT_BASE(slot) + APP_CODE_OFFSET, ws, h.code_size);
|
||||
|
||||
if (!assets_ok || MB_Crc32Bytes(ws, h.code_size) != h.code_crc32) {
|
||||
PY25Q16_InvalidateCache();
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
RXTX_LOG_Resume();
|
||||
#endif
|
||||
return APP_ERR_CRC;
|
||||
}
|
||||
|
||||
/* Ensure every store to the overlay is visible before we branch into it. */
|
||||
__DSB();
|
||||
__ISB();
|
||||
|
||||
app_state.run_slot = slot; /* for cfg_load / cfg_save / asset_read */
|
||||
app_state.asset_size = h.asset_size;
|
||||
app_state.cfg_len = 0;
|
||||
app_rng_mix();
|
||||
#ifdef ENABLE_FMRADIO
|
||||
app_state.fm_dirty = false;
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM
|
||||
app_state.beam_dirty = false;
|
||||
#endif
|
||||
|
||||
app_state.allow_screen_saver = (h.flags & APP_FLAG_SCREEN_SAVER) != 0;
|
||||
app_state.screen_saver_wake = false;
|
||||
APP_ModalScreenSaverExit();
|
||||
BACKLIGHT_TurnOn();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
/* The caller enters from a debounced key event, so the resident key state
|
||||
* still contains that trigger while the modal app is running. Clear it so
|
||||
* K5Viewer is allowed to mirror overlay frames immediately. */
|
||||
gKeyReading0 = KEY_INVALID;
|
||||
gKeyReading1 = KEY_INVALID;
|
||||
#endif
|
||||
|
||||
/* Pin RX to the user-selected VFO before the app runs. Under dual watch
|
||||
* gRxVfo is whichever VFO the receiver was parked on when F+7 was pressed,
|
||||
* so an RF app (FoxHunt, a future S-meter, ...) would measure and display a
|
||||
* VFO the user did not pick - sometimes A, sometimes B. Point RX at the
|
||||
* selected (TX) VFO and retune so rx_freq(), rssi_dbm() and the tuned
|
||||
* hardware all agree on the selected channel. gCurrentVfo follows too:
|
||||
* RADIO_SetTxParameters keys it, and dual watch may have left it on the
|
||||
* other VFO, so a TX app (APRS TX, Beacon, SSTV) would transmit there while
|
||||
* tx_freq() shows the selected one. Save all three pointers:
|
||||
* radio apps such as BEAM temporarily replace them while they run. */
|
||||
const uint8_t saved_rx_vfo = gEeprom.RX_VFO;
|
||||
VFO_Info_t *const saved_rx = gRxVfo;
|
||||
VFO_Info_t *const saved_tx = gTxVfo;
|
||||
VFO_Info_t *const saved_current = gCurrentVfo;
|
||||
gEeprom.RX_VFO = gEeprom.TX_VFO;
|
||||
gRxVfo = gTxVfo;
|
||||
gCurrentVfo = gTxVfo;
|
||||
RADIO_SetupRegisters(true);
|
||||
|
||||
app_entry_t entry = (app_entry_t)(((uint32_t)ws + h.entry_off) | 1u);
|
||||
entry(&app_api);
|
||||
|
||||
APP_ModalScreenSaverExit();
|
||||
app_state.allow_screen_saver = false;
|
||||
app_state.screen_saver_wake = false;
|
||||
|
||||
/* Restore the resident RX/dual-watch tuning the app ran on top of. */
|
||||
gEeprom.RX_VFO = saved_rx_vfo;
|
||||
gRxVfo = saved_rx;
|
||||
gTxVfo = saved_tx;
|
||||
gCurrentVfo = saved_current;
|
||||
RADIO_SetupRegisters(true);
|
||||
|
||||
/* The overlay held app code, not a valid config sector. */
|
||||
PY25Q16_InvalidateCache();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM
|
||||
app_beam_commit();
|
||||
#endif
|
||||
|
||||
/* Commit any deferred config the app staged (RMW keeps the slot header),
|
||||
* tagged with the app's name. */
|
||||
if (app_state.cfg_len) {
|
||||
const uint32_t base = APP_SLOT_BASE(slot);
|
||||
PY25Q16_WriteBuffer(base + APP_CFG_OFFSET, app_state.cfg_buf, app_state.cfg_len, false);
|
||||
PY25Q16_WriteBuffer(base + APP_CFG_OWNER, h.name, APP_NAME_LEN, false);
|
||||
PY25Q16_InvalidateCache();
|
||||
}
|
||||
#ifdef ENABLE_FMRADIO
|
||||
/* Commit the FM config + 48 channels the app edited (shared with resident FM). */
|
||||
if (app_state.fm_dirty) {
|
||||
app_state.fm_dirty = false;
|
||||
SETTINGS_SaveFM();
|
||||
PY25Q16_InvalidateCache();
|
||||
}
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
RXTX_LOG_Resume();
|
||||
#endif
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
uint8_t APP_SlotErase(uint8_t slot)
|
||||
{
|
||||
if (slot >= APP_SLOT_COUNT)
|
||||
return APP_ERR_SLOT;
|
||||
uint32_t base = APP_SLOT_BASE(slot);
|
||||
/* An update erases the slot before the host rewrites it: keep the config
|
||||
* and its owner tag across the erase (an untagged config is given the
|
||||
* installed app's name), so that the same app finds its settings again. */
|
||||
uint8_t keep[APP_CFG_OWNER + APP_NAME_LEN - APP_CFG_OFFSET];
|
||||
uint8_t *const owner = keep + (APP_CFG_OWNER - APP_CFG_OFFSET);
|
||||
app_header_t h;
|
||||
PY25Q16_ReadBuffer(base + APP_CFG_OFFSET, keep, sizeof(keep));
|
||||
if (*owner == 0xFFu && app_read_validated_header(slot, &h) == APP_OK)
|
||||
memcpy(owner, h.name, APP_NAME_LEN);
|
||||
for (uint32_t off = 0; off < APP_SLOT_STRIDE; off += APP_SECTOR_SIZE)
|
||||
PY25Q16_SectorErase(base + off);
|
||||
PY25Q16_InvalidateCache();
|
||||
if (*owner != 0xFFu) {
|
||||
PY25Q16_WriteBuffer(base + APP_CFG_OFFSET, keep, sizeof(keep), false);
|
||||
PY25Q16_InvalidateCache();
|
||||
}
|
||||
APP_NotifySlotChanged();
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
uint8_t APP_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len)
|
||||
{
|
||||
if (slot >= APP_SLOT_COUNT)
|
||||
return APP_ERR_SLOT;
|
||||
if (offset > APP_SLOT_STRIDE || len > APP_SLOT_STRIDE - offset)
|
||||
return APP_ERR_SIZE;
|
||||
PY25Q16_WriteBuffer(APP_SLOT_BASE(slot) + offset, data, len, false);
|
||||
PY25Q16_InvalidateCache();
|
||||
app_state.shortcuts_cached = false;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
#endif /* ENABLE_FEAT_F4HWN_OVERLAY_APPS */
|
||||
@@ -0,0 +1,163 @@
|
||||
/* 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 ]
|
||||
*
|
||||
* Header sector layout (slot-relative):
|
||||
*
|
||||
* 0x000 app_header_t (64 B)
|
||||
* 0x040 per-app config staged by cfg_save (16 B)
|
||||
* 0x050 name of the app that config belongs to (16 B, reserved up to 0x0FF);
|
||||
* kept with the config when the slot is erased for an update
|
||||
* 0x100 read-only assets (API level 2, up to 3840 B, CRC-checked at launch)
|
||||
* 0x1000 code (<= 4 KiB, copied into the overlay RAM)
|
||||
*
|
||||
* 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 */
|
||||
|
||||
/* ---- read-only assets, served by api->asset_read (API level 2) ----
|
||||
* Literal values so pack_app.py can parse them; checked against the slot
|
||||
* geometry by static asserts in app_overlay.c. */
|
||||
#define APP_ASSET_OFFSET 0x00000100u /* assets start in the header sector */
|
||||
#define APP_ASSET_MAX 0x00000F00u /* up to the end of the header sector */
|
||||
|
||||
/* ---- 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_EXIT_TO_MAIN 0x0004u /* leave the Apps menu when the app returns */
|
||||
#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
|
||||
/* Retired: the v6.0.0 BEAM bridge (resident FSK send/receive). No longer
|
||||
* advertised, so a v6.0.0 Beam.app is refused with APP_ERR_CAP. */
|
||||
#define APP_CAP_BEAM 0x00000004u
|
||||
#define APP_CAP_SYSINFO 0x00000008u
|
||||
/* BEAM channel bridge only (beam_prepare/get/save/draw); the app drives the
|
||||
* FSK modem itself through bk_read/bk_write. */
|
||||
#define APP_CAP_BEAM2 0x00000010u
|
||||
|
||||
/* Aligned RAM objects; app_overlay.c pins every serialized field offset. */
|
||||
typedef struct {
|
||||
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 */
|
||||
uint16_t asset_size; /* bytes at APP_ASSET_OFFSET, 0 = none */
|
||||
uint16_t asset_crc; /* low 16 bits of the assets' CRC-32 */
|
||||
} 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 or asset_size out of range */
|
||||
APP_ERR_CRC, /* code or asset CRC 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);
|
||||
|
||||
/* Change token used by the modal Apps selector. A completed host install is
|
||||
* published after validation; erasing a slot publishes directly. */
|
||||
uint8_t APP_SlotRevision(void);
|
||||
void APP_NotifySlotChanged(void);
|
||||
|
||||
|
||||
#endif /* APPS_APP_OVERLAY_H */
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Shared 20x20 monochrome APRS symbol assets.
|
||||
|
||||
Each icon is stored in native ST7565 page order: 20 bytes for each of three
|
||||
8-pixel pages. The masks below fill rows 0-19; they are packed one pixel
|
||||
lower (rows 1-20, the low five bits of the third page), so the icon sits one
|
||||
pixel below the top of its three display lines. This lets the overlay apps
|
||||
copy an icon straight from external flash to the framebuffer.
|
||||
|
||||
The masks were validated pixel by pixel with
|
||||
aprsrx/test/aprs_symbols_20_editor.html and are packed here from its export
|
||||
aprsrx/test/aprs_symbols_20_edited_v2.json.
|
||||
"""
|
||||
|
||||
WIDTH = 20
|
||||
PAGES = 3
|
||||
BYTES_PER_ICON = WIDTH * PAGES
|
||||
|
||||
CODES = (
|
||||
"/#", "/X", "/&", "/Y", "/'", "/[", "/-", "/\\",
|
||||
"/.", "/^", "/0", "/_", "/:", "/a", "/;", "/b",
|
||||
"/<", "/f", "/>", "/g", "/C", "/j", "/E", "/k",
|
||||
"/I", "/m", "/K", "/O", "/P", "/s", "/R", "/v",
|
||||
"/y", "/T", "/U", "/V", "\\#", "\\&", "\\-", "/W",
|
||||
"\\.", "\\0", "E0", "I0", "W0", "YY", "\\^", "\\Y",
|
||||
)
|
||||
|
||||
_HEX = """
|
||||
00000000000000c0f0fcf0c000000000000000000001030787cfffffff7fffffffcf8707030100000000000607030101000000010103070600000000
|
||||
800060606020202020a0f0a020202020606060001b1f0e04040c1c3e7fff7f737f7af27c380000000000000000000000010303030303030201000000
|
||||
000000001c04047c04c4fcc4047c04041c00000000000000000080f0fccfcfcffcf08000000000000000000000040707050404040507070400000000
|
||||
0000000000000080e000fefcf0c000000000000000008080a0b8bebfbf80ffdfdfdfdfdcd0c060200000050505050505050505050505050505000000
|
||||
8080000000040c38f0e0c0800000000000000000203f1f0e0e0e0e8eeeff7f3f1f0e0e0e0e0e0e040000000000040603010000000000000000000000
|
||||
0000000080c0c0cedfdfdfdfcec0c08000000000000000001f1f00ffff3f3fffff001f1f000000000000000000000007070000070700000000000000
|
||||
0000000000000080c0c28a2afe2a0a0200000000000000f8fcfe1f1fffff1f1f1ffcf800000000000000000707070707070704040407070000000000
|
||||
00000030f090101010101010101090f0300000000000000000030e38e08080e0380e0300000000000000000000000000000303000000000000000000
|
||||
00000078f8f0e0c080000080c0e0f0f878000000000000c0e0f1fb7f3f1f1f3f7ffbf1e0c00000000000000303010000000000000000010303000000
|
||||
000000000080c0e0fefffffee0c08000000000001c0c0e07071f1f07ffffffff071f1f07070e0c1c0000000000060707030707030707060000000000
|
||||
0080e070180c0c0606060606060c0c1870e08000001f7fe0800000000000000000000080e07f1f000000000001030306060606060603030100000000
|
||||
00007c381010d0d010386c6c000000000000000000000000181cffff1c180000007cfc7c000000000000000000000707020202020202030000000000
|
||||
00000000008000c0f0fcfee08080e0000000000000000038feffff1f0f01031f9ffff9fc3e0000000000000000010302000000020303010000000000
|
||||
0080c0c0c0c0c04040c0c0c0f0f87840c0800000007f3f9fdf9939606079797f7f3f9cdc9c3d7e7c0000000103010000000000000000010301000000
|
||||
0000000000000000c0f0c0000000000000000000000000c0f0f8fe3f0f070f3ffef8e0c0000000000001010101010101010101010101010101010100
|
||||
0000000000000040c0400000101090e0800000000070882474248c722127190905738924772488700000000101010000000000000000000101010000
|
||||
00000000000000000000008898f0c000000000003249b5b54933061c3c3c3f1f073349b5b54932000000000000000000000000000000000000000000
|
||||
00008080c040406020203010101888888880c0807e3e9fdf9f3f9fdf9f3f617f7c7c3f98d898397f0000010301000103010000000000000103010000
|
||||
000000008080c0c0c0c0c0c0c080000000000000787c1c4fefed4c1c3c3f3c3c1d4feeec4c1c7c780000000000000000000000000000000000000000
|
||||
0000000000000040c0c0800000000000808000000c1830606070703838191f0f0e060703030101000000000000000000000000000000000000000000
|
||||
00000000000000000000000080c07038381000000e1f1f1e9cdc6c34180c161b1d1c1c1c1e1f1f0e0002070703000000000000000000000000000000
|
||||
000000000000000000e02020e0202020608000003e3e0e66f6660e1e1f1f1c1c1f0e66f6660f1f3e0000000000000000000000000000000000000000
|
||||
000080c06030101888c8c88818103060c080000000070f18306040c78f9f9f8fc7406030180f07000000000000000000000000000000000000000000
|
||||
e0e0e0e0e0e0e0e0e0e0e0e0e0e00080808000003f1fcfefcf1f3f1fcfefcf1f3f3f13cfe8c81f3c0000000100000000000100000000000001000000
|
||||
000000000000007c44c4c4447c000000000000000000e0203c24e4040407070404e4243c20e000000000010101010100000000000001010101010000
|
||||
0000f80601f88c0070f0f070008cf80106f8000000000003040001e0b8cfcfb8e0010004030000000000000000020303020202020303020000000000
|
||||
0000000000000080c0e0fee6c680000000000000000000f8fc1e1fffff1c1c1cffff1f1efcf800000000000303030303030000000303030303030000
|
||||
0000e0f8fc0ce6fafefefefefae60cfcf8e0000000000003070e1927df3f3fdf27190e07030000000000000000000003070707070300000000000000
|
||||
000000000000240840606c604008240000000000703898d8dc9e3b79797f7f79793b9edcd89838780000000101000000000000000000000101000000
|
||||
00000000000000000000c0d0180c0c0e0e00000000103070787c76767f777f777f777c78703000000000010101010101010101010101010101010000
|
||||
0080c0e0e0e0e0e0e0e0e0e0e0606060e0c00000003f1fc8e8c8183f3f2020203f3f19c9ebce1c380000000001000000000000000000000001000000
|
||||
00c0c0e0e0e0e0e0e0e0e0e0e06060c080000000003f1fcfefcf1f3f3f3f3f3f3f1ccceccd1f3e3c0000000001000000000000000000000100000000
|
||||
00e0000000c0000000c0000000c0000000c00000000f01010107010101ff010101070101010701000000000000000000000300000000000000000000
|
||||
00e03090d050505254585854525050d09030e00000ff803f6040424e4240424c424040603f80ff000000010103030301010101010103030301010000
|
||||
00e03030f03030f03030f03030f0303070f0e000003f0e66f7660e3f3e3e3f3e0e67f6640c3f3f3c0000000000000000000000000000000000000000
|
||||
00000080c0c08000000080c8f8e0c080000000003049b5b54933071f1f1f1f1f073349b5b54932000000000000000000000000000000000000000000
|
||||
0000c07018080c0486e2e286040c081870c00000000f30e08003677f3f1f1f3f7f670380e0300f000000000001010302060404060203010100000000
|
||||
0000c07018088cc4e6f2f2e6c48c081870c00000000f30e0860f1f3f7fffff7f3f1f0f86e0300f000000000001010302060404060203010100000000
|
||||
0000000000000082c2ca8a2aff2a0a0a02020000000000f8fcfe1f1fffff1f1f1ffcf800000000000000000707070707070704040407070000000000
|
||||
000080c0e0e0f0f8fcfcfefcf8f8f0f0e0c0000000000143630707436307074363070743630100000000000000020300000203000002030000000000
|
||||
000000000070787c1c1c1cfcf8f00000000000000000000000000000dcdedf0701000000000000000000000000000000010101000000000000000000
|
||||
80e0781c0c86c6e3e3e3e3e3e3c6860c1c78e0801f7fe080001f3f7f7f7f7f7f7f3f1f0080e07f1f000001030306060c0c0c0c0c0c06060303010000
|
||||
00000000000000c06c1e1e6cc00000000000000000000080e0360f0f060000060f0f36e0800000000000030707030000000000000000030707030000
|
||||
0000000000000080dc3e3edc8000000000000000000000c0e0f8ce838080808083cef8e0c00000000000000103030100000000000001030301000000
|
||||
000080603028fc2c26fe262cfc2830608000000000000f3262a7faa222ff22a2faa762320f0000000000000000000101030303010100000000000000
|
||||
000000000000c06070607f60c080000000000000000000000000ff5455fd5554ff0f0000000000000000000000000103030303030100000000000000
|
||||
008000000000001090f0f0901000000000008000030703030303034347ffff4743030303030307030000000000000000000000000000000000000000
|
||||
00000000000080c0e0c0fefe8000000000000000000000000000ffa8faaafaa8ffff0000000000000000000000000102030203020301000000000000
|
||||
"""
|
||||
|
||||
def _lowered(masks):
|
||||
"""Shift every 24-row column down by one pixel (bit 0 of page 0 is the top)."""
|
||||
out = bytearray(len(masks))
|
||||
for icon in range(0, len(masks), BYTES_PER_ICON):
|
||||
for x in range(WIDTH):
|
||||
column = sum(masks[icon + page * WIDTH + x] << (8 * page) for page in range(PAGES))
|
||||
if column >> (8 * PAGES - 1):
|
||||
raise ValueError("APRS symbol mask has no free row to move down")
|
||||
column <<= 1
|
||||
for page in range(PAGES):
|
||||
out[icon + page * WIDTH + x] = (column >> (8 * page)) & 0xFF
|
||||
return bytes(out)
|
||||
|
||||
BITMAPS = _lowered(bytes.fromhex(_HEX))
|
||||
|
||||
if len(CODES) != 48 or len(BITMAPS) != len(CODES) * BYTES_PER_ICON:
|
||||
raise ValueError("invalid APRS 20x20 symbol table")
|
||||
@@ -0,0 +1,184 @@
|
||||
# APRS RX: on-radio APRS receiver (work in progress)
|
||||
|
||||
Goal: an overlay app that receives APRS (AX.25 UI frames, Bell 202 AFSK 1200
|
||||
bauds) on the UV-K1 / UV-K5 v3 and keeps the last five frames, shown one at a time.
|
||||
|
||||
Status:
|
||||
|
||||
| Step | State |
|
||||
|---|---|
|
||||
| Hardware demodulation by the BK4829 FSK block | **Dead end** (2026-09-25, FT3D on 144.800: the chip has no Bell 202 demodulator) |
|
||||
| Audio path an app can sample | **Done** by EPIRB 406: RX audio on PA4, ADC channel 4 at 9.6 kHz |
|
||||
| Integer demodulator, modelled on synthetic audio (`test/model_rx.py`) | **Done** (see below) |
|
||||
| Flipper Zero test transmitter (`test/flipper_aprs.py`) | **Done**, files in `test/flipper/` |
|
||||
| Radio app (`aprsrx_app.c`) | **v0.1 receives the Flipper frames on the radio**; v0.3 decodes Mic-E (on air: F5RAV via F1PRY-14); v0.4 decodes continuously with 3 slicers (on air: F1PRY-14 via F5KTR-3); v0.5 shows standard positions; v0.6 adds the speaker key; v0.7 adds the corrected 20x20 APRS symbol bitmaps; v0.8 saves the speaker setting; v0.9 scrolls long compact frames by 1 px |
|
||||
| Bench test with the Flipper on 433.650 MHz | **Done** (2026-09-30, v0.1: `_long` 10/10 in STD; `_badfcs` not shown) |
|
||||
| Real station: FT3D beacon on 144.800 MHz | **Done** (2026-09-30, v0.1: Mic-E frame `>TXUPX9` received, FCS good, = 48°50.89' N 2°16.25' E) |
|
||||
| Mic-E decoding (`test/mice.py`: spec encoder vs the app's decoder) | **Done** in the model: 6 cases + the FT3D frame |
|
||||
|
||||
## Using the app
|
||||
|
||||
1. Set the VFO to the APRS frequency, **FM**: 144.800 MHz, or 433.650 MHz for the
|
||||
Flipper test files.
|
||||
2. Launch **APRS RX**.
|
||||
3. The speaker is off by default (v0.6): the decoder does not need it, only the
|
||||
BK4829 AF output (tested on the radio: PA4 joins the audio before the
|
||||
amplifier). Key 1 turns it on to listen to the channel, as FoxHunt's audio
|
||||
key; since v0.8 the app keeps it as you left it.
|
||||
|
||||
Since v0.4 the app decodes **continuously**, as a TNC does: no RSSI trigger,
|
||||
the FCS and a UI-frame check (control 0x03, PID 0xF0) sort frames from noise.
|
||||
Up to v0.3, captures started 10 dB above a tracked noise floor; on a busy
|
||||
144.800 that floor crept up (−132 → −104 dBm), so weak stations were never
|
||||
sampled. Several frames in one transmission are now received too.
|
||||
|
||||
Keys and the screen cost sample time (the key scan alone is ~400 µs), so they
|
||||
are served every 50 ms only while no slicer is inside a preamble (two
|
||||
back-to-back flags) or a frame whose first bytes look like a callsign (busy
|
||||
~4 % of the time on noise, in the model), and after 3 s of busy at the latest.
|
||||
The screen is redrawn after a new frame, a key, or every 5 s.
|
||||
|
||||
The RAM history holds five frames, newest first; a new frame replaces the
|
||||
oldest when the history is full. The screen shows one frame at a time: its
|
||||
callsign in the large bold font, fixed on line 0, and its body scrolled under it
|
||||
(y = 8 to 30). At the right of the callsign, once several frames are kept, a capsule
|
||||
tells which frame is shown: `2/5`, the second newest of five kept (`1` is the
|
||||
newest). Left of it (or alone at the edge with a single frame), a second capsule
|
||||
shows the count of frames received. The symbol and RSSI are those of the frame shown. A new frame is
|
||||
shown at once, from its first row. UP/DOWN held scrolls the body and stops at
|
||||
its first / last row; pressing the key again there shows the newer (UP) or older
|
||||
(DOWN) frame from its first row, so holding a key never leaves the frame. Body
|
||||
rows occupy eight pixels in normal mode (3 rows at rest) or six in compact mode
|
||||
(4 rows). History is lost on exit; no external-flash journal is used.
|
||||
|
||||
| Screen | Content |
|
||||
|---|---|
|
||||
| Status bar | `APRS RX` title, then a `WAIT` capsule until the first frame, the speaker icon while the speaker is on, then ▲ while rows or a newer frame are hidden above and ▼ while rows or an older frame are hidden below (x = 72-76 as in APRS TX, the speaker icon at x = 59-68; both drawn in one asset read) |
|
||||
| Lines 0-3 | One frame: source call in bold, fixed on line 0, with two capsules at the right (the frames received, then with several frames kept `2/5`, frame shown / frames kept, 1 = newest; x >= 89); under it the body in the small font (18 characters a row, non-ASCII shown as `.`), 3 rows at a time, scrolled pixel by pixel with UP/DOWN (v0.7): `>DEST,DIGI*,...` (as many path entries as fit two rows), then the info field, whole. For Mic-E (most Yaesu/Kenwood beacons, v0.3): latitude `48 50.89N`, longitude `002 16.25E`, speed km/h, course and symbol, the message type (`Off Duty`, `En Route`...), then altitude and comment without the device markers; for an uncompressed position (`!` `=` `/` `@`, v0.5): latitude, longitude, then the timestamp if any, the symbol (table + code) and the comment. Compressed positions stay raw text. A new frame is shown at once. Key * switches to the compact view (below) |
|
||||
| Lines 4-6 | Under a dotted separator (y=31), the frequency drawn as the main screen draws it (big digits up to the kHz, the last two in the small font), aligned left. A matching 20x20 bitmap from the 48-symbol Yaesu set is shown at x=108, y=33-52, on the right for decoded Mic-E and uncompressed positions; the area stays blank for an unknown or unavailable symbol |
|
||||
| Bottom row y=49 | `-89dBm`: RSSI at the end of the frame shown (the count of frames received moved to the capsule on line 0). (v0.4-v0.5 also showed frames per slicer, `sl a/b/c`: on air the outer slicers found frames too; dropped in v0.6 to fit the speaker key) |
|
||||
|
||||
Keys (UV-K5 and UV-K1):
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| UP/DOWN (held) | Scroll the body of the frame shown 1 px per 50 ms slot (UV-K1: LEFT/RIGHT, as `nav_dir`), down to its last row |
|
||||
| UP/DOWN (pressed again at the first / last row) | Show the newer / older frame, from its first row |
|
||||
| * | Normal / compact view, saved on exit, returns to the first row of the frame shown. The callsign stays large and bold on line 0. Body rows use the normal font (18 characters) or 3x5 font (32 characters), continuous 1 px scrolling |
|
||||
| 1 | Speaker on/off, saved on exit (v0.8; off by default); FoxHunt's speaker icon in the status bar while on |
|
||||
| 2 | Clear all five frames and the counters |
|
||||
| EXIT | Quit |
|
||||
|
||||
The receive path is the firmware's own (STD: 300 Hz high-pass, de-emphasis,
|
||||
3 kHz low-pass). Up to v0.3, key 1 switched to RAW (filters and AFC off, as
|
||||
EPIRB 406); every on-air decode was made in STD and the model shows no gain
|
||||
for RAW, so v0.4 dropped it to fit the 4 KiB overlay.
|
||||
|
||||
The five frame buffers and their rotating pointers live on `app_main`'s stack;
|
||||
the demodulator state lives in `listen`. Frames are copied only once on receipt;
|
||||
rotation moves pointers, not payloads. Buffers and pointers take 1690 bytes on
|
||||
the MCU, 1360 bytes more than the former 330-byte buffer (676 more than three
|
||||
frames), excluding compiler
|
||||
stack alignment and spill slots. The 4 KiB overlay also holds `.bss`. The screen
|
||||
texts, symbol bitmaps and the two cos tables are assets. The display loads a
|
||||
252-byte block containing 36 bytes of UI labels, 88 bytes of Mic-E messages and
|
||||
the 128-byte Mic-E lookup. Compared with the original label-only read, this adds
|
||||
212 temporary stack bytes and replaces character-decoding branches and separate
|
||||
message reads.
|
||||
|
||||
The front-end initial state is stored in a 116-byte asset immediately before
|
||||
the cosine tables, allowing one read to initialize `dem_t`. This replaces the
|
||||
zeroing and initial-value assignments without adding a RAM buffer. Total assets
|
||||
occupy 3832 of the 3840 available bytes.
|
||||
|
||||
## Demodulator (`test/model_rx.py`, class `Demod`; the C is a transcription)
|
||||
|
||||
Per ADC sample (9.6 kHz, 8 samples per bit):
|
||||
|
||||
1. DC removal (1-pole tracker, 64 samples).
|
||||
2. Band-pass biquad centred on √(1200·2200) = 1625 Hz, Q 0.9 (Q14): equal gain
|
||||
on both tones, removes the out-of-band noise the box correlators let through.
|
||||
3. Four sliding correlators over one bit: I/Q at 1200 Hz (8-entry cos table) and
|
||||
2200 Hz (48 entries = 11 cycles). The sine is the cos table read 3/4 period
|
||||
ahead (+6, +12). Products `>> 8`, so the decision below never overflows int32.
|
||||
4. Magnitudes `max + 3/8 min`, per-tone peak trackers (attack 1/32, decay
|
||||
1/1024): each tone normalised by its own peak, so the twist (pre-emphasis or
|
||||
not, de-emphasis or not) does not bias the decision much.
|
||||
5. **3 slicers** (since v0.4), decision `sign(wa·Mm·Ps − wb·Ms·Pm)` with
|
||||
wa:wb = 2:3, 1:1, 3:2, each with its own DPLL, NRZI and HDLC below (Direwolf's
|
||||
multi-slicer idea): the AGC does not cancel a strong twist in noise. A frame
|
||||
from any slicer is accepted; a copy from another slicer within 1 s is dropped.
|
||||
6. DPLL: 65536 per bit, a bit at each wrap; each transition pulls the phase 1/4 of
|
||||
the way toward mid-bit + half a sample (the transition is only seen at the
|
||||
next sample: without that half-sample, a +1 % clock failed long frames).
|
||||
7. NRZI, HDLC (flag, destuffing, abort after 7 ones), CRC-16/X.25 per byte,
|
||||
frame accepted on the residue 0xF0B8 **and** as a UI frame: noise brings ~0.7
|
||||
candidates per second to the FCS check, so without the UI check a false frame
|
||||
would pass about once a day.
|
||||
|
||||
### Slicers (`test/variants.py`, 4 seeds, 24 hard cases: noise 3000-4000 Hz, ±1 % clock, twist −9 to +9 dB at noise 2500 Hz, RAW and STD)
|
||||
|
||||
| Variant | Frames /96 |
|
||||
|---|---|
|
||||
| 1 slicer (v0.1-v0.3) | 57 |
|
||||
| 3 slicers, weights ×2 | 65 |
|
||||
| **3 slicers, weights ×1.5 (v0.4)** | **71** |
|
||||
| 5 slicers (×1.5 and ×2) | 71 |
|
||||
|
||||
The gain is on strong twist: sine AFSK at +6 / +9 dB in RAW goes from 0/4 to 4/4.
|
||||
Noise only (30 s, 2 seeds, 50 and 400 LSB rms): no frame accepted.
|
||||
|
||||
### Model results (`test/model_rx.py`, 3 seeds per case, 1 slicer; 3 slicers: all ≥ these)
|
||||
|
||||
Channel: discriminator output + white noise (rms over 48 kHz) → radio audio path
|
||||
(RAW: 5 kHz low-pass; STD: 300 Hz high-pass, 750 µs de-emphasis, 3 kHz low-pass)
|
||||
→ 12-bit ADC at 9.6 kHz, 0.065 LSB/Hz (EPIRB 406 measurement), optional clock
|
||||
error. 150 ms of no-carrier noise before and after each burst.
|
||||
|
||||
| Case | RAW | STD |
|
||||
|---|---|---|
|
||||
| Flipper, clean: `pos`, `long` (233 bytes) | 3/3, 3/3 | 3/3, 3/3 |
|
||||
| Flipper `badfcs` (must be rejected) | 0 frames | 0 frames |
|
||||
| Flipper, noise 1500 / 3000 / 4500 Hz | 3/3, 2/3, 0/3 | 3/3, 2/3, 0/3 |
|
||||
| Flipper, ±4 kHz carrier offset | 3/3 | 3/3 |
|
||||
| Flipper `long`, ±1 % sample clock | 3/3 | 3/3 (−1 %), 3/3 (+1 %) |
|
||||
| Sine AFSK (a real station), twist −6 / 0 / +6 dB, noise 1500 | 3/3, 3/3, 2/3 | 2/3, 3/3, 3/3 |
|
||||
|
||||
Noise 4500 Hz is Eb/N0 ≈ 9.6 dB: the theoretical frame success of non-coherent
|
||||
FSK on a 520-bit frame is then ~6 %, so the failures there are expected.
|
||||
(At ±0.5 % clock, 6/6 in every variant tried.)
|
||||
|
||||
## Flipper Zero test transmitter (`test/flipper_aprs.py`)
|
||||
|
||||
The Flipper's CC1101 cannot put an audio tone on FM, only switch between two
|
||||
frequencies (preset `2FSKDev238Async`, ±2.38 kHz). Each AFSK tone is therefore
|
||||
sent as a square wave: the carrier toggles at twice the tone frequency,
|
||||
phase-continuous across bits, with edge times rounded from exact values (no
|
||||
drift). The receiver's discriminator outputs that square wave and its audio
|
||||
low-pass leaves mostly the fundamental. The Flipper cannot reach 2 m anyway; the
|
||||
script refuses 144-146 MHz.
|
||||
|
||||
```
|
||||
test/flipper_aprs.py test/flipper [--call F4HWN] [--freq 433650000]
|
||||
```
|
||||
|
||||
writes `aprs_pos.sub`, `_digi`, `_msg`, `_status`, `_long` (233 bytes, 2 s) and
|
||||
`_badfcs` (one info bit flipped: must not be shown). 100 ms of carrier, 40 flags
|
||||
(267 ms), the frame, 3 flags, 10 ms of carrier. Copy them to `subghz/` on the
|
||||
Flipper, set the radio to 433.650 MHz FM, launch the app, then Send one file at
|
||||
a time, about 1 s apart. (Up to v0.4, UP/DOWN tuned ±5 kHz for a Flipper whose
|
||||
crystal puts the carrier off the channel; never needed on the bench, dropped in
|
||||
v0.5 to fit 4 KiB.)
|
||||
|
||||
`test/ax25.py` builds the frames (shared by the generator and the model);
|
||||
`test/mice.py` checks the Mic-E display decoder against a spec encoder, and
|
||||
the standard position display against real frames;
|
||||
`test/variants.py` compares demodulator variants on the hard cases (dev tool).
|
||||
|
||||
## Bench checklist
|
||||
|
||||
- Level reaching the ADC (`pp`, shown up to v0.4, dropped in v0.5 to make
|
||||
room): estimated ~300 LSB peak-to-peak for the Flipper (±2.38 kHz at
|
||||
0.065 LSB/Hz), **measured 734-778** with the Flipper and **916** with a real
|
||||
station (F1PRY-14): ~20 % of full scale, plenty of headroom, and the per-tone
|
||||
AGC makes the absolute level irrelevant.
|
||||
@@ -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,
|
||||
"APRS RX overlay app overflows the 4 KiB overlay")
|
||||
|
||||
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||
}
|
||||
@@ -0,0 +1,800 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* APRS RX - receives APRS (AX.25 UI frames, Bell 202 AFSK 1200 bauds) in
|
||||
* software and keeps the last five frames, shown one at a time: source,
|
||||
* destination and path, the info field. Tune the VFO (FM; 144.800 MHz, or 433.650 MHz for the Flipper Zero
|
||||
* bench files of test/flipper_aprs.py), then launch.
|
||||
*
|
||||
* The BK4829 has no Bell 202 demodulator (see README.md), so the RX audio is
|
||||
* sampled as EPIRB 406 does: it reaches PA4 (the voice DAC pin), held at
|
||||
* mid-scale by the MCU DAC (unbuffered), and is read on ADC channel 4 at
|
||||
* 9.6 kHz, timed from SysTick, continuously, as a TNC does: no RSSI gate, the
|
||||
* FCS and the UI check sort frames from noise. Each sample goes through the
|
||||
* demodulator modelled and tested in test/model_rx.py: band-pass, 1200/2200 Hz
|
||||
* sliding correlators over one bit, per-tone AGC, then 3 slicers (mark/space
|
||||
* weights 2:3, 1:1, 3:2), each with its DPLL, NRZI, HDLC and FCS. Keep the two
|
||||
* in step.
|
||||
* Keys and the screen cost sample time (the key scan alone is ~400 us), so they
|
||||
* are served every 50 ms only while no slicer is inside a preamble or a
|
||||
* plausible frame; the redraw follows a new frame, a key, or every 5 s.
|
||||
*
|
||||
* Keys (UV-K5 and UV-K1): UP/DOWN (held) scroll the frame 1 px per slot when
|
||||
* needed; pressed again at either end: the newer / older frame · * normal
|
||||
* view / compact view (saved) · 1 speaker on/off (saved, off by default: the
|
||||
* decoder does not need it) · 2 clear · EXIT quit.
|
||||
* The loader re-runs RADIO_SetupRegisters on exit; the app restores the ADC,
|
||||
* PA4, the DAC and its clock itself.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "aprsrx_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
/* ---- MCU registers (PY32F071, core and SysTick at 48 MHz, 10 ms period) ----
|
||||
* The ADC and DAC registers are reached from one base per block: the base goes
|
||||
* through an empty asm, so GCC addresses them as [base, #offset] from one
|
||||
* literal-pool word instead of one word per register address. */
|
||||
static inline volatile uint32_t *hw(uint32_t a){
|
||||
volatile uint32_t *p=(volatile uint32_t *)a;
|
||||
__asm__("" : "+l"(p));
|
||||
return p;
|
||||
}
|
||||
#define SYST_LOAD (*(volatile uint32_t *)0xE000E014u)
|
||||
#define SYST_VAL (*(volatile uint32_t *)0xE000E018u)
|
||||
#define ADC_SR (hw(0x40012400u)[0x00/4])
|
||||
#define ADC_CR2 (hw(0x40012400u)[0x08/4])
|
||||
#define ADC_SMPR3 (hw(0x40012400u)[0x14/4])
|
||||
#define ADC_SQR3 (hw(0x40012400u)[0x38/4])
|
||||
#define ADC_DR (hw(0x40012400u)[0x50/4])
|
||||
#define GPIOA_MODER (*(volatile uint32_t *)0x50000000u)
|
||||
#define DAC_CR (hw(0x40007400u)[0x00/4])
|
||||
#define DAC_SWTRIGR (hw(0x40007400u)[0x04/4])
|
||||
#define DAC_DHR12R1 (hw(0x40007400u)[0x08/4])
|
||||
#define RCC_APBENR1 (*(volatile uint32_t *)0x4002103Cu)
|
||||
#define RCC_DACEN (1u << 29)
|
||||
/* DAC channel 1 on, output buffer off, software trigger (TSEL1 = 111) */
|
||||
#define DAC_CR_BIAS ((1u << 0) | (1u << 1) | (1u << 2) | (7u << 3))
|
||||
#define BIAS_CODE 2048u
|
||||
#define ADC_SR_EOC (1u << 1)
|
||||
#define ADC_CR2_START ((1u << 22) | (1u << 20)) /* SWSTART | EXTTRIG */
|
||||
#define SMP8_POS 24u
|
||||
#define SMP4_POS 12u
|
||||
#define ADC_CH_PA4 4u
|
||||
#define FS 9600u
|
||||
#define CYC_PER_SAMPLE (48000000u / FS) /* 5000 */
|
||||
#define HOUSE_EVERY 480u /* samples between key/screen slots (50 ms) */
|
||||
#define BUSY_MAX 60u /* serve them anyway after 60 busy slots (3 s) */
|
||||
#define REFRESH_SLOTS 100u /* 100 housekeeping slots = 5 s (battery redraw) */
|
||||
#define REDRAW_ON 1u /* g.redraw: a key or the periodic refresh */
|
||||
#define REDRAW_FRAME 2u /* g.redraw: a new frame, also wakes the backlight */
|
||||
#define DUP_MS 1000u /* the same frame from another slicer */
|
||||
|
||||
/* ---- BK4829 ---- */
|
||||
|
||||
/* ---- demodulator (test/model_rx.py, class Demod) ---- */
|
||||
#define BP_B0 5356 /* band-pass 1625 Hz, Q 0.9, Q14 (b2 = -b0, b1 = 0) */
|
||||
#define BP_A1 (-10714)
|
||||
#define BP_A2 5673
|
||||
#define AGC_ATT 5 /* peak tracker: attack 1/32 ... */
|
||||
#define AGC_DEC 10 /* ... decay 1/1024 per sample */
|
||||
#define PK_MIN 16
|
||||
#define PLL_STEP 8192 /* 65536 per bit / 8 samples per bit */
|
||||
#define PLL_CTR 36864 /* transitions pulled to mid-bit + half a sample */
|
||||
#define PLL_SHIFT 2 /* 1/4 of the error per transition */
|
||||
#define NSL 3 /* slicers */
|
||||
#define PRE_BITS 24u /* busy for 3 bytes after two back-to-back flags */
|
||||
|
||||
/* ---- AX.25 ---- */
|
||||
#define FRAME_MAX 330u /* 10 addresses + control + PID + 256 info + FCS */
|
||||
#define FRAME_MIN 18u /* 2 addresses + control + PID + FCS */
|
||||
#define FCS_GOOD 0xF0B8u
|
||||
#define HISTORY 5u
|
||||
|
||||
typedef struct {
|
||||
uint16_t len;
|
||||
int16_t rssi;
|
||||
uint8_t buf[FRAME_MAX];
|
||||
} frame_t;
|
||||
|
||||
/* the front end shared by the slicers */
|
||||
typedef struct {
|
||||
int32_t dc; /* DC tracker, Q4 */
|
||||
int32_t x1, x2, y1, y2; /* band-pass state */
|
||||
int32_t im, qm, is, qs; /* sliding correlator sums */
|
||||
int32_t pm, ps; /* per-tone magnitude peaks (AGC) */
|
||||
uint32_t r, ks; /* ring slot = 1200 Hz phase, 2200 Hz phase;
|
||||
words: dem_t is inlined on the stack, where
|
||||
only words have an sp-relative load/store */
|
||||
int16_t ring[8][4]; /* the products of the last bit */
|
||||
int8_t cm[8], cs[48]; /* cos tables (assets) */
|
||||
} dem_t;
|
||||
/* listen() loads both tables in one asset_read: they must stay back to back,
|
||||
* in the assets as in dem_t. */
|
||||
_Static_assert(COS2200==COS1200+COS1200_LEN &&
|
||||
sizeof(((dem_t *)0)->cm)==COS1200_LEN &&
|
||||
sizeof(((dem_t *)0)->cs)==COS2200_LEN &&
|
||||
offsetof(dem_t,cs)==offsetof(dem_t,cm)+COS1200_LEN,
|
||||
"cos tables must stay contiguous");
|
||||
_Static_assert(BIAS_CODE==2048u && offsetof(dem_t,pm)==36u &&
|
||||
offsetof(dem_t,ps)==40u &&
|
||||
offsetof(dem_t,cm)==DEMOD_INIT_LEN &&
|
||||
COS1200==DEMOD_INIT+DEMOD_INIT_LEN &&
|
||||
sizeof(dem_t)==DEMOD_INIT_LEN+COS1200_LEN+COS2200_LEN,
|
||||
"demodulator initialization asset must match dem_t");
|
||||
|
||||
/* one slicer: decision weights, DPLL, NRZI, HDLC */
|
||||
typedef struct {
|
||||
int32_t dprev, phase; /* last decision, DPLL phase (65536/bit) */
|
||||
uint16_t n, crc; /* bytes, running FCS */
|
||||
uint8_t wa, wb, last; /* mark/space weights, last level */
|
||||
uint8_t sr, ones, byte, nb; /* HDLC: shift reg, 1 run, byte, bit count */
|
||||
uint8_t inframe, hdr; /* between flags, first bytes look like a call */
|
||||
uint8_t bits; /* bits since the last flag */
|
||||
bool pre; /* this flag came right after another one */
|
||||
uint8_t buf[FRAME_MAX];
|
||||
} sl_t;
|
||||
|
||||
/* The app state in one struct: a function loads one base address instead of one
|
||||
* literal-pool word per global. Bytes, then halfwords, then words, so every field
|
||||
* stays within the Thumb load/store immediate ranges (ldrb <= 31, ldrh <= 62,
|
||||
* ldr <= 124); the ADC registers shared with housekeeping go last.
|
||||
* The loader zeroes the overlay (.bss) at every launch. */
|
||||
static struct {
|
||||
uint8_t prevKey, redraw;
|
||||
bool running;
|
||||
uint8_t count, cur; /* history count, frame shown (0 = newest) */
|
||||
uint8_t symTable, symCode; /* symbol of the frame shown; table zero never
|
||||
matches an asset */
|
||||
uint8_t cw, spk; /* the saved settings, in cfg order: view (0 scroll,
|
||||
CW_COMPACT compact), speaker amplifier on (key 1) */
|
||||
uint8_t refresh; /* housekeeping slots since the last redraw */
|
||||
uint16_t vrow; /* y of the next body row before scrolling */
|
||||
uint16_t top, lim, nOk; /* pixel scroll of the body, its limit, frames */
|
||||
const app_api_t *A;
|
||||
char *text; /* formatting buffer (app_main's stack) */
|
||||
frame_t **history; /* newest first; buffers live on app_main's stack */
|
||||
uint32_t tPrev, tCyc; /* SysTick cycle counter */
|
||||
uint32_t tFrame; /* ticks_ms of the last frame */
|
||||
uint32_t savedSqr3, savedSmpr3;
|
||||
} g;
|
||||
#define str g.text
|
||||
|
||||
/* ---- tiny freestanding helpers: GCC may turn the zeroing / copy loops into calls ---- */
|
||||
void *memset(void *d, int c, size_t n) {
|
||||
uint8_t *p = d; while (n) p[--n] = (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) { --n; p[n] = q[n]; } return d;
|
||||
}
|
||||
|
||||
/* ---- formatting ---- */
|
||||
/* Share the string-copy loop across the display formatters. */
|
||||
__attribute__((noinline))
|
||||
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
|
||||
/* Formatting by repeated subtraction: no division. Values printed stay below 100000. */
|
||||
static unsigned sub(uint32_t *v,uint32_t d){ unsigned q=0; while(*v>=d){ *v-=d; q++; } return q; }
|
||||
static const uint16_t P10[]={10000u,1000u,100u,10u,1u};
|
||||
static char *puti(char *o,int32_t v){
|
||||
uint32_t u;
|
||||
if(v<0){*o++='-';u=(uint32_t)(-v);} else u=(uint32_t)v;
|
||||
bool lead=false;
|
||||
for(unsigned i=0;i<5u;i++){
|
||||
unsigned c=sub(&u,P10[i]);
|
||||
if(c||lead||i==4u){ *o++=(char)('0'+c); lead=true; }
|
||||
}
|
||||
return o;
|
||||
}
|
||||
/* A tiny row holds 32 characters (4 px each): print_tiny does not clip, so
|
||||
* every row built below stays within that. */
|
||||
static void tiny(uint8_t y,char *end){ *end='\0'; g.A->print_tiny(str,0,y,false,true); }
|
||||
|
||||
#define WAIT_CAPS_X 40u /* "WAIT" capsule in the status bar, after the title */
|
||||
#define SPK_X 59u /* speaker icon (FoxHunt's, x = 59-68) after the capsule
|
||||
(x <= 56), then the scroll marks at x = 72-76 as in APRS
|
||||
TX, before the battery text (x >= 83): BMP_TAIL in the
|
||||
assets, both in one read */
|
||||
|
||||
/* One frame at a time: its source in bold on line 0, its body scrolled under
|
||||
* it, y = 8 to 30, in one of two views (key *):
|
||||
* - normal (g.cw = 0): 18 characters per row, 8 px apart, 3 rows at rest;
|
||||
* - compact (g.cw = CW_COMPACT): 32 tiny characters per row, 6 px apart, 4 rows. */
|
||||
#define ROW_CHARS 18u
|
||||
#define CAPS_END 127u /* last column of the capsules at the right of the
|
||||
source (x >= 89 with "65535" and "2/5") */
|
||||
#define CW_COMPACT 14u /* compact rows: ROW_CHARS + 14 = 32 characters */
|
||||
|
||||
/* The frequency (10 Hz units) drawn as the main screen draws it (ui/main.c,
|
||||
* ENABLE_BIG_FREQ): the big digits up to the kHz on lines 4-5, the last two
|
||||
* digits in the small font on line 5 right after them. The group is aligned
|
||||
* left to reserve the rightmost 20 columns for the APRS symbol. Big digits are
|
||||
* 13 px (drawn from +2), the point 3 px. No division. As APRS TX, without its
|
||||
* 1 GHz case (the main screen's small font there): 1296.000 still fits. */
|
||||
__attribute__((noinline))
|
||||
static void drawFreq(uint32_t f){
|
||||
const app_api_t *A=g.A;
|
||||
unsigned m=0;
|
||||
while(f>=100000u){ f-=100000u; m++; }
|
||||
char *o=puti(str,m);
|
||||
*o++='.';
|
||||
for(unsigned i=0;i<5u;i++) *o++=(char)('0'+sub(&f,P10[i]));
|
||||
*o='\0';
|
||||
o-=2; /* last two digits */
|
||||
A->print_normal(o,(uint8_t)((o-str)*13u-10u),0,5);
|
||||
*o='\0';
|
||||
A->display_freq(str,0,4,false); /* "144.800" */
|
||||
}
|
||||
|
||||
#define SYM_X 108u
|
||||
static void drawSymbol(void){
|
||||
const app_api_t *A=g.A;
|
||||
uint8_t hit[3];
|
||||
uint8_t slot=(uint8_t)((2u*g.symTable+13u*g.symCode)&127u);
|
||||
A->asset_read((uint16_t)(SYM_MAP+slot*3u),hit,sizeof hit);
|
||||
if(!hit[0] || hit[1]!=g.symTable || hit[2]!=g.symCode) return;
|
||||
uint16_t off=(uint16_t)(SYM_BITMAPS+(uint16_t)(hit[0]-1u)*SYM_BYTES);
|
||||
for(uint8_t page=0;page<3u;page++)
|
||||
A->asset_read((uint16_t)(off+page*SYM_W),A->fb[4u+page]+SYM_X,SYM_W);
|
||||
}
|
||||
|
||||
/* ---- SysTick cycle counter (call at least every 10 ms; wraps every 89 s,
|
||||
* so compare with a signed difference) ---- */
|
||||
static void clkStart(void){ g.tPrev=SYST_VAL; g.tCyc=0; }
|
||||
static uint32_t clkCyc(void){
|
||||
uint32_t v=SYST_VAL;
|
||||
g.tCyc += (v<=g.tPrev) ? g.tPrev-v : g.tPrev+(SYST_LOAD+1u-v);
|
||||
g.tPrev=v;
|
||||
return g.tCyc;
|
||||
}
|
||||
|
||||
/* ---- PA4 bias: the pin has no DC reference of its own (AC-coupled audio); the
|
||||
* MCU DAC on PA4, output buffer off, at mid-scale, holds it at ~VDD/2.
|
||||
* Set for the whole run, restored on exit (see EPIRB 406). ---- */
|
||||
static void biasOn(uint32_t moder,uint32_t rcc){
|
||||
GPIOA_MODER=moder|(3u<<8); /* PA4 analog */
|
||||
RCC_APBENR1=rcc|RCC_DACEN;
|
||||
DAC_CR=DAC_CR_BIAS;
|
||||
DAC_DHR12R1=BIAS_CODE;
|
||||
DAC_SWTRIGR=1u; /* DHR -> DOR */
|
||||
}
|
||||
static void biasOff(uint32_t dac,uint32_t dhr,uint32_t rcc,uint32_t moder){
|
||||
DAC_CR=dac; DAC_DHR12R1=dhr; RCC_APBENR1=rcc; GPIOA_MODER=moder;
|
||||
}
|
||||
|
||||
/* ---- ADC on PA4 (channel 4); channel 8 (battery) restored afterwards ---- */
|
||||
static void adcSelPA4(void){
|
||||
ADC_SMPR3=(g.savedSmpr3&~(7u<<SMP4_POS))|(((g.savedSmpr3>>SMP8_POS)&7u)<<SMP4_POS);
|
||||
ADC_SQR3=(g.savedSqr3&~0x1Fu)|ADC_CH_PA4;
|
||||
}
|
||||
static void adcRestore(void){ ADC_SQR3=g.savedSqr3; ADC_SMPR3=g.savedSmpr3; }
|
||||
static uint16_t adcRead(void){
|
||||
ADC_CR2|=ADC_CR2_START;
|
||||
for(uint16_t n=0; !(ADC_SR&ADC_SR_EOC) && n<2000u; n++){}
|
||||
return (uint16_t)(ADC_DR&0x0FFFu);
|
||||
}
|
||||
|
||||
static bool callChar(uint8_t c){ return (c>='A'&&c<='Z')||(c>='0'&&c<='9')||c==' '; }
|
||||
|
||||
/* An APRS frame is a UI frame: the address field ends (bit 0 of an SSID byte)
|
||||
* and is followed by control 0x03 and PID 0xF0. Rejects the noise burst that
|
||||
* passes the 16-bit FCS about once a day in continuous decoding. */
|
||||
static bool isUI(const uint8_t *f,uint16_t n){
|
||||
uint16_t end=(uint16_t)(n-2u), e=13;
|
||||
while(!(f[e]&1u) && e+7u<end) e+=7;
|
||||
return (f[e]&1u) && e+2u<end && f[e+1]==0x03u && f[e+2]==0xF0u;
|
||||
}
|
||||
|
||||
/* ---- a good frame from a slicer: shown unless another slicer just gave it ---- */
|
||||
static void accept(const sl_t *m){
|
||||
const app_api_t *A=g.A;
|
||||
uint16_t n=m->n;
|
||||
uint32_t t=A->ticks_ms();
|
||||
frame_t *f=g.history[0];
|
||||
if(!(g.count && n==f->len && f->buf[n-1]==m->buf[n-1] && f->buf[n-2]==m->buf[n-2]
|
||||
&& t-g.tFrame<DUP_MS)){
|
||||
f=g.history[HISTORY-1u];
|
||||
for(unsigned k=HISTORY-1u;k;k--) g.history[k]=g.history[k-1u];
|
||||
g.history[0]=f;
|
||||
memcpy(f->buf,m->buf,n);
|
||||
f->len=n;
|
||||
f->rssi=A->rssi_dbm(); /* capture while the carrier is still up */
|
||||
if(g.count<HISTORY) g.count++;
|
||||
g.nOk++; /* wraps after 65535: five digits in its capsule */
|
||||
g.cur=0; /* show it, from its first row */
|
||||
g.top=0;
|
||||
g.redraw|=REDRAW_FRAME;
|
||||
}
|
||||
g.tFrame=t;
|
||||
}
|
||||
|
||||
/* ---- HDLC: one bit after NRZI. A UI frame with a good FCS goes to accept()
|
||||
* (m->buf, m->n bytes) before the closing flag opens the next frame. Not
|
||||
* inlined: a function of its own, its m stays in a register (inlined in the
|
||||
* slicer loop, it cost 56 B more); called 3600 times a second only. ---- */
|
||||
__attribute__((noinline))
|
||||
static void hdlcBit(sl_t *m,uint8_t b){
|
||||
if(m->bits<255u) m->bits++;
|
||||
m->sr=(uint8_t)((m->sr>>1)|(b<<7));
|
||||
if(m->sr==0x7Eu){
|
||||
bool ok = m->inframe && m->n>=FRAME_MIN && m->crc==FCS_GOOD && isUI(m->buf,m->n);
|
||||
m->pre = m->bits==8u;
|
||||
m->bits=0;
|
||||
if(ok) accept(m);
|
||||
m->inframe=1; m->hdr=1; m->n=0; m->crc=0xFFFFu;
|
||||
m->ones=m->byte=m->nb=0;
|
||||
return;
|
||||
}
|
||||
if(b){
|
||||
if(++m->ones>6u){ m->ones=7; m->inframe=0; return; } /* abort / noise */
|
||||
} else {
|
||||
if(m->ones==5u){ m->ones=0; return; } /* stuffed zero */
|
||||
m->ones=0;
|
||||
}
|
||||
if(!m->inframe) return;
|
||||
m->byte|=(uint8_t)(b<<m->nb);
|
||||
if(++m->nb==8u){
|
||||
if(m->n<FRAME_MAX){
|
||||
uint16_t c=m->crc^m->byte; /* CRC-16/X.25, reflected 0x1021 */
|
||||
for(uint8_t i=0;i<8u;i++) c=(c&1u)?(uint16_t)((c>>1)^0x8408u):(uint16_t)(c>>1);
|
||||
m->crc=c;
|
||||
if(m->n<6u && ((m->byte&1u) || !callChar((uint8_t)(m->byte>>1)))) m->hdr=0;
|
||||
m->buf[m->n++]=m->byte;
|
||||
} else m->inframe=0;
|
||||
m->byte=m->nb=0;
|
||||
}
|
||||
}
|
||||
|
||||
/* A slicer is busy inside a preamble (two back-to-back flags, then 3 bytes) or
|
||||
* a frame whose first bytes look like a callsign: noise is busy ~4 % of the time. */
|
||||
static bool busy(const sl_t *m){
|
||||
return (m->pre && m->bits<PRE_BITS) || (m->inframe && m->n && m->hdr);
|
||||
}
|
||||
|
||||
/* |(i, q)| ~ max + 3/8 min */
|
||||
static int32_t mag(int32_t i,int32_t q){
|
||||
if(i<0) i=-i;
|
||||
if(q<0) q=-q;
|
||||
if(i<q){ int32_t t=i; i=q; q=t; }
|
||||
return i+(q>>2)+(q>>3);
|
||||
}
|
||||
|
||||
/* Magnitude of one tone and its peak tracker (AGC). Not inlined: one copy for
|
||||
* both tones, two calls per sample. */
|
||||
__attribute__((noinline))
|
||||
static int32_t tone(int32_t i,int32_t q,int32_t *pk){
|
||||
int32_t mt=mag(i,q), p=*pk;
|
||||
p+= mt>p ? (mt-p)>>AGC_ATT : -(p>>AGC_DEC);
|
||||
if(p<PK_MIN) p=PK_MIN;
|
||||
*pk=p;
|
||||
return mt;
|
||||
}
|
||||
|
||||
/* ---- front end, one ADC sample: *a = Mm*Ps, *b = Ms*Pm (each tone over its
|
||||
* own peak, without division; both stay below ~2^21) ---- */
|
||||
static void front(dem_t *m,int32_t adc,int32_t *a,int32_t *b){
|
||||
m->dc+=((adc<<4)-m->dc)>>6;
|
||||
int32_t x=adc-(m->dc>>4);
|
||||
int32_t y=(BP_B0*(x-m->x2)-BP_A1*m->y1-BP_A2*m->y2)>>14;
|
||||
m->x2=m->x1; m->x1=x; m->y2=m->y1; m->y1=y;
|
||||
|
||||
/* sliding correlators over one bit; the sine is the cos table 3/4 period ahead.
|
||||
* At 9.6 kHz one bit and one 1200 Hz period are both 8 samples: the ring slot
|
||||
* is also the 1200 Hz table phase. */
|
||||
int16_t *o=m->ring[m->r];
|
||||
uint32_t k=m->ks+12u; if(k>=48u) k-=48u;
|
||||
int32_t p;
|
||||
p=(y*m->cm[m->r])>>8; m->im+=p-o[0]; o[0]=(int16_t)p;
|
||||
p=(y*m->cm[(m->r+6u)&7u])>>8; m->qm+=p-o[1]; o[1]=(int16_t)p;
|
||||
p=(y*m->cs[m->ks])>>8; m->is+=p-o[2]; o[2]=(int16_t)p;
|
||||
p=(y*m->cs[k])>>8; m->qs+=p-o[3]; o[3]=(int16_t)p;
|
||||
m->r=(m->r+1u)&7u;
|
||||
if(++m->ks>=48u) m->ks=0;
|
||||
|
||||
int32_t mm=tone(m->im,m->qm,&m->pm), ms=tone(m->is,m->qs,&m->ps);
|
||||
*a=mm*m->ps;
|
||||
*b=ms*m->pm;
|
||||
}
|
||||
|
||||
/* ---- slicer: decision sign(wa*a - wb*b), DPLL, NRZI, HDLC ---- */
|
||||
static void slice(sl_t *m,int32_t a,int32_t b){
|
||||
int32_t d=a*m->wa-b*m->wb;
|
||||
/* DPLL: a bit at each phase wrap (mid-bit), transitions pulled to mid-phase */
|
||||
m->phase+=PLL_STEP;
|
||||
if((d>0)!=(m->dprev>0)) m->phase+=(PLL_CTR-m->phase)>>PLL_SHIFT;
|
||||
m->dprev=d;
|
||||
if(m->phase>=65536){
|
||||
m->phase-=65536;
|
||||
uint8_t lv=d>0;
|
||||
hdlcBit(m,lv==m->last); /* NRZI: no change = 1 */
|
||||
m->last=lv;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- AX.25 address field -> "F4HWN-7" ---- */
|
||||
static char *putCall(char *o,const uint8_t *a){
|
||||
for(uint8_t i=0;i<6u;i++){ char c=(char)(a[i]>>1); if(c!=' ') *o++=c; }
|
||||
uint8_t ssid=(uint8_t)((a[6]>>1)&15u);
|
||||
if(ssid){ *o++='-'; o=puti(o,ssid); }
|
||||
return o;
|
||||
}
|
||||
|
||||
/* The tiny font covers 0x20-0x7F: anything else shows as '.'. */
|
||||
__attribute__((noinline))
|
||||
static char safe(uint8_t ch){ return (ch<0x20u||ch>0x7Eu)?'.':(char)ch; }
|
||||
|
||||
/* Body rows str..end, wrapped every ROW_CHARS + g.cw. A visible row is rendered
|
||||
* on scratch page 4, then its columns are ORed into lines 0-3 at its exact pixel
|
||||
* offset: 8-pixel and 6-pixel rows scroll alike, without another framebuffer.
|
||||
* Rows may spill over line 0 and the separator: draw() redraws both afterwards.
|
||||
* The scratch page is left blank: display_clear() blanks it before the first
|
||||
* row and each row clears it after use. */
|
||||
static char *emit(char *end){
|
||||
const app_api_t *A=g.A;
|
||||
char *p=str;
|
||||
while(p<end){
|
||||
unsigned w=ROW_CHARS+g.cw; /* 18 or 32; compare lengths before forming p + w */
|
||||
char *q=((unsigned)(end-p)>w)?p+w:end;
|
||||
char c=*q; *q='\0';
|
||||
unsigned y=(unsigned)g.vrow-g.top; /* wraps to a huge value above the screen */
|
||||
if(y-1u<30u){ /* y = 1..30: higher rows lie under the source */
|
||||
uint8_t *scratch=A->fb[4];
|
||||
if(g.cw) A->print_tiny(p,0,32,false,true);
|
||||
else A->print_normal(p,0,0,4);
|
||||
for(unsigned x=0;x<128u;x++){
|
||||
uint32_t bits=(uint32_t)scratch[x]<<y;
|
||||
for(uint8_t *out=A->fb[0]+x;bits;out+=128){
|
||||
*out|=(uint8_t)bits;
|
||||
bits>>=8;
|
||||
}
|
||||
}
|
||||
memset(scratch,0,128);
|
||||
}
|
||||
*q=c;
|
||||
g.vrow+=g.cw?6u:8u;
|
||||
p=q;
|
||||
}
|
||||
return str; /* the next row starts there */
|
||||
}
|
||||
|
||||
/* Between two parts of a row ("48 50.89N" / "002 16.25E"): a new row in the
|
||||
* scroll view, a space in the compact one. */
|
||||
static char *brk(char *o){
|
||||
if(g.cw){ *o++=' '; return o; }
|
||||
return emit(o);
|
||||
}
|
||||
|
||||
/* Info bytes f[i..end) after the text already at str..o, on as many rows as
|
||||
* they take. */
|
||||
static void textRows(char *o,const uint8_t *f,unsigned i,unsigned end){
|
||||
const uint8_t *p=f+i, *last=f+end;
|
||||
do{
|
||||
for(; o<str+ROW_CHARS+g.cw && p<last; p++) *o++=safe(*p);
|
||||
o=emit(o);
|
||||
}while(p<last);
|
||||
}
|
||||
|
||||
/* ---- Mic-E (APRS 1.0.1 chapter 10; test/mice.py is the reference) ----
|
||||
* The latitude, N/S, the longitude +100 offset, E/W and the message bits are in
|
||||
* the destination address; the longitude, speed, course and symbol in the info
|
||||
* field. Byte arithmetic wraps as uint8, so a bad field prints junk, never loops. */
|
||||
static char *put2(char *o,uint32_t v){ *o++=(char)('0'+sub(&v,10u)); *o++=(char)('0'+v); return o; }
|
||||
static char *put3(char *o,uint32_t v){ *o++=(char)('0'+sub(&v,100u)); return put2(o,v); }
|
||||
static bool micType(uint8_t t){ return t=='`'||t=='\''||t==0x1Cu||t==0x1Du; }
|
||||
|
||||
/* Keep decoder temporaries out of the history renderer's register allocation. */
|
||||
__attribute__((noinline))
|
||||
static void drawMicE(const uint8_t *f,unsigned i,unsigned end,const char *s){
|
||||
uint8_t c[6];
|
||||
/* The asset lookup packs digit, position/message bit and custom flag. */
|
||||
for(unsigned k=0;k<6u;k++) c[k]=(uint8_t)s[T_MIC+(f[k]>>1)];
|
||||
|
||||
/* "48 50.89N" / "002 16.25E" (brk) */
|
||||
char *o=str;
|
||||
for(uint8_t k=0;k<6u;k++){
|
||||
*o++=(char)('0'+(c[k]&15u));
|
||||
if(k==1u) *o++=' ';
|
||||
if(k==3u) *o++='.';
|
||||
}
|
||||
*o++=(c[3]&16u)?'N':'S';
|
||||
o=brk(o);
|
||||
uint32_t deg=(uint8_t)(f[i+1]-28u);
|
||||
if(c[4]&16u) deg+=100u;
|
||||
if(deg>=180u && deg<=189u) deg-=80u;
|
||||
else if(deg>=190u && deg<=199u) deg-=190u;
|
||||
uint32_t mn=(uint8_t)(f[i+2]-28u);
|
||||
if(mn>=60u) mn-=60u;
|
||||
o=put3(o,deg); *o++=' '; o=put2(o,mn); *o++='.'; o=put2(o,(uint8_t)(f[i+3]-28u));
|
||||
*o++=(c[5]&16u)?'W':'E';
|
||||
emit(o);
|
||||
|
||||
/* "12km/h 245 [/" / "En Route" (brk): speed in knots x 1.8525 (1897/1024) */
|
||||
uint32_t dc=(uint8_t)(f[i+5]-28u);
|
||||
uint32_t sp=(uint32_t)(uint8_t)(f[i+4]-28u)*10u+sub(&dc,10u);
|
||||
uint32_t crs=dc*100u+(uint8_t)(f[i+6]-28u);
|
||||
if(sp>=800u) sp-=800u;
|
||||
if(crs>=400u) crs-=400u;
|
||||
o=puti(str,(int32_t)((sp*1897u)>>10)); o=put(o,s+T_KMH);
|
||||
o=puti(o,(int32_t)crs); *o++=' ';
|
||||
*o++=safe(f[i+7]); *o++=safe(f[i+8]);
|
||||
g.symTable=f[i+8]; g.symCode=f[i+7];
|
||||
o=brk(o);
|
||||
unsigned idx=0;
|
||||
unsigned custom=0;
|
||||
for(unsigned k=0;k<3u;k++){
|
||||
idx=(idx<<1)|((c[k]>>4)&1u);
|
||||
custom|=c[k]&32u;
|
||||
}
|
||||
if(custom && idx){ o=put(o,s+T_CUSTOM); *o++=(char)('0'+7u-idx); }
|
||||
else o=put(o,s+T_MSG+idx*T_MSG_STRIDE);
|
||||
emit(o);
|
||||
|
||||
/* comment: trailing CR/LF, the device markers (Yaesu/other: '`' or '\'' +
|
||||
* text + 2 chars; Kenwood: '>' or ']' + text [+ '=' or '^']) and a leading
|
||||
* "xxx}" altitude (base 91, metres + 10000) */
|
||||
unsigned j=i+9u; /* the caller guarantees j <= end */
|
||||
while(end>j && (f[end-1]=='\r'||f[end-1]=='\n')) end--;
|
||||
if(j<end){
|
||||
uint8_t m=f[j];
|
||||
if(m=='`'||m=='\''){ j++; if(end-j>=2) end-=2u; }
|
||||
else if(m=='>'||m==']'){ j++; if(end>j && (f[end-1]=='='||f[end-1]=='^')) end--; }
|
||||
}
|
||||
o=str;
|
||||
if(end-j>=4 && f[j+3]=='}'){
|
||||
int32_t alt=(((int32_t)f[j]-33)*91+((int32_t)f[j+1]-33))*91+((int32_t)f[j+2]-33)-10000;
|
||||
o=puti(o,alt); *o++='m'; *o++=' ';
|
||||
j+=4u;
|
||||
}
|
||||
textRows(o,f,j,end);
|
||||
}
|
||||
|
||||
/* ---- uncompressed position ('!' '=' or, after a 7-character timestamp, '/'
|
||||
* '@'; test/mice.py draw_pos is the reference): "49 21.07N 001 50.50E", then
|
||||
* the timestamp, the symbol and the comment. False for anything else (compressed
|
||||
* positions included), which is then shown as raw text. The position takes
|
||||
* two rows in the scroll view, "49 21.07N" / "001 50.50E", one when compact. ---- */
|
||||
__attribute__((noinline))
|
||||
static bool drawPos(const uint8_t *f,unsigned i,unsigned end){
|
||||
uint8_t t=f[i];
|
||||
unsigned p=i+1u; /* native-width offset; bounds checked below */
|
||||
if(t=='/'||t=='@') p+=7u;
|
||||
else if(t!='!'&&t!='=') return false;
|
||||
if(end<p+19u || f[p+4]!='.' || f[p+14]!='.') return false;
|
||||
const uint8_t *q=f+p; /* DDMM.hhN T DDDMM.hhE C */
|
||||
char *o=str;
|
||||
for(uint8_t k=0;k<18u;k++){
|
||||
if(k==8u){ o=brk(o); continue; } /* symbol table: later */
|
||||
if(k==2u||k==12u) *o++=' ';
|
||||
*o++=safe(q[k]);
|
||||
}
|
||||
o=emit(o);
|
||||
if(p>i+1u){
|
||||
/* A timestamp always occupies the seven bytes after the data type. */
|
||||
for(unsigned k=0;k<7u;k++) *o++=safe(f[i+1u+k]);
|
||||
*o++=' ';
|
||||
}
|
||||
*o++=safe(q[8]); *o++=safe(q[18]); *o++=' ';
|
||||
g.symTable=q[8]; g.symCode=q[18];
|
||||
textRows(o,f,p+19u,end);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---- display ---- (not inlined: 16 B less) */
|
||||
__attribute__((noinline))
|
||||
static void draw(void){
|
||||
const app_api_t *A=g.A;
|
||||
/* The UI texts and Mic-E lookup live in the assets: one read into this word-aligned
|
||||
* stack block, so each text costs a 2-byte sp-relative add instead of a
|
||||
* literal and its pool word (gen_assets.py keeps every offset aligned). */
|
||||
char s[UI_SIZE] __attribute__((aligned(4)));
|
||||
char *o;
|
||||
frame_t *frame=g.history[g.cur]; /* read only when there is a frame */
|
||||
uint8_t *b=A->fb[0];
|
||||
A->asset_read(0,s,UI_SIZE);
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
g.vrow=8; /* the body starts under the source */
|
||||
g.symTable=0;
|
||||
A->print_inverse(s+T_TITLE,2,0,true,true,(uint8_t)(2u+T_TITLE_CHARS*4u));
|
||||
A->draw_battery();
|
||||
|
||||
if(!g.count) /* no frame yet: a capsule, as APRS TX's TRANSMIT */
|
||||
A->print_inverse(s+T_WAIT,WAIT_CAPS_X,0,true,true,(uint8_t)(WAIT_CAPS_X+T_WAIT_CHARS*4u));
|
||||
else {
|
||||
const uint8_t *f=frame->buf;
|
||||
unsigned end=frame->len-2u; /* the FCS is not shown */
|
||||
|
||||
/* ">DEST,DIGI*,..." while it fits 2 rows, then the rest of the
|
||||
* address field is skipped up to its end bit */
|
||||
o=str; *o++='>'; o=putCall(o,f);
|
||||
unsigned e=13; /* isUI() already checked the end marker and bounds */
|
||||
while(!(f[e]&1u)){
|
||||
e+=7;
|
||||
if(o<=str+20){ *o++=','; o=putCall(o,f+e-6); if(f[e]&0x80u) *o++='*'; }
|
||||
}
|
||||
emit(o);
|
||||
|
||||
/* info field (after control and PID): Mic-E and uncompressed positions
|
||||
* decoded, anything else as text */
|
||||
unsigned i=e+3u;
|
||||
if(end>=i+9u && micType(f[i])) drawMicE(f,i,end,s);
|
||||
else if(i<end && !drawPos(f,i,end)) textRows(str,f,i,end);
|
||||
|
||||
/* the source, fixed on line 0 over the body rows scrolled there */
|
||||
memset(b,0,128);
|
||||
*putCall(str,f+7)='\0';
|
||||
A->print_bold(str,0,0,0);
|
||||
/* at the right of the source, two capsules: once several frames are
|
||||
* kept, "2/5" at the edge (the frame shown / the frames kept, 1 =
|
||||
* newest), and left of it the frames received. A capsule runs from 2 px before its text to its end column. */
|
||||
unsigned capsEnd=CAPS_END;
|
||||
if(g.count>1u){
|
||||
o=str; *o++=(char)('1'+g.cur); *o++='/'; *o++=(char)('0'+g.count); *o='\0';
|
||||
A->print_inverse(str,CAPS_END-12u,0,false,true,CAPS_END);
|
||||
capsEnd-=16u; /* that capsule and a blank column */
|
||||
}
|
||||
o=puti(str,g.nOk); *o='\0';
|
||||
A->print_inverse(str,(uint8_t)(capsEnd-4u*(unsigned)(o-str)),0,false,true,(uint8_t)capsEnd);
|
||||
}
|
||||
/* The last glyph ends one pixel before its row height: the scroll stops
|
||||
* with it right above the separator. */
|
||||
unsigned lim=g.vrow>32u?g.vrow-32u:0u;
|
||||
g.lim=lim;
|
||||
/* Status bar from SPK_X: the speaker icon while on, an up mark while rows
|
||||
* or a newer frame hide above, a down mark while rows or an older frame hide
|
||||
* below: UP / DOWN has something to show. */
|
||||
unsigned t=g.spk;
|
||||
if(g.top || g.cur) t+=2u;
|
||||
if(g.top<lim || g.cur+1u<g.count) t+=4u;
|
||||
A->asset_read((uint16_t)(BMP_TAIL+t*TAIL_W),A->status_line+SPK_X,TAIL_W);
|
||||
|
||||
/* dotted separator above the frequency: y = 31, bit 7 of line 3, over
|
||||
* whatever the shift brought there (the big digits start at y = 33: one
|
||||
* blank px between) */
|
||||
for(unsigned x=0;x<128u;x+=2u){
|
||||
b[384u+x]|=0x80u;
|
||||
b[385u+x]&=0x7Fu;
|
||||
}
|
||||
drawFreq(A->rx_freq());
|
||||
drawSymbol();
|
||||
|
||||
/* bottom line: "-89dBm", RSSI of the frame shown (none captured yet: blank) */
|
||||
o=str;
|
||||
if(g.count){ o=puti(o,frame->rssi); o=put(o,s+T_DBM); }
|
||||
tiny(49,o);
|
||||
}
|
||||
|
||||
/* ---- input ---- */
|
||||
static void handleKeys(void){
|
||||
const app_api_t *A=g.A;
|
||||
uint8_t key=A->get_key();
|
||||
/* UP/DOWN, held: scroll the body 1 px per slot (20 px/s) while rows overflow.
|
||||
* Pressed again once at an end: the newer (UP) or older (DOWN) frame, from its
|
||||
* first row; holding the key never leaves the frame. */
|
||||
int d=A->nav_dir(key);
|
||||
unsigned top=(unsigned)(g.top+d); /* wraps past the first row */
|
||||
if(top<=g.lim){
|
||||
if(top!=g.top){ g.top=(uint16_t)top; g.redraw|=REDRAW_ON; }
|
||||
} else if(key!=g.prevKey && (unsigned)(g.cur+d)<g.count){
|
||||
g.cur=(uint8_t)(g.cur+d); /* the new key redraws below */
|
||||
g.top=0;
|
||||
}
|
||||
if(key==APP_KEY_INVALID||key==g.prevKey){ g.prevKey=key; return; }
|
||||
g.prevKey=key;
|
||||
g.redraw|=REDRAW_ON;
|
||||
if(key==APP_KEY_EXIT) g.running=false;
|
||||
else if(key==APP_KEY_2){ g.count=g.cur=0; g.nOk=0; g.top=0; } /* g.lim: 0 at the redraw that follows */
|
||||
else if(key==APP_KEY_1){ g.spk^=1u; A->audio_path(g.spk); } /* speaker, as FoxHunt's audio */
|
||||
else if(key==APP_KEY_STAR){ g.cw^=CW_COMPACT; g.top=0; } /* the view (saved on exit) */
|
||||
}
|
||||
|
||||
/* ---- keys and screen, between frames ---- */
|
||||
static void house(void){
|
||||
const app_api_t *A=g.A;
|
||||
handleKeys();
|
||||
if(!g.running) return;
|
||||
if(g.redraw&REDRAW_FRAME) A->backlight_on(); /* wake on a new frame */
|
||||
A->backlight_update(); /* normal BLTime timeout; keys re-arm it in get_key() */
|
||||
if(++g.refresh>=REFRESH_SLOTS) g.redraw|=REDRAW_ON;
|
||||
if(!g.redraw) return;
|
||||
g.redraw=0;
|
||||
g.refresh=0;
|
||||
adcRestore(); /* the battery is on ADC channel 8 */
|
||||
A->battery_sample();
|
||||
adcSelPA4();
|
||||
draw();
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
}
|
||||
|
||||
/* ---- the receiver: PA4 at 9.6 kHz, continuously, until EXIT ---- */
|
||||
/* Keep decoder stack offsets independent of the five history buffers. */
|
||||
__attribute__((noinline))
|
||||
static void listen(void){
|
||||
const app_api_t *A=g.A;
|
||||
dem_t d;
|
||||
sl_t sl[NSL];
|
||||
memset(sl,0,sizeof sl);
|
||||
A->asset_read(DEMOD_INIT,&d,sizeof d); /* initial state, then both cosine tables */
|
||||
sl[0].wa=2; sl[0].wb=3; /* favours space */
|
||||
sl[1].wa=1; sl[1].wb=1;
|
||||
sl[2].wa=3; sl[2].wb=2; /* favours mark */
|
||||
/* hdlcBit initializes the CRC at the opening flag, before collecting bytes. */
|
||||
|
||||
adcSelPA4();
|
||||
clkStart();
|
||||
uint32_t next=0; /* the first sample at once */
|
||||
uint16_t cnt=0;
|
||||
uint8_t busyFor=0;
|
||||
g.redraw=REDRAW_ON;
|
||||
while(g.running){
|
||||
while((int32_t)(clkCyc()-next)<0){}
|
||||
next+=CYC_PER_SAMPLE;
|
||||
int32_t a,b;
|
||||
front(&d,adcRead(),&a,&b);
|
||||
for(uint8_t k=0;k<NSL;k++) slice(&sl[k],a,b);
|
||||
if(++cnt<HOUSE_EVERY) continue;
|
||||
cnt=0;
|
||||
bool bz=false; /* only the slot's own sample decides */
|
||||
for(uint8_t k=0;k<NSL;k++) bz|=busy(&sl[k]);
|
||||
if(bz && ++busyFor<BUSY_MAX) continue;
|
||||
busyFor=0;
|
||||
house();
|
||||
next=clkCyc(); /* skip the samples the slot took */
|
||||
}
|
||||
adcRestore();
|
||||
}
|
||||
|
||||
__attribute__((section(".text.entry"),used))
|
||||
void app_main(const app_api_t *api){
|
||||
frame_t frames[HISTORY]; /* on the stack: the 4 KiB overlay also holds .bss */
|
||||
frame_t *history[HISTORY];
|
||||
for(unsigned k=0;k<HISTORY;k++) history[k]=&frames[k];
|
||||
char text[34];
|
||||
g.A=api;
|
||||
g.text=text;
|
||||
g.history=history;
|
||||
/* The overlay loader zeroes g; the first idle key scan records APP_KEY_INVALID. */
|
||||
|
||||
g.savedSqr3=ADC_SQR3; g.savedSmpr3=ADC_SMPR3;
|
||||
/* Only app_main needs these snapshots; keep them outside the overlay .bss. */
|
||||
const uint32_t savedModer=GPIOA_MODER, savedDac=DAC_CR;
|
||||
const uint32_t savedRcc=RCC_APBENR1, savedDhr=DAC_DHR12R1;
|
||||
biasOn(savedModer,savedRcc);
|
||||
api->backlight_on();
|
||||
api->cfg_load(&g.cw,2); /* view and speaker; erased flash (0xFF) or junk:
|
||||
scroll, speaker off */
|
||||
if(g.cw!=CW_COMPACT) g.cw=0;
|
||||
g.spk=(uint8_t)(g.spk==1u);
|
||||
/* Speaker amplifier (PA8) as saved, off by default: the decoder does not
|
||||
* need it (tested on the radio, 2026-09-30): PA4, the voice-prompt DAC pin,
|
||||
* joins the audio before the amplifier; only the BK4829 AF output must be
|
||||
* on. Key 1 toggles the speaker to listen to the channel. */
|
||||
api->audio_path(g.spk);
|
||||
api->set_af(APP_AF_FM);
|
||||
api->delay_ms(50);
|
||||
|
||||
g.running=true;
|
||||
listen();
|
||||
|
||||
biasOff(savedDac,savedDhr,savedRcc,savedModer);
|
||||
api->set_af(APP_AF_MUTE);
|
||||
api->audio_path(false);
|
||||
api->cfg_save(&g.cw,2); /* staged: flash is written only after a change */
|
||||
}
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/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="APRS RX" # <-- the only per-app line
|
||||
APP_VER="0.9"
|
||||
APP_API_MIN=2
|
||||
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; }
|
||||
|
||||
# GCC 13 supports -Oz: prioritize overlay size over instruction count.
|
||||
CFLAGS="-mcpu=cortex-m0plus -mthumb -Oz -fno-jump-tables -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/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 assets ; APP_VER="$APP_VER" python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
# A failed link (4 KiB overflow) leaves no ELF: keep an object to disassemble.
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" \
|
||||
|| { "$CC" $CFLAGS -c "${APP}_app.c" -o "${APP}.o"; false; }
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
|
||||
--shortcut none --assets "${APP}_assets.bin" >/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"
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
# APRS RX read-only assets: the screen texts and the correlator tables.
|
||||
#
|
||||
# The UI texts come first, each padded to a multiple of 4 bytes: draw() reads
|
||||
# them in one asset_read into a word-aligned stack block, so every text costs a
|
||||
# 2-byte sp-relative add instead of a literal-pool load and a pool word.
|
||||
# The Mic-E message names are a fixed-stride table read straight into the row.
|
||||
# The tables are 127 * cos(2 pi f n / 9600): one period of 1200 Hz (8 samples)
|
||||
# and of 2200 Hz (48 samples = 11 cycles). The sine is the same table read 3/4
|
||||
# of a period ahead (+6 and +12), as in test/model_rx.py.
|
||||
#
|
||||
# ./gen_assets.py aprsrx_assets.bin aprsrx_assets.h
|
||||
import math, os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.join(HERE, ".."))
|
||||
from app_assets import Assets
|
||||
from aprs_symbols_20 import BITMAPS, BYTES_PER_ICON, CODES, WIDTH
|
||||
|
||||
FS = 9600
|
||||
TITLE = "APRS RX" # the version is in the .app header
|
||||
WAIT_CAPS = "WAIT" # status-bar capsule until a frame
|
||||
|
||||
UI = [
|
||||
("T_TITLE", TITLE),
|
||||
("T_WAIT", WAIT_CAPS),
|
||||
("T_DBM", "dBm"),
|
||||
("T_KMH", "km/h "),
|
||||
("T_CUSTOM", "Custom-"),
|
||||
]
|
||||
|
||||
# Mic-E message types, indexed by the destination's message bits A B C
|
||||
MIC_MSG = ["Emergency", "Priority", "Special", "Committed",
|
||||
"Returning", "In Service", "En Route", "Off Duty"]
|
||||
|
||||
def cos_table(f):
|
||||
per = FS // math.gcd(FS, f)
|
||||
return [int(round(127 * math.cos(2 * math.pi * f * n / FS))) for n in range(per)]
|
||||
|
||||
def mic_code(c):
|
||||
"""Digit in bits 0-3, message/position bit in bit 4, custom flag in bit 5."""
|
||||
digit = 0
|
||||
for base in (ord('0'), ord('A'), ord('P')):
|
||||
if base <= c < base + 10:
|
||||
digit = c - base
|
||||
custom = ord('A') <= c <= ord('K')
|
||||
bit = c >= ord('P') or custom
|
||||
return digit | (int(bit) << 4) | (int(custom) << 5)
|
||||
|
||||
a = Assets("APRSRX")
|
||||
ui_size = 0
|
||||
for name, s in UI:
|
||||
pad = -(len(s) + 1) % 4 # keep every offset word-aligned
|
||||
a.text(name, s + "\0" * pad)
|
||||
ui_size += len(s) + 1 + pad
|
||||
a.u8("T_MIC", [mic_code(c) for c in range(128)])
|
||||
a.const("T_TITLE_CHARS", len(TITLE))
|
||||
a.const("T_WAIT_CHARS", len(WAIT_CAPS))
|
||||
a.table("T_MSG", MIC_MSG) # Mic-E standard messages
|
||||
# Assets.build() places every text/table before binary data. Include the
|
||||
# messages between the UI labels and T_MIC in the single display read.
|
||||
ui_size += len(MIC_MSG) * (max(map(len, MIC_MSG)) + 1) + 128
|
||||
a.const("UI_SIZE", ui_size)
|
||||
# The status bar from x = 59 (SPK_X), in one asset_read: the speaker icon
|
||||
# (bit 0 of the index), 3 blank columns, then the scroll marks at x = 72, up
|
||||
# (bit 1, rows hidden above) in bits 0-2, down (bit 2, rows hidden below) in
|
||||
# bits 4-6. 8 variants of TAIL_W bytes.
|
||||
SPK = [0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c] # FoxHunt's
|
||||
UP, DN = [0x04, 0x06, 0x07, 0x06, 0x04], [0x10, 0x30, 0x70, 0x30, 0x10]
|
||||
tail = []
|
||||
for i in range(8):
|
||||
tail += (SPK if i & 1 else [0] * len(SPK)) + [0] * 3
|
||||
tail += [(u if i & 2 else 0) | (d if i & 4 else 0) for u, d in zip(UP, DN)]
|
||||
a.u8("BMP_TAIL", tail)
|
||||
a.const("TAIL_W", len(tail) // 8)
|
||||
symbol_map = bytearray(128 * 3)
|
||||
for index, code in enumerate(CODES):
|
||||
slot = (2 * ord(code[0]) + 13 * ord(code[1])) & 127
|
||||
if symbol_map[slot * 3]:
|
||||
raise ValueError("APRS symbol hash collision")
|
||||
symbol_map[slot * 3:slot * 3 + 3] = bytes((index + 1, ord(code[0]), ord(code[1])))
|
||||
a.raw("SYM_MAP", symbol_map)
|
||||
a.raw("SYM_BITMAPS", BITMAPS)
|
||||
a.const("SYM_COUNT", len(CODES))
|
||||
a.const("SYM_W", WIDTH)
|
||||
a.const("SYM_BYTES", BYTES_PER_ICON)
|
||||
# dem_t prefix: dc, eight filter/correlator words, pm/ps, r/ks, ring.
|
||||
# Append the existing cosine tables so one read initializes the entire front end.
|
||||
a.u32("DEMOD_INIT", [2048 << 4] + [0] * 8 + [64, 64] + [0] * 18)
|
||||
a.i8("COS1200", cos_table(1200)) # 8 entries
|
||||
a.i8("COS2200", cos_table(2200)) # 48 entries
|
||||
if __name__ == "__main__":
|
||||
a.main()
|
||||
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"format": "aprs-symbols-20",
|
||||
"version": 1,
|
||||
"width": 20,
|
||||
"height": 20,
|
||||
"symbols": [
|
||||
{
|
||||
"code": "/#",
|
||||
"name": "DIGI",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000100000000000000000001000000000000000000111000000000000000001110000000000000000111110000000000000001111100000000011111111111111111000011111111111111100000011111111111110000000001111111110000000000001111111000000000000011111110000000000001111111110000000000111110111110000000001111000111100000000111000000011100000001100000000011000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/X",
|
||||
"name": "HELICOPTER",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000001111111111111111100011100000100000111010000000011100000000110000001111100000001110000111111110000001111111111010010000111001111110110110001100001111111111100000000001111111111000000000001111111100000000000001000010000000000000111111101000000000000111111100000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/&",
|
||||
"name": "HF GATEWAY",
|
||||
"pixels": "0000000000000000000000000000000000000000000011111111111110000000100100100100100000001001001001001000000000010010010000000000000101110100000000000000011100000000000000000111000000000000000001110000000000000000111110000000000000001111100000000000000110001100000000000001100011000000000000011111110000000000001111111110000000000011100011100000000000110000011000000000011111111111000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/Y",
|
||||
"name": "SAILBOAT",
|
||||
"pixels": "0000000000000000000000000000001000000000000000000011000000000000000000110000000000000000001110000000000000001011100000000000000010111100000000000001101111000000000000011011111000000000001110111110000000000011101111110000000001111011111100000000011110111111100000001111101000000011000000000011111111100011111111111111110000111111111111111000000000000000000000000011111111111111100000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/'",
|
||||
"name": "SMALL ACFT",
|
||||
"pixels": "0000000000000000000000000000000000000000000001100000000000000000001100000000000000000001100000000000000000011100000000000000000011100000000011000000111100000000011000000111100000000111111111111111111001111111111111111111011111111111111111100110000001111000000011000000111100000000000000001110000000000000000111000000000000000001100000000000000000110000000000000000011000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/[",
|
||||
"name": "PERSON",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000111000000000000000011111000000000000000111110000000000000000111000000000000000000000000000000000000111110000000000000011111110000000000001111111110000000000110011100110000000001100111001100000000011001110011000000000000111110000000000000001101100000000000000011011000000000000000110110000000000000011101110000000000000111011100000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/-",
|
||||
"name": "VHF HOUSE",
|
||||
"pixels": "0000000000000000000000000000011111110000000000000000100000000000000000111110000000000000000010000000000000000001110000000000000011001000000000000001111010000000000000111111100000000000011111111000000000001111111111000000000111111111111000000001111111111110000000011100110001100000000111001100011000000001110011000110000000011111110001100000000111111100011000000001111111111110000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/\\",
|
||||
"name": "DIRECTION FIND",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111000000110000000000110000000100000000001000000001100000000110000000001000000001000000000011000000110000000000010000001000000000000110000110000000000000100001000000000000001100110000000000000001001000000000000000011110000000000000000011000000000000000000110000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/.",
|
||||
"name": "X",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000001100000000001100000011100000000111000000111100000011110000001111100001111100000001111100111110000000001111111111000000000001111111100000000000001111110000000000000111111110000000000011111111110000000001111100111110000000111110000111110000001111000000111100000011100000000111000000110000000000110000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/^",
|
||||
"name": "LARGE ACFT",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000110000000000000000001100000000000000000111100000000000000001001000000000000000111111000000000000111111111100000000111111111111110000111111111111111111011111110111101111111111000001111000001110000000011110000000000000000111100000000000000001111000000000000000111111000000000000111111111100000000001110110111000000000010001100010000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/0",
|
||||
"name": "CIRCLE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000111111000000000000111000011100000000011000000001100000001100000000001100000011000000000011000001100000000000011000011000000000000110000110000000000001100001100000000000011000011000000000000110000011000000000011000000110000000000110000000110000000011000000000111000011100000000000011111100000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/_",
|
||||
"name": "WEATHER STN",
|
||||
"pixels": "0000000000000000000000000000000000000000001000000011000000000011000001110000000000111111110000000000001100000111000000000010001100110000000000000011000000000000000000110000000000000000001100000000000000000111100001110000000011111100011100000000111111000111000000000011000001110000000000110000011100000000001100000010000000000011000000100000000000111111111000000000001100000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/:",
|
||||
"name": "FIRE",
|
||||
"pixels": "0000000000000000000000000000001000000000000000000110000000000000000001100000000000000000111000000000000000001111001000000000000111110010000000000101111111100000000001111111111000000000111110111100100000001111100111011000000111111001111110000001111100011111100000011110000001111000000011100000011100000000111000001111000000000110000011100000000000110001110000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/a",
|
||||
"name": "AMBULANCE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000110000000000000000011100000000000000000111000000011111111111111100001111110011111001100011111100111110001000111100000011100001001111000000111111111011111100111111111110111111001111111111101100011111111000111010010011111100100110001110000000011100000011100000000111000000010000000000100000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/;",
|
||||
"name": "CAMPGROUND",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000100000000000000000011100000000000000000111000000000000000011111000000000000001111111000000000000011111110000000000001111011110000000000111100011100000000001111000111100000000111100000111100000001111000001111000001111111111111111110000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/b",
|
||||
"name": "BICYCLE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011100000000000000000000100000000000111000001000000000000100000111000000000001111111010000000000101000100100000011110010010011100001000100011001000100100100100100100100101011101110001011101010010010000010010010010001000000010001000011100000000011100000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/<",
|
||||
"name": "MOTORCYCLE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000011000000000000000000010000000000000000000110000000000000000111100000011111000011111111001000011000111100001000110011111110011000010010011111001001001011010111110101101010110100111001011010010010000000001001000011000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/f",
|
||||
"name": "FIRE TRUCK",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000111100000000000001111000000000000011110000000000000111100000000001000111000000000111111001111111111001000111111111111010010000111111111110111100001111111111101111111111111111111011111111111000100011111100011100100010011110010010011101110000001110000111011100000011100000100010000000010000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/>",
|
||||
"name": "CAR",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111000000000001111111111000000000111000100110000000001100001000110000001111111111111111110111111111111111111111110000111111000011111001100111100110011110111100000011110110000110000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/g",
|
||||
"name": "GLIDER",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111000000000000000000111000001100000000000111001111100000000000111111100010000000001111100000110000011111100000000110011111100000000000111111100000000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/C",
|
||||
"name": "CANOE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000111100000000000000001110000000000000000110000000000000000011000000011000000001100001101111000000110000111111111111011011111111111111101101111111110111110110111111111000000011000000000000000001100000000000000000110000000000000000111000000000000000011110000000000000000011000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/j",
|
||||
"name": "JEEP",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111110000000000001001000100000000000010010000100000000001100100001101111111111001111111111111111111111111111111000111111110001111100100111111001001111011100000000111001000111000000001110000000100000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/E",
|
||||
"name": "EYE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000111111000000000000111000011100000000011000000001100000001100001100001100000110000111100001100011000011111100001100110000111111000011001100001111110000110001100001111000011000001100001100001100000001100000000110000000001110000111000000000000111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/k",
|
||||
"name": "TRUCK",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111110000001111111111111100000011111111111111011100111111111111111100101111111111111111001011111111111111010011111111111111110111111100011100011110001110010010010011001001001110001110000111000011100011100001110000010000010000001000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/I",
|
||||
"name": "TCP/IP",
|
||||
"pixels": "0000000000000000000000000000000000000000000000011111100000000000000100001000000000000001000010000000000000010000100000000000000111111000000000000000011000000000000000000110000000000000000001100000000000001111111111110000000010000000000100000000100000000001000000111110000001111100001000100000010001000010001000000100010000111110000001111100000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/m",
|
||||
"name": "MIC-E REPEATER",
|
||||
"pixels": "0000100000000001000000010000000000001000000100100000010010000010011000000110010000100100111100100100001001001111001001000010010011110010010000100110011001100100000100100110010010000001000001100000100000001000011000010000000000001111000000000000000010010000000000000001100110000000000000010110100000000000000111111000000000000011000011000000000001111111111000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/K",
|
||||
"name": "SCHOOL",
|
||||
"pixels": "0000000000000000000000000000001110000000000000000011100000000000000000100000000000000000001000000000000000000111000000000000000011111000000000000001111111000000000000111000111000000000011110001111000000001111111111111000000111111111111111000001111111111111110000011001100011001100000110011000110011000001100110001100110000011111100011111100000111111000111111000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/O",
|
||||
"name": "BALLOON",
|
||||
"pixels": "0000000000000000000000000000111110000000000000111010111000000000001101110110000000000110111110110000000001101111101100000000011011111011000000000110111110110000000001101111101100000000011001110011000000000011011101100000000000010111010000000000000101110100000000000000101010000000000000001101100000000000000001110000000000000000000000000000000000000111000000000000000001110000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/P",
|
||||
"name": "POLICE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000100010001000000000000100100100000000000000000000000000000000100111001000000000000011111000000000000000000000000000000000111111110000000000011001100110000000001100011000110000011111111111111111111111111111111111111111000011111111000011100110011111100110010011110000000011110000011000000000011000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/s",
|
||||
"name": "POWERED SHIP",
|
||||
"pixels": "0000000000000000000000000000000000011000000000000000011110000000000000001111100000000000000110000000000000000000000000000000000000110000000000000000001100000000000000001111110000000000001111111100000000000111111111100000000011001010101100000111111111111111110000111111111111111100000111111111111110000000000000000000000000111111111111111100000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/R",
|
||||
"name": "REC VEHICLE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111110000011111111111111110001111111111110001100011000011000111110000110000110001100110001100001100011000110011111111000111111110110001110001110001101001001111111001001000111000000000111000001110000000001110000001000000000001000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/v",
|
||||
"name": "VAN",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111000000111111111111111000001111111111110011000011111111111100011000111111111111000011001111111111111111111011111111111111111110110001111111100011101001001111110010011000111000000001110000001110000000011100000001000000000010000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/y",
|
||||
"name": "YAGI",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000100010001000100010001000100010001000100011111111111111111100100010001000100010001000100010001000100010000000100000000000000000001000000000000000000010000000000000000000100000000000000000001000000000000000000010000000000000000000100000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/T",
|
||||
"name": "SSTV",
|
||||
"pixels": "0000000000000000000000000001000010000000000000001001000000000000000001100000000000111111111111111100011000000000000001100100111111111111001001011000000000011010010100000000000010100101001110101000101001010001000100001010010100010001000010100101000000000000101001011000000000011010010011111111111100100110000000000000011000111111111111111100000011100000011100000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/U",
|
||||
"name": "BUS",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111100011111111111111111100100100100100100111001001001001001000110010010010010010001100111111111111110011001111111111111111111011000111111100011110100100111110010011101011101111101110111000111000000011100000000100000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/V",
|
||||
"name": "ATV",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000010000000000000000000110000000000110000011110000000011110001111110000011111111111111111000000011111111100001000110011111110011000010010011111001001001011010111110101101010110100000001011010010010000000001001000011000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\#",
|
||||
"name": "OVERLAY DIGI",
|
||||
"pixels": "0000000000000000000000000000111100000000000000111001110000000000111000000111000000011000000000011000000100000110000010000011000001100000110000100000111100000100010001111111111000100100011111111110001001000011111111000010010000011111100000100010000111111000010000110011100111001100000100110000110010000001100000000001100000001110000001110000000000111001110000000000000011110000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\&",
|
||||
"name": "OVERLAY GATE",
|
||||
"pixels": "0000000000000000000000000000111110000000000000011000110000000000011000000011000000001100001000011000000110000111000011000001000011111000010000110001111111000110001000111111111000100010011111111111001000100111111111110010001000111111111000100011000111111100011000010000111110000100000110000111000011000000110000100001100000000110000000110000000000011000110000000000000011111000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\-",
|
||||
"name": "HF HOUSE",
|
||||
"pixels": "0000000000001000000000000001111111111100000000000000100000000000000001111111000000000000000010000000000000000001110000000000000011001000000000000001111010000000000000111111100000000000011111111000000000001111111111000000000111111111111000000001111111111110000000011100110001100000000111001100011000000001110011000110000000011111110001100000000111111100011000000001111111111110000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/W",
|
||||
"name": "NWS SITE",
|
||||
"pixels": "0000000000000000000000000000001000000000000000001111000000000000000111111100000000000011111111110000000011111111111110000001111111111111110000111111111111111100001111111111111111000001111111111111100000000110011001100000000000000000000000000000000000000000000000001000100010001000000110011001100110000000000000000000000000000010001000100000000001100110011000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\.",
|
||||
"name": "AMBIGUOUS",
|
||||
"pixels": "0000000000000000000000000000000000000000000000011111000000000000001111111000000000000111111111000000000001110001110000000000011100011100000000000000000111000000000000000011100000000000000001110000000000000000111100000000000000001110000000000000000011100000000000000000000000000000000000001110000000000000000011100000000000000000111000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\0",
|
||||
"name": "OVERLAY CIRCLE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000011111100000000000011100001110000000001100000000110000000110001111000110000001000111111000100000110011111111001100001001111111111001000010011111111110010000100111111111100100001100111111110011000001000111111000100000011000111100011000000011000000001100000000011100001110000000000001111110000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "E0",
|
||||
"name": "ECHOLINK",
|
||||
"pixels": "0000000000000000000000000000011000000000000000001111000000000000000011110000000000000000011000000000000000001001000000000000001100001100000000000111100111100000000001111001111000000000000100001000000000000010000001000000000001000000001000000000010000000010000000001100000000110000000111100000011110000001111000000111100000001100000000110000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "I0",
|
||||
"name": "IRLP",
|
||||
"pixels": "0000000000000000000000000000011000000000000000001111000000000000000011110000000000000000111100000000000000000110000000000000000010010000000000000001100110000000000000010000100000000000001100001100000000000010000001000000000001100000011000000000010000000010000000001110000001110000000111110000111110000001111111111111100000011111000011111000000011100000011100000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "W0",
|
||||
"name": "WIRES",
|
||||
"pixels": "0000000000000000000000000000111000000000000000111111100000000000011101011100000000001010010010100000000111111111111100000001001001001001000000100010010010001000001001000100010010000011111111111111100000100100010001001000001000100100100010000001001001001001000000011111111111110000000010100100101000000000011101011100000000000011111110000000000000001110000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "YY",
|
||||
"name": "YAESU RADIO",
|
||||
"pixels": "0000000000100000000000000000001000000000000000000010000000000000000000100000000000000000101000000000000000011111000000000000001111111000000000000010000011000000000000101110110000000000001000001100000000000011111111000000000000100100110000000000001111111000000000000010010010000000000000111111100000000000001001001000000000000011111110000000000000011111000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\^",
|
||||
"name": "ALT AIRCRAFT",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111110000000000000000110000000000000000001100000000001000000111100000010111111111111111111111111111111111111111101000000111100000010000000000110000000000000000001100000000000000000011000000000000000011111100000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\Y",
|
||||
"name": "RADIOS/DEVICES",
|
||||
"pixels": "0000000000000000000000000000001100000000000000000011000000000000000000110000000000000000001100000000000000001011000000000000000111110000000000000011111110000000000000100000110000000000001011101100000000000010000011000000000000111111110000000000001010101100000000000011111111000000000000101010110000000000001111111100000000000010101011000000000000011111100000000000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
{
|
||||
"format": "aprs-symbols-20",
|
||||
"version": 1,
|
||||
"width": 20,
|
||||
"height": 20,
|
||||
"symbols": [
|
||||
{
|
||||
"code": "/#",
|
||||
"name": "DIGI",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000100000000000000000001000000000000000000111000000000000000001110000000000000000111110000000000000001111100000000011111111111111111000011111111111111100000011111111111110000000001111111110000000000001111111000000000000011111110000000000001111111110000000000111110111110000000001111000111100000000111000000011100000001100000000011000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/X",
|
||||
"name": "HELICOPTER",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000001111111111111111100011100000100000111010000000011100000000110000001111100000001110000111111110000001111111111010010000111001111110110110001100001111111111100000000001111111111000000000001111111100000000000001000010000000000000111111101000000000000111111100000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/&",
|
||||
"name": "HF GATEWAY",
|
||||
"pixels": "0000000000000000000000000000000000000000000011111111111110000000100100100100100000001001001001001000000000010010010000000000000101110100000000000000011100000000000000000111000000000000000001110000000000000000111110000000000000001111100000000000000110001100000000000001100011000000000000011111110000000000001111111110000000000011100011100000000000110000011000000000011111111111000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/Y",
|
||||
"name": "SAILBOAT",
|
||||
"pixels": "0000000000000000000000000000001000000000000000000011000000000000000000110000000000000000001110000000000000001011100000000000000010111100000000000001101111000000000000011011111000000000001110111110000000000011101111110000000001111011111100000000011110111111100000001111101000000011000000000011111111100011111111111111110000111111111111111000000000000000000000000011111111111111100000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/'",
|
||||
"name": "SMALL ACFT",
|
||||
"pixels": "0000000000000000000000000000000000000000000001100000000000000000001100000000000000000001100000000000000000011100000000000000000011100000000011000000111100000000011000000111100000000111111111111111111001111111111111111111011111111111111111100110000001111000000011000000111100000000000000001110000000000000000111000000000000000001100000000000000000110000000000000000011000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/[",
|
||||
"name": "PERSON",
|
||||
"pixels": "0000000011110000000000000001111110000000000000011111100000000000000111111000000000000000111100000000000000000000000000000000011111111110000000001111111111110000000011011111101100000000110111111011000000001101111110110000000011011111101100000000110111111011000000000001111110000000000000011001100000000000000110011000000000000001100110000000000000011001100000000000000110011000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/-",
|
||||
"name": "VHF HOUSE",
|
||||
"pixels": "0000000000000000000000000000011111110000000000000000100000000000000000111110000000000000000010000000000000000001110000000000000011001000000000000001111010000000000000111111100000000000011111111000000000001111111111000000000111111111111000000001111111111110000000011100110001100000000111001100011000000001110011000110000000011111110001100000000111111100011000000001111111111110000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/\\",
|
||||
"name": "DIRECTION FIND",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111000000110000000000110000000100000000001000000001100000000110000000001000000001000000000011000000110000000000010000001000000000000110000110000000000000100001000000000000001100110000000000000001001000000000000000011110000000000000000011000000000000000000110000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/.",
|
||||
"name": "X",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000001100000000001100000011100000000111000000111100000011110000001111100001111100000001111100111110000000001111111111000000000001111111100000000000001111110000000000000111111110000000000011111111110000000001111100111110000000111110000111110000001111000000111100000011100000000111000000110000000000110000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/^",
|
||||
"name": "LARGE ACFT",
|
||||
"pixels": "0000000001100000000000000000111100000000000000001111000000000000000011110000000000000000111100000000000000011111100000000000001111111100000000000111111111100000000111111111111110000011111111111111110011111111111111111111111001101111011001111000011011110110000100000000111100000000000000001111000000000000000011110000000000000011111111000000000001111111111000000000011101101110000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/0",
|
||||
"name": "CIRCLE",
|
||||
"pixels": "0000000000000000000000000001111110000000000001111111111000000000111000000111000000011000000000011000001100000000000011000011000000000000110001100000000000000110011000000000000001100110000000000000011001100000000000000110011000000000000001100110000000000000011000110000000000001100001100000000000011000001100000000001100000001110000001110000000001111111111000000000000111111000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/_",
|
||||
"name": "WEATHER STN",
|
||||
"pixels": "0000000000000000000000000000000000000000001000000011000000000011000001110000000000111111110000000000001100000111000000000010001100110000000000000011000000000000000000110000000000000000001100000000000000000111100001110000000011111100011100000000111111000111000000000011000001110000000000110000011100000000001100000010000000000011000000100000000000111111111000000000001100000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/:",
|
||||
"name": "FIRE",
|
||||
"pixels": "0000000000000000000000000000001000000000000000000110000000000000000001100000000000000000111000000000000000001111001000000000000111110010000000000101111111100000000001111111111000000000111110111100100000001111100111011000000111111001111110000001111100011111100000011110000001111000000011100000011100000000111000001111000000000110000011100000000000110001110000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/a",
|
||||
"name": "AMBULANCE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000110000000000000000011100000000000000000111000000011111111111111100001111110011111001100011111100111110001000111100000011100001001111000000111111111011111100111111111110111111001111111111101100011111111000111010010011111100100110001110000000011100000011100000000111000000010000000000100000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/;",
|
||||
"name": "CAMPGROUND",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000100000000000000000011100000000000000000111000000000000000011111000000000000001111111000000000000011111110000000000001111011110000000000111100011100000000001111000111100000000111100000111100000001111000001111000001111111111111111110000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/b",
|
||||
"name": "BICYCLE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011100000000000000000000100000000000111000001000000000000100000111000000000001111111010000000000101000100100000011110010010011100001000100011001000100100100100100100100101011101110001011101010010010000010010010010001000000010001000011100000000011100000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/<",
|
||||
"name": "MOTORCYCLE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000011000000000000000000010000000000000000000110000000000000000111100000011111000011111111001000011000111100001000110011111110011000010010011111001001001011010111110101101010110100111001011010010010000000001001000011000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/f",
|
||||
"name": "FIRE TRUCK",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000111100000000000001111000000000000011110000000000000111100000000001000111000000000111111001111111111001000111111111111010010000111111111110111100001111111111101111111111111111111011111111111000100011111100011100100010011110010010011101110000001110000111011100000011100000100010000000010000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/>",
|
||||
"name": "CAR",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111000000000001111111111000000000111000100110000000001100001000110000001111111111111111110111111111111111111111110000111111000011111001100111100110011110111100000011110110000110000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/g",
|
||||
"name": "GLIDER",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111000000000000000000111000001100000000000111001111100000000000111111100010000000001111100000110000011111100000000110011111100000000000111111100000000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/C",
|
||||
"name": "CANOE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000111100000000000000001110000000000000000110000000000000000011000000011000000001100001101111000000110000111111111111011011111111111111101101111111110111110110111111111000000011000000000000000001100000000000000000110000000000000000111000000000000000011110000000000000000011000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/j",
|
||||
"name": "JEEP",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111110000000000001001000100000000000010010000100000000001100100001101111111111001111111111111111111111111111111000111111110001111100100111111001001111011100000000111001000111000000001110000000100000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/E",
|
||||
"name": "EYE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000111111000000000000111000011100000000011000000001100000001100001100001100000110000111100001100011000011111100001100110000111111000011001100001111110000110001100001111000011000001100001100001100000001100000000110000000001110000111000000000000111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/k",
|
||||
"name": "TRUCK",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111110000001111111111111100000011111111111111011100111111111111111100101111111111111111001011111111111111010011111111111111110111111100011100011110001110010010010011001001001110001110000111000011100011100001110000010000010000001000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/I",
|
||||
"name": "TCP/IP",
|
||||
"pixels": "0000000000000000000000000000000000000000000000011111100000000000000100001000000000000001000010000000000000010000100000000000000111111000000000000000011000000000000000000110000000000000000001100000000000001111111111110000000010000000000100000000100000000001000000111110000001111100001000100000010001000010001000000100010000111110000001111100000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/m",
|
||||
"name": "MIC-E REPEATER",
|
||||
"pixels": "0000100000000001000000010000000000001000000100100000010010000010011000000110010000100100111100100100001001001111001001000010010011110010010000100110011001100100000100100110010010000001000001100000100000001000011000010000000000001111000000000000000010010000000000000001100110000000000000010110100000000000000111111000000000000011000011000000000001111111111000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/K",
|
||||
"name": "SCHOOL",
|
||||
"pixels": "0000000000000000000000000000001110000000000000000011100000000000000000100000000000000000001000000000000000000111000000000000000011111000000000000001111111000000000000111000111000000000011110001111000000001111111111111000000111111111111111000001111111111111110000011001100011001100000110011000110011000001100110001100110000011111100011111100000111111000111111000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/O",
|
||||
"name": "BALLOON",
|
||||
"pixels": "0000000000000000000000000011111111000000000011101111011100000001110111111011100000011001111110011000001110111111110111000011101111111101110000111011111111011100000110111111110110000001110111111011100000001101111110110000000001101111011000000000001011110100000000000001011010000000000000001001000000000000000010010000000000000001111110000000000000011111100000000000000011110000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/P",
|
||||
"name": "POLICE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000100010001000000000000100100100000000000000000000000000000000100111001000000000000011111000000000000000000000000000000000111111110000000000011001100110000000001100011000110000011111111111111111111111111111111111111111000011111111000011100110011111100110010011110000000011110000011000000000011000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/s",
|
||||
"name": "POWERED SHIP",
|
||||
"pixels": "0000000000000000000000000000000000011000000000000000011110000000000000001111100000000000000110000000000000000000000000000000000000110000000000000000001100000000000000001111110000000000001111111100000000000111111111100000000011001010101100000111111111111111110000111111111111111100000111111111111110000000000000000000000000111111111111111100000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/R",
|
||||
"name": "REC VEHICLE",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111110000011111111111111110001111111111110001100011000011000111110000110000110001100110001100001100011000110011111111000111111110110001110001110001101001001111111001001000111000000000111000001110000000001110000001000000000001000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/v",
|
||||
"name": "VAN",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111000000111111111111111000001111111111110011000011111111111100011000111111111111000011001111111111111111111011111111111111111110110001111111100011101001001111110010011000111000000001110000001110000000011100000001000000000010000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/y",
|
||||
"name": "YAGI",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000100010001000100010001000100010001000100011111111111111111100100010001000100010001000100010001000100010000000100000000000000000001000000000000000000010000000000000000000100000000000000000001000000000000000000010000000000000000000100000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/T",
|
||||
"name": "SSTV",
|
||||
"pixels": "0000000000000000000000000001000010000000000000001001000000000000000001100000000000111111111111111100011000000000000001100100111111111111001001011000000000011010010100000000000010100101001110101000101001010001000100001010010100010001000010100101000000000000101001011000000000011010010011111111111100100110000000000000011000111111111111111100000011100000011100000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/U",
|
||||
"name": "BUS",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111100011111111111111111100100100100100100111001001001001001000110010010010010010001100111111111111110011001111111111111111111011000111111100011110100100111110010011101011101111101110111000111000000011100000000100000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/V",
|
||||
"name": "ATV",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000010000000000000000000110000000000110000011110000000011110001111110000011111111111111111000000011111111100001000110011111110011000010010011111001001001011010111110101101010110100000001011010010010000000001001000011000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\#",
|
||||
"name": "OVERLAY DIGI",
|
||||
"pixels": "0000000000000000000000000000111100000000000000111001110000000000111000000111000000011000000000011000000100000110000010000011000001100000110000100000111100000100010001111111111000100100011111111110001001000011111111000010010000011111100000100010000111111000010000110011100111001100000100110000110010000001100000000001100000001110000001110000000000111001110000000000000011110000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\&",
|
||||
"name": "OVERLAY GATE",
|
||||
"pixels": "0000000000000000000000000000111100000000000000111001110000000000111000000111000000011000011000011000000100001111000010000011000111111000110000100011111111000100010001111111111000100100111111111111001001001111111111110010010001111111111000100010001111111100010000110001111110001100000100001111000010000001100001100001100000001110000001110000000000111001110000000000000011110000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\-",
|
||||
"name": "HF HOUSE",
|
||||
"pixels": "0000000000001000000000000001111111111100000000000000100000000000000001111111000000000000000010000000000000000001110000000000000011001000000000000001111010000000000000111111100000000000011111111000000000001111111111000000000111111111111000000001111111111110000000011100110001100000000111001100011000000001110011000110000000011111110001100000000111111100011000000001111111111110000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "/W",
|
||||
"name": "NWS SITE",
|
||||
"pixels": "0000000000000000000000000000001000000000000000001111000000000000000111111100000000000011111111110000000011111111111110000001111111111111110000111111111111111100001111111111111111000001111111111111100000000110011001100000000000000000000000000000000000000000000000001000100010001000000110011001100110000000000000000000000000000010001000100000000001100110011000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\.",
|
||||
"name": "AMBIGUOUS",
|
||||
"pixels": "0000000000000000000000000000000000000000000000011111000000000000001111111000000000000111111111000000000001110001110000000000011100011100000000000000000111000000000000000011100000000000000001110000000000000000111100000000000000001110000000000000000011100000000000000000000000000000000000001110000000000000000011100000000000000000111000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\0",
|
||||
"name": "OVERLAY CIRCLE",
|
||||
"pixels": "0000000111111000000000000111111111100000000111100000011110000011100000000001110000110000000000001100011000011111100001100110001111111100011011000111111111100011110001111111111000111100011111111110001111000111111111100011110001111111111000111100011111111110001101100011111111000110011000011111100001100011000000000000110000111000000000011100000111100000011110000000011111111110000000000001111110000000"
|
||||
},
|
||||
{
|
||||
"code": "E0",
|
||||
"name": "ECHOLINK",
|
||||
"pixels": "0000000000000000000000000000011000000000000000001111000000000000000011110000000000000000011000000000000000001001000000000000000110011000000000000001000010000000000000110000110000000000011110011110000000000111100111100000000000110000110000000000010000000010000000001100000000110000000010000000000100000001100000000001100000111100000000111100001111000000001111000001100000000001100000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "I0",
|
||||
"name": "IRLP",
|
||||
"pixels": "0000000000000000000000000000011000000000000000001111000000000000000011110000000000000000111100000000000000000110000000000000000010010000000000000001100110000000000000010000100000000000001100001100000000000010000001000000000001100000011000000000010000000010000000001100000000110000000111100000011110000001111111111111100000011110000001111000000011000000001100000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "W0",
|
||||
"name": "WIRES",
|
||||
"pixels": "0000000000000000000000000000111000000000000000111111100000000000011101011100000000001010010010100000000111111111111100000001001001001001000000100010010010001000001001000100010010000011111111111111100000100100010001001000001000100100100010000001001001001001000000011111111111110000000010100100101000000000011101011100000000000011111110000000000000001110000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "YY",
|
||||
"name": "YAESU RADIO",
|
||||
"pixels": "0000000000100000000000000000001000000000000000000010000000000000000000100000000000000000101000000000000000011111000000000000001111111000000000000010000011000000000000101110110000000000001000001100000000000011111111000000000000100100110000000000001111111000000000000010010010000000000000111111100000000000001001001000000000000011111110000000000000011111000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\^",
|
||||
"name": "ALT AIRCRAFT",
|
||||
"pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111110000000000000000110000000000000000001100000000001000000111100000010111111111111111111111111111111111111111101000000111100000010000000000110000000000000000001100000000000000000011000000000000000011111100000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"code": "\\Y",
|
||||
"name": "RADIOS/DEVICES",
|
||||
"pixels": "0000000000000000000000000000001100000000000000000011000000000000000000110000000000000000001100000000000000001011000000000000000111110000000000000011111110000000000000100000110000000000001011101100000000000010000011000000000000111111110000000000001010101100000000000011111111000000000000101010110000000000001111111100000000000010101011000000000000011111100000000000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
<div id="aprs-pixel-workbench" class="aprs-workbench viz-dotted-background">
|
||||
<style>
|
||||
#aprs-pixel-workbench {
|
||||
--p-bg: light-dark(#f7fbfd, #101820);
|
||||
--p-panel: light-dark(#ffffff, #17232d);
|
||||
--p-ink: light-dark(#102536, #eef8ff);
|
||||
--p-muted: light-dark(#577184, #a8bdca);
|
||||
--p-line: light-dark(#b8d8e8, #355163);
|
||||
--p-grid: light-dark(#d8eef7, #29414f);
|
||||
--p-pixel: light-dark(#06141f, #f6fbff);
|
||||
--p-cyan: light-dark(#007b9d, #43dff8);
|
||||
color-scheme: light dark;
|
||||
color: var(--p-ink);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
padding: 16px;
|
||||
}
|
||||
#aprs-pixel-workbench * { box-sizing: border-box; }
|
||||
#aprs-pixel-workbench .shell {
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
background: var(--p-bg);
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#aprs-pixel-workbench header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: light-dark(#07385f, #082b48);
|
||||
color: #eefcff;
|
||||
}
|
||||
#aprs-pixel-workbench h1,
|
||||
#aprs-pixel-workbench h2 { margin: 0; font-weight: 500; }
|
||||
#aprs-pixel-workbench header span { color: #43dff8; }
|
||||
#aprs-pixel-workbench .stage {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(230px, 1fr) minmax(260px, 1fr) 96px;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
}
|
||||
#aprs-pixel-workbench .pane-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--p-muted);
|
||||
}
|
||||
#aprs-pixel-workbench .source-window {
|
||||
width: 100%;
|
||||
aspect-ratio: 156 / 136;
|
||||
background-image: url("./aprs_symbols_20_mockup.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 897.4359% 823.5294%;
|
||||
border: 1px solid var(--p-line);
|
||||
background-color: #fff;
|
||||
}
|
||||
#aprs-pixel-workbench .matrix {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(20, 1fr);
|
||||
aspect-ratio: 1;
|
||||
width: min(100%, 320px);
|
||||
background: var(--p-panel);
|
||||
border: 1px solid var(--p-line);
|
||||
}
|
||||
#aprs-pixel-workbench .matrix button {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-right: 1px solid var(--p-grid);
|
||||
border-bottom: 1px solid var(--p-grid);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
#aprs-pixel-workbench .matrix button.on { background: var(--p-pixel); }
|
||||
#aprs-pixel-workbench .native-wrap {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
#aprs-pixel-workbench .native {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(20, 1px);
|
||||
grid-template-rows: repeat(20, 1px);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: light-dark(#fff, #eaf3f8);
|
||||
outline: 1px solid var(--p-line);
|
||||
}
|
||||
#aprs-pixel-workbench .native i,
|
||||
#aprs-pixel-workbench .mini i { background: transparent; }
|
||||
#aprs-pixel-workbench .native i.on,
|
||||
#aprs-pixel-workbench .mini i.on { background: #07141d; }
|
||||
#aprs-pixel-workbench .selected-code {
|
||||
font-size: 22px;
|
||||
color: var(--p-cyan);
|
||||
text-align: center;
|
||||
}
|
||||
#aprs-pixel-workbench .tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 0 16px 16px;
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
}
|
||||
#aprs-pixel-workbench .tools button,
|
||||
#aprs-pixel-workbench .tools label,
|
||||
#aprs-pixel-workbench .symbol {
|
||||
color: var(--p-ink);
|
||||
background: var(--p-panel);
|
||||
border: 1px solid var(--p-line);
|
||||
font: inherit;
|
||||
}
|
||||
#aprs-pixel-workbench .tools button,
|
||||
#aprs-pixel-workbench .tools label {
|
||||
padding: 7px 10px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
#aprs-pixel-workbench .tools input { display: none; }
|
||||
#aprs-pixel-workbench .save-status {
|
||||
align-self: center;
|
||||
color: var(--p-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
#aprs-pixel-workbench .symbols {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(72px, 1fr));
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
#aprs-pixel-workbench .symbol {
|
||||
min-width: 0;
|
||||
padding: 7px 4px 6px;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
#aprs-pixel-workbench .symbol[aria-pressed="true"] {
|
||||
border-color: var(--p-cyan);
|
||||
box-shadow: inset 0 0 0 1px var(--p-cyan);
|
||||
}
|
||||
#aprs-pixel-workbench .mini {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(20, 2px);
|
||||
grid-template-rows: repeat(20, 2px);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #f7fbfd;
|
||||
}
|
||||
#aprs-pixel-workbench .symbol small {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--p-muted);
|
||||
}
|
||||
#aprs-pixel-workbench details { margin: 0 16px 16px; }
|
||||
#aprs-pixel-workbench summary { color: var(--p-cyan); }
|
||||
#aprs-pixel-workbench details img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--p-line);
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
#aprs-pixel-workbench .stage { grid-template-columns: 1fr 1fr; }
|
||||
#aprs-pixel-workbench .native-wrap { grid-column: 1 / -1; }
|
||||
#aprs-pixel-workbench .symbols { grid-template-columns: repeat(4, minmax(64px, 1fr)); }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
#aprs-pixel-workbench .stage { grid-template-columns: 1fr; }
|
||||
#aprs-pixel-workbench .symbols { grid-template-columns: repeat(3, minmax(64px, 1fr)); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<section class="shell" aria-label="Atelier de validation APRS 20 par 20">
|
||||
<header>
|
||||
<h1>APRS 20×20</h1>
|
||||
<span>48 pictogrammes · validation pixel par pixel</span>
|
||||
</header>
|
||||
|
||||
<div class="stage">
|
||||
<section>
|
||||
<div class="pane-title"><span>Planche source</span><strong id="source-name"></strong></div>
|
||||
<div id="source-window" class="source-window" role="img"></div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="pane-title"><span>Matrice CSS éditable</span><strong>20 × 20</strong></div>
|
||||
<div id="matrix" class="matrix" aria-label="Matrice de pixels éditable"></div>
|
||||
</section>
|
||||
<section class="native-wrap">
|
||||
<div class="pane-title">Taille écran</div>
|
||||
<div id="native" class="native" role="img" aria-label="Aperçu à taille réelle"></div>
|
||||
<div id="selected-code" class="selected-code"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="tools">
|
||||
<button id="undo-one" type="button">Rétablir ce picto</button>
|
||||
<button id="undo-all" type="button">Rétablir les 48</button>
|
||||
<button id="invert" type="button">Inverser les pixels</button>
|
||||
<button id="export-json" type="button">Exporter JSON</button>
|
||||
<label for="import-json">Importer JSON</label>
|
||||
<input id="import-json" type="file" accept="application/json,.json">
|
||||
<span id="save-status" class="save-status" aria-live="polite">Sauvegarde automatique active</span>
|
||||
</div>
|
||||
|
||||
<div id="symbols" class="symbols" aria-label="Sélection des 48 pictogrammes"></div>
|
||||
|
||||
<details>
|
||||
<summary>Afficher la planche complète</summary>
|
||||
<img src="./aprs_symbols_20_mockup.png" alt="Planche source des 48 pictogrammes APRS">
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const root = document.getElementById('aprs-pixel-workbench');
|
||||
const codes = ['/\x23','/X','/&','/Y',"/'",'/[','/-','/\\','/.','/^','/0','/_','/:','/a','/;','/b','/<','/f','/>','/g','/C','/j','/E','/k','/I','/m','/K','/O','/P','/s','/R','/v','/y','/T','/U','/V','\\\x23','\\&','\\-','/W','\\.','\\0','E0','I0','W0','YY','\\^','\\Y'];
|
||||
const names = ['DIGI','HELICOPTER','HF GATEWAY','SAILBOAT','SMALL ACFT','PERSON','VHF HOUSE','DIRECTION FIND','X','LARGE ACFT','CIRCLE','WEATHER STN','FIRE','AMBULANCE','CAMPGROUND','BICYCLE','MOTORCYCLE','FIRE TRUCK','CAR','GLIDER','CANOE','JEEP','EYE','TRUCK','TCP/IP','MIC-E REPEATER','SCHOOL','BALLOON','POLICE','POWERED SHIP','REC VEHICLE','VAN','YAGI','SSTV','BUS','ATV','OVERLAY DIGI','OVERLAY GATE','HF HOUSE','NWS SITE','AMBIGUOUS','OVERLAY CIRCLE','ECHOLINK','IRLP','WIRES','YAESU RADIO','ALT AIRCRAFT','RADIOS/DEVICES'];
|
||||
const sourceX = [17,189,362,535,708,881,1054,1227];
|
||||
const sourceY = [87,256,427,598,768,939];
|
||||
const encoded = 'AAAAAAAAAMDw/PDAAAAAAAAAAAAAAQMHh8////9/////z4cHAwEAAAAAAAYHAwEBAAAAAQEDBwYAAAAAgABgYGAgICAgoPCgICAgIGBgYAAbHw4EBAwcPn//f3N/evJ8OAAAAAAAAAAAAAAAAQMDAwMDAwIBAAAAAAAAABwEBHwExPzEBHwEBBwAAAAAAAAAAACA8PzPz8/88IAAAAAAAAAAAAAABAcHBQQEBAUHBwQAAAAAAAAAAAAAAIDgAP788MAAAAAAAAAAAICAoLi+v7+A/9/f39/c0MBgIAAABQUFBQUFBQUFBQUFBQUFAAAAgIAAAAAEDDjw4MCAAAAAAAAAAAAgPx8ODg4Oju7/fz8fDg4ODg4OBAAAAAAABAYDAQAAAAAAAAAAAAAAAAAAAAAAAACYvLy8mAAAAAAAAAAAAAAAABweA+P/P//jAx4cAAAAAAAAAAAAAAAGBwcABwcGAAAAAAAAAAAAAAAAAIDAwooq/ioKAgAAAAAAAAD4/P4fH///Hx8f/PgAAAAAAAAAAAcHBwcHBwcEBAQHBwAAAAAAAAAAMPCQEBAQEBAQEBCQ8DAAAAAAAAAAAAMOOOCAgOA4DgMAAAAAAAAAAAAAAAAAAAMDAAAAAAAAAAAAAAAAePjw4MCAAACAwODw+HgAAAAAAADA4PH7fz8fHz9/+/HgwAAAAAAAAAMDAQAAAAAAAAAAAQMDAAAAAAAAAACAgMDw3NzwwICAAAAAAAAMDg4HBwcHg/////+DBwcHBw4ODAAAAAAABwMDAQcHAQMDBwAAAAAAAAAAwOAwEBgICAgIGBAw4MAAAAAAAB9/4IAAAAAAAAAAAIDgfx8AAAAAAAAAAQEDAgICAgMBAQAAAAAAAAB8OBAQ0NAQOGxsAAAAAAAAAAAAAAAAGBz//xwYAAAAfPx8AAAAAAAAAAAAAAcHAgICAgICAwAAAAAAAAAAAACAAMDw/P7ggIDgAAAAAAAAAAA4/v//Hw8BAx+f//n8PgAAAAAAAAAAAQMCAAAAAgMDAQAAAAAAAIDAwMDAwEBAwMDA8Ph4QMCAAAAAfz+f35k5YGB5eX9/P5zcnD1+fAAAAAEDAQAAAAAAAAAAAQMBAAAAAAAAAAAAAADA8MAAAAAAAAAAAAAAAADA8Pj+Pw8HDz/++ODAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAEDAQAAAEBCQ4IAAAAAAcIgkdCSMciEnGQkFc4kkdySIcAAAAAEBAQAAAAAAAAAAAAEBAQAAAAAAAAAAAAAAAACImPDAAAAAAAAySbW1STMGHDw8Px8HM0m1tUkyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgMBAQGAgIDAQEBiIiIiAwIB+Pp/fnz+f358/YX98fD+Y2Jg5fwAAAQMBAAEDAQAAAAAAAAEDAQAAAAAAAICAwMDAwMDAwIAAAAAAAAB4fBxP7+1MHDw/PDwdT+7sTBx8eAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEDAwIAAAAAAAICAAAAMGDBgYHBwODgZHw8OBgcDAwEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgMBwODgQAAAOHx8enNxsNBgMFhsdHBwcHh8fDgACBwcDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4CAg4CAgIGCAAAA+Pg5m9mYOHh8fHBwfDmb2Zg8fPgAAAAAAAAAAAAAAAAAAAAAAAAAAAACAwGAwEBiIyMiIGBAwYMCAAAAABw8YMGBAx4+fn4/HQGAwGA8HAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ODg4ODg4ODg4ODg4OAAgICAAAA/H8/vzx8/H8/vzx8/PxPP6MgfPAAAAAEAAAAAAAEAAAAAAAABAAAAAAAAAAAAAHxExMREfAAAAAAAAAAAAOAgPCTkBAQHBwQE5CQ8IOAAAAAAAQEBAQEAAAAAAAABAQEBAQAAAAD4BgH4jABw8PBwAIz4AQb4AAAAAAADBAAB4LjPz7jgAQAEAwAAAAAAAAAAAgMDAgICAgMDAgAAAAAAAAAAAAAAAIDA4P7mxoAAAAAAAAAAAAD4/B4f//8cHBz//x8e/PgAAAAAAAMDAwMDAwAAAAMDAwMDAwAAAAAAAADw/Az2+v769gz88AAAAAAAAAAAAAMHHGHfv99hHAcDAAAAAAAAAAAAAAAAAAYGBgAAAAAAAAAAAAAAAAAAJAhAYGxgQAgkAAAAAABwOJjY3J47eXl/f3l5O57c2Jg4eAAAAAEBAAAAAAAAAAAAAAEBAAAAAAAAAAAAAAAAAMDQGAwMDg4AAAAAEDBweHx2dn93f3d/d3x4cDAAAAAAAQEBAQEBAQEBAQEBAQEBAQAAAIDA4ODg4ODg4ODg4GBgYODAAAAAPx/I6MgYPz8gICA/PxnJ684cOAAAAAABAAAAAAAAAAAAAAABAAAAAMDA4ODg4ODg4ODg4GBgwIAAAAAAPx/P788fPz8/Pz8/HMzszR8+PAAAAAABAAAAAAAAAAAAAAEAAAAAAOAAAADAAAAAwAAAAMAAAADAAAAADwEBAQcBAQH/AQEBBwEBAQcBAAAAAAAAAAAAAAMAAAAAAAAAAAAAAOAwkNBQUFJUWFhUUlBQ0JAw4AAA/4A/YEBCTkJAQkxCQEBgP4D/AAAAAQEDAwMBAQEBAQEDAwMBAQAAAOAwMPAwMPAwMPAwMPAwMHDw4AAAPw5m92YOPz4+Pz4OZ/ZkDD8/PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgMDAgAAAAIDI+ODAgAAAAAAwSbW1STMHHx8fHx8HM0m1tUkyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAcBgIDASG4uKGBAwIGHDAAAAADzDggANnfz8fHz9/ZwOA4DAPAAAAAAABAQMCBgQEBgIDAQEAAAAAAACA4DAYCITG4vLixoQIGDDggAAAAB9wwIYPHz9//38/Hw+GwHAfAAAAAAAAAQECBgQEBAYCAQEAAAAAAAAAAAAAAILCyooq/yoKCgICAAAAAAD4/P4fH///Hx8f/PgAAAAAAAAAAAcHBwcHBwcEBAQHBwAAAAAAAACAwODg8Pj8/P78+Pjw8ODAAAAAAAFDYwcHQ2MHB0NjBwdDYwEAAAAAAAAAAgMAAAIDAAACAwAAAAAAAAAAAABweHwcHBz8+PAAAAAAAAAAAAAAAAAAANze3wcBAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAACA4DAYiMzk5OTkzIgYMOCAAAAAAA84YMePnz8/Pz+fj8dgOA8AAAAAAAAAAAABAQEBAQEAAAAAAAAAAAAAAACAwMCsHh6swMCAAAAAAAAAAADA4PnFAwEAAAEDxfngwAAAAAAAAAABAQAAAAAAAAAAAQEAAAAAAAAAAAAAAIDcPj7cgAAAAAAAAAAAAADA4Pjuw4CAgIDD7vjgwAAAAAAAAAEDAwMBAAAAAAEDAwMBAAAAAACAYDAo/Cwm/iYs/CgwYIAAAAAAAA8yYqf6oiL/IqL6p2IyDwAAAAAAAAAAAAEBAwMDAQEAAAAAAAAAAAAAAAAAwGBwYH9gwIAAAAAAAAAAAAAAAAD/VFX9VVT/DwAAAAAAAAAAAAAAAAEDAwMDAwEAAAAAAAAAAIAAAAAAABCQ8PCQEAAAAAAAgAADBwMDAwMDQ0f//0dDAwMDAwMHAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgMDgwP7+gAAAAAAAAAAAAAAAAAD/qPqq+qj//wAAAAAAAAAAAAAAAAECAwIDAgMBAAAAAAAA';
|
||||
const raw = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
|
||||
const original = new Uint8Array(48 * 400);
|
||||
|
||||
for (let n = 0; n < 48; n++) {
|
||||
for (let y = 0; y < 20; y++) {
|
||||
for (let x = 0; x < 20; x++) {
|
||||
original[n * 400 + y * 20 + x] = (raw[n * 60 + Math.floor(y / 8) * 20 + x] >> (y & 7)) & 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function packPixels(values) {
|
||||
const bytes = new Uint8Array(values.length / 8);
|
||||
for (let i = 0; i < values.length; i++) if (values[i]) bytes[i >> 3] |= 1 << (i & 7);
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
function unpackPixels(encoded) {
|
||||
const bytes = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
|
||||
if (bytes.length !== 2400) throw new Error('Sauvegarde locale invalide');
|
||||
const values = new Uint8Array(48 * 400);
|
||||
for (let i = 0; i < values.length; i++) values[i] = (bytes[i >> 3] >> (i & 7)) & 1;
|
||||
return values;
|
||||
}
|
||||
|
||||
const storageKey = 'aprs-symbols-20-editor-v1';
|
||||
let localSaved = null;
|
||||
try { localSaved = JSON.parse(localStorage.getItem(storageKey)); } catch (_) {}
|
||||
const saved = window.openai?.widgetState?.privateContent || localSaved;
|
||||
let pixels = new Uint8Array(original);
|
||||
let selected = 5;
|
||||
if (saved) {
|
||||
try {
|
||||
if (typeof saved.packed === 'string') pixels = unpackPixels(saved.packed);
|
||||
else if (saved.pixels && saved.pixels.length === 19200)
|
||||
pixels = Uint8Array.from(saved.pixels, c => c === '1' ? 1 : 0);
|
||||
selected = Number.isInteger(saved.selected) ? Math.max(0, Math.min(47, saved.selected)) : selected;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const matrix = root.querySelector('#matrix');
|
||||
const native = root.querySelector('#native');
|
||||
const symbols = root.querySelector('#symbols');
|
||||
const source = root.querySelector('#source-window');
|
||||
const sourceName = root.querySelector('#source-name');
|
||||
const selectedCode = root.querySelector('#selected-code');
|
||||
const saveStatus = root.querySelector('#save-status');
|
||||
|
||||
function cells(container, index, editable) {
|
||||
container.replaceChildren();
|
||||
for (let p = 0; p < 400; p++) {
|
||||
const cell = document.createElement(editable ? 'button' : 'i');
|
||||
if (editable) {
|
||||
cell.type = 'button';
|
||||
cell.setAttribute('aria-label', `Pixel ${p % 20 + 1}, ${Math.floor(p / 20) + 1}`);
|
||||
cell.addEventListener('click', () => {
|
||||
pixels[index * 400 + p] ^= 1;
|
||||
renderSelected();
|
||||
renderMini(index);
|
||||
remember();
|
||||
});
|
||||
}
|
||||
cell.classList.toggle('on', !!pixels[index * 400 + p]);
|
||||
container.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMini(index) {
|
||||
const mini = symbols.children[index]?.querySelector('.mini');
|
||||
if (mini) cells(mini, index, false);
|
||||
}
|
||||
|
||||
function renderSelected() {
|
||||
cells(matrix, selected, true);
|
||||
cells(native, selected, false);
|
||||
selectedCode.textContent = codes[selected];
|
||||
sourceName.textContent = names[selected];
|
||||
const col = selected % 8;
|
||||
const row = Math.floor(selected / 8);
|
||||
const scale = source.clientWidth / 156;
|
||||
source.style.backgroundSize = `${1400 * scale}px ${1120 * scale}px`;
|
||||
source.style.backgroundPosition = `${-sourceX[col] * scale}px ${-sourceY[row] * scale}px`;
|
||||
source.setAttribute('aria-label', `${codes[selected]} ${names[selected]}, planche source`);
|
||||
[...symbols.children].forEach((button, i) => button.setAttribute('aria-pressed', String(i === selected)));
|
||||
}
|
||||
|
||||
function remember() {
|
||||
const packed = packPixels(pixels);
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify({ selected, packed }));
|
||||
saveStatus.textContent = 'Sauvegardé automatiquement';
|
||||
} catch (_) {
|
||||
saveStatus.textContent = 'Sauvegarde locale indisponible — exportez le JSON';
|
||||
}
|
||||
if (window.openai?.setWidgetState) {
|
||||
window.openai.setWidgetState({
|
||||
modelContent: { selectedCode: codes[selected], edited: !pixels.every((v, i) => v === original[i]) },
|
||||
privateContent: { selected, packed }
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function exportDocument() {
|
||||
return {
|
||||
format: 'aprs-symbols-20',
|
||||
version: 1,
|
||||
width: 20,
|
||||
height: 20,
|
||||
symbols: codes.map((code, index) => ({
|
||||
code,
|
||||
name: names[index],
|
||||
pixels: Array.from(pixels.subarray(index * 400, index * 400 + 400), p => p ? '1' : '0').join('')
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
function pixelsFromDocument(document) {
|
||||
if (!document || document.format !== 'aprs-symbols-20' || document.version !== 1 ||
|
||||
document.width !== 20 || document.height !== 20 || !Array.isArray(document.symbols) ||
|
||||
document.symbols.length !== 48) throw new Error('Format APRS 20x20 invalide');
|
||||
const next = new Uint8Array(48 * 400);
|
||||
document.symbols.forEach((symbol, index) => {
|
||||
if (!symbol || symbol.code !== codes[index] || typeof symbol.pixels !== 'string' ||
|
||||
!/^[01]{400}$/.test(symbol.pixels)) throw new Error(`Symbole ${codes[index]} invalide`);
|
||||
for (let p = 0; p < 400; p++) next[index * 400 + p] = symbol.pixels[p] === '1' ? 1 : 0;
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
codes.forEach((code, index) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'symbol';
|
||||
button.setAttribute('aria-label', `${code} ${names[index]}`);
|
||||
button.innerHTML = `<span class="mini" aria-hidden="true"></span><strong>${code}</strong><small>${names[index]}</small>`;
|
||||
button.addEventListener('click', () => { selected = index; renderSelected(); remember(); });
|
||||
symbols.appendChild(button);
|
||||
renderMini(index);
|
||||
});
|
||||
|
||||
root.querySelector('#undo-one').addEventListener('click', () => {
|
||||
pixels.set(original.subarray(selected * 400, selected * 400 + 400), selected * 400);
|
||||
renderSelected(); renderMini(selected); remember();
|
||||
});
|
||||
root.querySelector('#undo-all').addEventListener('click', () => {
|
||||
pixels = new Uint8Array(original);
|
||||
for (let i = 0; i < 48; i++) renderMini(i);
|
||||
renderSelected(); remember();
|
||||
});
|
||||
root.querySelector('#invert').addEventListener('click', () => {
|
||||
for (let p = 0; p < 400; p++) pixels[selected * 400 + p] ^= 1;
|
||||
renderSelected(); renderMini(selected); remember();
|
||||
});
|
||||
root.querySelector('#export-json').addEventListener('click', () => {
|
||||
const blob = new Blob([JSON.stringify(exportDocument(), null, 2) + '\n'], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'aprs_symbols_20_edited.json';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 0);
|
||||
saveStatus.textContent = 'JSON exporté';
|
||||
});
|
||||
root.querySelector('#import-json').addEventListener('change', async event => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
pixels = pixelsFromDocument(JSON.parse(await file.text()));
|
||||
for (let i = 0; i < 48; i++) renderMini(i);
|
||||
renderSelected();
|
||||
remember();
|
||||
saveStatus.textContent = 'JSON importé et sauvegardé';
|
||||
} catch (error) {
|
||||
saveStatus.textContent = error instanceof Error ? error.message : 'Import impossible';
|
||||
}
|
||||
event.target.value = '';
|
||||
});
|
||||
|
||||
window.addEventListener('openai:set_globals', event => {
|
||||
const next = event.detail?.globals?.widgetState?.privateContent;
|
||||
if (!next) return;
|
||||
try {
|
||||
if (typeof next.packed === 'string') pixels = unpackPixels(next.packed);
|
||||
else if (next.pixels && next.pixels.length === 19200)
|
||||
pixels = Uint8Array.from(next.pixels, c => c === '1' ? 1 : 0);
|
||||
else return;
|
||||
selected = Math.max(0, Math.min(47, next.selected || 0));
|
||||
for (let i = 0; i < 48; i++) renderMini(i);
|
||||
renderSelected();
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
new ResizeObserver(renderSelected).observe(source);
|
||||
|
||||
renderSelected();
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""AX.25 UI frames for APRS, and their Bell 202 bit stream.
|
||||
|
||||
build(src, dst, path, info) -> frame bytes (addresses + 0x03 0xF0 + info + FCS)
|
||||
hdlc_bits(frame) -> bits on the air before NRZI: flags, the frame
|
||||
LSB first with bit stuffing, flags
|
||||
decode(frame) -> "SRC>DST,PATH:info" (None if the FCS is wrong)
|
||||
|
||||
ax25.py [--call F4HWN] prints the test frames and their length
|
||||
"""
|
||||
import argparse
|
||||
|
||||
TXDELAY_FLAGS = 40 # ~267 ms of 0x7E before the frame (1 flag = 6.67 ms)
|
||||
TAIL_FLAGS = 3
|
||||
|
||||
|
||||
def crc16(data):
|
||||
"""CRC-16/X.25 (the AX.25 FCS): reflected 0x1021, init/xorout 0xFFFF."""
|
||||
crc = 0xFFFF
|
||||
for b in data:
|
||||
crc ^= b
|
||||
for _ in range(8):
|
||||
crc = (crc >> 1) ^ 0x8408 if crc & 1 else crc >> 1
|
||||
return crc ^ 0xFFFF
|
||||
|
||||
|
||||
def addr(call, last=False, repeated=False):
|
||||
"""One 7-byte address field: 6 chars shifted left, then the SSID byte."""
|
||||
call, _, ssid = call.upper().rstrip("*").partition("-")
|
||||
out = bytes((ord(c) << 1) for c in call.ljust(6)[:6])
|
||||
sb = 0x60 | ((int(ssid) if ssid else 0) << 1)
|
||||
if repeated:
|
||||
sb |= 0x80 # H bit: this digipeater has relayed it
|
||||
if last:
|
||||
sb |= 0x01 # end of the address field
|
||||
return out + bytes([sb])
|
||||
|
||||
|
||||
def build(src, dst="APZK5", path=(), info=""):
|
||||
hops = list(path)
|
||||
f = addr(dst) + addr(src, last=not hops)
|
||||
for i, h in enumerate(hops):
|
||||
f += addr(h, last=i == len(hops) - 1, repeated=h.endswith("*"))
|
||||
f += b"\x03\xF0" + info.encode("latin-1")
|
||||
fcs = crc16(f)
|
||||
return f + bytes([fcs & 0xFF, fcs >> 8])
|
||||
|
||||
|
||||
def hdlc_bits(frame, lead=TXDELAY_FLAGS, tail=TAIL_FLAGS):
|
||||
flag = [(0x7E >> i) & 1 for i in range(8)]
|
||||
bits = flag * lead
|
||||
ones = 0
|
||||
for b in frame:
|
||||
for i in range(8):
|
||||
bit = (b >> i) & 1
|
||||
bits.append(bit)
|
||||
ones = ones + 1 if bit else 0
|
||||
if ones == 5:
|
||||
bits.append(0) # stuffed zero
|
||||
ones = 0
|
||||
return bits + flag * tail
|
||||
|
||||
|
||||
def decode(frame):
|
||||
"""Readable form of a received frame, or None if too short / bad FCS."""
|
||||
if len(frame) < 18 or crc16(frame[:-2]) != frame[-2] | (frame[-1] << 8):
|
||||
return None
|
||||
calls, i = [], 0
|
||||
while i + 7 <= len(frame) - 2:
|
||||
c = "".join(chr(x >> 1) for x in frame[i:i + 6]).rstrip()
|
||||
ssid = (frame[i + 6] >> 1) & 15
|
||||
if ssid:
|
||||
c += "-%d" % ssid
|
||||
if len(calls) >= 2 and frame[i + 6] & 0x80:
|
||||
c += "*"
|
||||
calls.append(c)
|
||||
i += 7
|
||||
if frame[i - 1] & 1:
|
||||
break
|
||||
info = frame[i + 2:-2].decode("latin-1")
|
||||
return "%s>%s%s:%s" % (calls[1], calls[0],
|
||||
"".join("," + c for c in calls[2:]), info)
|
||||
|
||||
|
||||
def test_frames(call="F4HWN"):
|
||||
"""name -> frame bytes. 'badfcs' has one flipped info bit: must be rejected."""
|
||||
pos = "!4850.90N/00216.25E-UV-K5 APRS RX test"
|
||||
fr = {
|
||||
"pos": build(call, path=["WIDE1-1"], info=pos),
|
||||
"digi": build(call, path=["F1ZZZ-10*", "WIDE2-1"], info=pos),
|
||||
"msg": build(call, path=["WIDE1-1"], info=":%-9s:Hello from the Flipper{01" % call),
|
||||
"status": build(call, info=">Flipper Zero AFSK square-wave test"),
|
||||
"long": build(call, path=["WIDE1-1", "WIDE2-2"],
|
||||
info=">" + "".join(chr(0x41 + i % 26) for i in range(200))),
|
||||
}
|
||||
bad = bytearray(fr["pos"])
|
||||
bad[20] ^= 0x04
|
||||
fr["badfcs"] = bytes(bad)
|
||||
return fr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--call", default="F4HWN")
|
||||
a = ap.parse_args()
|
||||
for name, f in test_frames(a.call).items():
|
||||
print("%-7s %3d bytes %s" % (name, len(f), decode(f)))
|
||||
@@ -0,0 +1,11 @@
|
||||
Filetype: Flipper SubGhz RAW File
|
||||
Version: 1
|
||||
Frequency: 433650000
|
||||
Preset: FuriHalSubGhzPreset2FSKDev238Async
|
||||
Protocol: RAW
|
||||
RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227
|
||||
RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353
|
||||
RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417
|
||||
RAW_Data: 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -416 417 -417 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 416 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417
|
||||
RAW_Data: 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -417 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227
|
||||
RAW_Data: 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 10278
|
||||
@@ -0,0 +1,11 @@
|
||||
Filetype: Flipper SubGhz RAW File
|
||||
Version: 1
|
||||
Frequency: 433650000
|
||||
Preset: FuriHalSubGhzPreset2FSKDev238Async
|
||||
Protocol: RAW
|
||||
RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227
|
||||
RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353
|
||||
RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227
|
||||
RAW_Data: 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -417 416 -417 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -416 417 -417 416 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416
|
||||
RAW_Data: 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290
|
||||
RAW_Data: 227 -228 227 -417 416 -417 417 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -417 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 10000
|
||||
@@ -0,0 +1,18 @@
|
||||
Filetype: Flipper SubGhz RAW File
|
||||
Version: 1
|
||||
Frequency: 433650000
|
||||
Preset: FuriHalSubGhzPreset2FSKDev238Async
|
||||
Protocol: RAW
|
||||
RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227
|
||||
RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353
|
||||
RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417
|
||||
RAW_Data: 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 417 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 417 -417 416 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -417 417 -416 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416
|
||||
RAW_Data: 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -417 416 -417 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353
|
||||
RAW_Data: 417 -291 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 417 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -417 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417
|
||||
RAW_Data: 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 417 -417 416 -417 417 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -417 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -228
|
||||
RAW_Data: 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227
|
||||
RAW_Data: 227 -291 417 -416 417 -417 416 -417 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 417 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227
|
||||
RAW_Data: 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -417 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 417 -417 416 -417 417 -416 354 -227 227 -228
|
||||
RAW_Data: 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -417 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416
|
||||
RAW_Data: 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -417 416 -417 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416
|
||||
RAW_Data: 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 417 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -10139
|
||||
@@ -0,0 +1,10 @@
|
||||
Filetype: Flipper SubGhz RAW File
|
||||
Version: 1
|
||||
Frequency: 433650000
|
||||
Preset: FuriHalSubGhzPreset2FSKDev238Async
|
||||
Protocol: RAW
|
||||
RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227
|
||||
RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353
|
||||
RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417
|
||||
RAW_Data: 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 417 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 417 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417
|
||||
RAW_Data: 416 -417 417 -416 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 417 -416 417 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -417 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -10277
|
||||
@@ -0,0 +1,11 @@
|
||||
Filetype: Flipper SubGhz RAW File
|
||||
Version: 1
|
||||
Frequency: 433650000
|
||||
Preset: FuriHalSubGhzPreset2FSKDev238Async
|
||||
Protocol: RAW
|
||||
RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227
|
||||
RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353
|
||||
RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417
|
||||
RAW_Data: 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 417 -416 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228
|
||||
RAW_Data: 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -417 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -417 416 -417 417 -416 417 -417 416 -291 227 -227 228 -416 417 -417 416 -417 417 -416 417 -417
|
||||
RAW_Data: 416 -417 417 -416 417 -227 228 -227 290 -417 417 -416 417 -417 416 -417 417 -416 417 -417 416 -417 353 -228 227 -227 10076
|
||||
@@ -0,0 +1,10 @@
|
||||
Filetype: Flipper SubGhz RAW File
|
||||
Version: 1
|
||||
Frequency: 433650000
|
||||
Preset: FuriHalSubGhzPreset2FSKDev238Async
|
||||
Protocol: RAW
|
||||
RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227
|
||||
RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353
|
||||
RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -417 416 -417 417 -416 417 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227
|
||||
RAW_Data: 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 417 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 417 -417 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227
|
||||
RAW_Data: 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -417 417 -416 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -416 417 -417 416 -417 417 -227 227 -227 291 -417 416 -417 417 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 417 -417 416 -417 417 -416 417 -417 416 -417 291 -227 227 -227 10000
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Flipper Zero test transmitter for the APRS RX app: write Sub-GHz RAW files
|
||||
(.sub) that play an APRS frame (AX.25, Bell 202 AFSK 1200 bauds) on 433.650 MHz
|
||||
(433 MHz SRD band, the same bench channel as EPIRB 406).
|
||||
|
||||
The Flipper's CC1101 cannot frequency-modulate with an audio tone, only switch
|
||||
between two frequencies (preset 2FSKDev238Async, +/-2.38 kHz). So each AFSK
|
||||
tone is sent as a square wave: the carrier toggles at twice the tone frequency
|
||||
(1200 Hz mark, 2200 Hz space), phase-continuous across bit boundaries, and the
|
||||
receiver's discriminator outputs that square wave; its audio low-pass leaves
|
||||
mostly the fundamental. The RAW durations are the square wave's half-periods,
|
||||
rounded from exact edge times so the timing error never accumulates.
|
||||
|
||||
flipper_aprs.py <out_dir> [--call F4HWN] [--freq 433650000]
|
||||
-> aprs_pos.sub, _digi, _msg, _status, _long, _badfcs (see ax25.test_frames)
|
||||
One frame per file: press Send once, wait ~1 s before the next one.
|
||||
"""
|
||||
import argparse, os, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from ax25 import test_frames, hdlc_bits, decode
|
||||
|
||||
PRESET = "FuriHalSubGhzPreset2FSKDev238Async"
|
||||
BAUD = 1200
|
||||
MARK, SPACE = 1200.0, 2200.0
|
||||
LEAD_US = 100000 # unmodulated carrier before the flags (squelch / RSSI trigger)
|
||||
TAIL_US = 10000 # carrier kept after the last flag
|
||||
PER_LINE = 512 # values per RAW_Data line, as the Flipper writes them
|
||||
|
||||
|
||||
def nrzi_tones(bits):
|
||||
"""Bell 202 NRZI: a 0 changes the tone, a 1 keeps it. Starts on mark."""
|
||||
f, out = MARK, []
|
||||
for b in bits:
|
||||
if not b:
|
||||
f = SPACE if f == MARK else MARK
|
||||
out.append(f)
|
||||
return out
|
||||
|
||||
|
||||
def edges(tones, lead_us=LEAD_US, tail_us=TAIL_US):
|
||||
"""Exact edge times (us) of the square wave; level is high at t=0.
|
||||
Returns (edge_times, end_time)."""
|
||||
t0 = float(lead_us) # lead carrier holds the high level
|
||||
ph = 0.0 # tone phase in cycles (high while frac < 0.5)
|
||||
tb = 1e6 / BAUD
|
||||
ed = []
|
||||
for i, f in enumerate(tones):
|
||||
start, end = t0 + i * tb, t0 + (i + 1) * tb
|
||||
t = start
|
||||
while True:
|
||||
nxt = (int(ph * 2) + 1) / 2.0 # next half-cycle boundary
|
||||
dt = (nxt - ph) / f * 1e6
|
||||
if t + dt > end:
|
||||
ph += (end - t) * f / 1e6
|
||||
break
|
||||
t += dt
|
||||
ph = nxt
|
||||
ed.append(t)
|
||||
return ed, t0 + len(tones) * tb + tail_us
|
||||
|
||||
|
||||
def raw_durations(ed, end):
|
||||
"""Signed RAW durations: positive = high tone, negative = low tone."""
|
||||
pts = [0] + [round(t) for t in ed] + [round(end)]
|
||||
out, lv = [], 1
|
||||
for a, b in zip(pts, pts[1:]):
|
||||
out.append((b - a) if lv else -(b - a))
|
||||
lv ^= 1
|
||||
return out
|
||||
|
||||
|
||||
def sub_durations(frame):
|
||||
ed, end = edges(nrzi_tones(hdlc_bits(frame)))
|
||||
return raw_durations(ed, end)
|
||||
|
||||
|
||||
def write_sub(path, frame, freq):
|
||||
d = sub_durations(frame)
|
||||
lines = ["Filetype: Flipper SubGhz RAW File", "Version: 1",
|
||||
"Frequency: %d" % freq, "Preset: %s" % PRESET, "Protocol: RAW"]
|
||||
for i in range(0, len(d), PER_LINE):
|
||||
lines.append("RAW_Data: " + " ".join(str(x) for x in d[i:i + PER_LINE]))
|
||||
with open(path, "w") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
return d
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("out", nargs="?", default=".")
|
||||
ap.add_argument("--call", default="F4HWN")
|
||||
ap.add_argument("--freq", type=int, default=433650000)
|
||||
a = ap.parse_args()
|
||||
if 144000000 <= a.freq <= 146000000:
|
||||
sys.exit("refusing 2 m: the Flipper cannot reach it anyway, and 144.800 is live APRS")
|
||||
os.makedirs(a.out, exist_ok=True)
|
||||
for name, fr in test_frames(a.call).items():
|
||||
p = os.path.join(a.out, "aprs_%s.sub" % name)
|
||||
d = write_sub(p, fr, a.freq)
|
||||
print("%-28s %5.0f ms %5d edges %s" % (p, sum(abs(x) for x in d) / 1000,
|
||||
len(d), decode(fr) or "(bad FCS)"))
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Host model checks for APRSRX history; does not compile or execute the C app."""
|
||||
from collections import deque
|
||||
import random
|
||||
|
||||
HISTORY = 5
|
||||
|
||||
|
||||
def check_rotation():
|
||||
rng = random.Random(330)
|
||||
slots = [None] * HISTORY
|
||||
order = list(range(HISTORY))
|
||||
count = 0
|
||||
expected = deque(maxlen=HISTORY)
|
||||
last_time = 0
|
||||
now = 0xFFFFFF00
|
||||
accepted = 0
|
||||
for _ in range(10000):
|
||||
if rng.randrange(20) == 0:
|
||||
count = 0
|
||||
expected.clear()
|
||||
now = (now + rng.choice((0, 50, 999, 1000, 2000))) & 0xFFFFFFFF
|
||||
if expected and rng.randrange(3) == 0:
|
||||
payload = expected[0][0]
|
||||
else:
|
||||
payload = rng.randbytes(rng.choice((18, 19, 233, 330)))
|
||||
record = (payload, rng.randrange(-130, -20))
|
||||
duplicate = bool(expected and len(payload) == len(expected[0][0])
|
||||
and payload[-2:] == expected[0][0][-2:]
|
||||
and (now - last_time) & 0xFFFFFFFF < 1000)
|
||||
newest = slots[order[0]]
|
||||
reject = bool(count and len(payload) == len(newest[0])
|
||||
and payload[-2:] == newest[0][-2:]
|
||||
and (now - last_time) & 0xFFFFFFFF < 1000)
|
||||
assert reject == duplicate
|
||||
if not reject:
|
||||
recycled = order[HISTORY - 1]
|
||||
for k in range(HISTORY - 1, 0, -1):
|
||||
order[k] = order[k - 1]
|
||||
order[0] = recycled
|
||||
slots[recycled] = record
|
||||
count = min(count + 1, HISTORY)
|
||||
expected.appendleft(record)
|
||||
accepted += 1
|
||||
last_time = now
|
||||
assert sorted(order) == list(range(HISTORY))
|
||||
assert [slots[k] for k in order[:count]] == list(expected)
|
||||
print(f'History: 10000 events, {accepted} accepted; rotation, clear, duplicates, tick wrap OK')
|
||||
|
||||
|
||||
def check_scroll():
|
||||
# One frame at a time: the source on line 0, body rows from y = 8, scrolled
|
||||
# by top. Transcription of emit() and of draw()'s limit.
|
||||
for step in (6, 8):
|
||||
for rows in range(1, 60):
|
||||
end = 8 + rows * step # g.vrow after the last row
|
||||
lim = end - 32 if end > 32 else 0
|
||||
assert end < 65536
|
||||
for top in range(lim + 1):
|
||||
drawn = []
|
||||
for r in range(rows):
|
||||
y = (8 + r * step - top) & 0xFFFFFFFF
|
||||
if (y - 1) & 0xFFFFFFFF < 30:
|
||||
drawn.append(y)
|
||||
else:
|
||||
# Skipped rows lie wholly under the source (y <= 0, at
|
||||
# most 8 px high) or wholly below the separator.
|
||||
signed = 8 + r * step - top
|
||||
assert signed <= 0 or signed >= 31
|
||||
# No row is cut by the separator at rest positions the scroll can
|
||||
# reach from the top: the first body row starts right under the source.
|
||||
if top == 0:
|
||||
assert drawn[0] == 8
|
||||
# At the limit the last glyph row (ending one pixel before its row
|
||||
# height) is the last one above the y = 31 separator.
|
||||
assert end - 2 - lim <= 30
|
||||
if lim:
|
||||
assert end - 2 - lim == 30
|
||||
print('Scroll: both fonts, 1 to 59 body rows, every pixel offset; clipping and limit OK')
|
||||
|
||||
|
||||
def check_keys():
|
||||
# Transcription of handleKeys(): held UP/DOWN scrolls inside the frame; a
|
||||
# new press at an end changes frame.
|
||||
rng = random.Random(59)
|
||||
for _ in range(300):
|
||||
count = rng.randrange(1, HISTORY + 1)
|
||||
lims = [rng.choice((0, 0, 3, 40)) for _ in range(count)]
|
||||
cur = top = 0
|
||||
prev = None
|
||||
for _ in range(400):
|
||||
key = rng.choice((None, 'UP', 'DOWN', 'UP', 'DOWN', 'OTHER'))
|
||||
if rng.randrange(3):
|
||||
key = prev if prev else key # mostly held keys
|
||||
d = {'UP': -1, 'DOWN': 1}.get(key, 0)
|
||||
before = (cur, top)
|
||||
lim = lims[cur]
|
||||
moved = (top + d) & 0xFFFFFFFF
|
||||
if moved <= lim:
|
||||
top = moved
|
||||
elif key != prev and (cur + d) & 0xFFFFFFFF < count:
|
||||
cur = (cur + d) & 0xFF
|
||||
top = 0
|
||||
# Oracle.
|
||||
if d == 0:
|
||||
assert (cur, top) == before
|
||||
elif 0 <= before[1] + d <= lim:
|
||||
assert (cur, top) == (before[0], before[1] + d)
|
||||
elif key == prev or not 0 <= before[0] + d < count:
|
||||
assert (cur, top) == before # held at an end, or no such frame
|
||||
else:
|
||||
assert (cur, top) == (before[0] + d, 0)
|
||||
assert 0 <= cur < count and 0 <= top <= lims[cur]
|
||||
prev = key
|
||||
print('Keys: held scroll stays in the frame, a new press at an end changes frame OK')
|
||||
|
||||
|
||||
def check_columns():
|
||||
# Compare the C scratch-column shift against a per-pixel clipping oracle.
|
||||
for y in range(1, 31):
|
||||
for glyph in range(256):
|
||||
bits = (glyph << y) & 0xFFFFFFFF
|
||||
pages = [0] * 4
|
||||
page = 0
|
||||
while bits:
|
||||
assert page < 4
|
||||
pages[page] |= bits & 255
|
||||
bits >>= 8
|
||||
page += 1
|
||||
packed = sum(value << (8 * page) for page, value in enumerate(pages))
|
||||
expected = sum(1 << (y + bit) for bit in range(8)
|
||||
if glyph & (1 << bit) and y + bit < 32)
|
||||
assert packed == expected
|
||||
print('Scratch renderer: all 256 column patterns, 30 offsets, clipping OK')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
check_rotation()
|
||||
check_scroll()
|
||||
check_keys()
|
||||
check_columns()
|
||||
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Mic-E: an encoder written from the APRS 1.0.1 spec (chapter 10), and the
|
||||
display decoder of aprsrx_app.c transliterated byte for byte (uint8 wrap, no
|
||||
division), checked against each other and against a real FT3D frame.
|
||||
Also the standard (uncompressed) position display, against real frames.
|
||||
|
||||
mice.py runs the tests and prints the screen rows
|
||||
"""
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
MSG = ["Emergency", "Priority", "Special", "Committed",
|
||||
"Returning", "In Service", "En Route", "Off Duty"] # index = bits A B C
|
||||
|
||||
|
||||
# ------------------------------------------------------------ encoder (spec) --
|
||||
|
||||
def encode(call, lat, lon, speed_kn=0, course=0, sym="[/", msg=7, custom=False,
|
||||
comment="", path=(), marker="`", suffix="_0", alt_m=None):
|
||||
"""lat/lon in signed decimal degrees -> AX.25 frame bytes."""
|
||||
north, east = lat >= 0, lon >= 0
|
||||
lat, lon = abs(lat), abs(lon)
|
||||
ld = int(lat); lm = (lat - ld) * 60
|
||||
lmi = int(lm); lh = int(round((lm - lmi) * 100))
|
||||
if lh == 100: lmi, lh = lmi + 1, 0
|
||||
digits = "%02d%02d%02d" % (ld, lmi, lh)
|
||||
od = int(lon); om = (lon - od) * 60
|
||||
omi = int(om); oh = int(round((om - omi) * 100))
|
||||
if oh == 100: omi, oh = omi + 1, 0
|
||||
offset = od < 10 or od >= 100
|
||||
bits = [(msg >> 2) & 1, (msg >> 1) & 1, msg & 1]
|
||||
dest = ""
|
||||
for k, ch in enumerate(digits):
|
||||
d = int(ch)
|
||||
if k < 3:
|
||||
one = bits[k]
|
||||
dest += chr((ord("A") if custom else ord("P")) + d) if one else chr(ord("0") + d)
|
||||
elif k == 3:
|
||||
dest += chr(ord("P") + d) if north else chr(ord("0") + d)
|
||||
elif k == 4:
|
||||
dest += chr(ord("P") + d) if offset else chr(ord("0") + d)
|
||||
else:
|
||||
dest += chr(ord("P") + d) if not east else chr(ord("0") + d)
|
||||
if od <= 9: b1 = od + 118
|
||||
elif od <= 99: b1 = od + 28
|
||||
elif od <= 109: b1 = od + 8
|
||||
else: b1 = od - 72
|
||||
b2 = omi + 88 if omi <= 9 else omi + 28
|
||||
b3 = oh + 28
|
||||
b4 = speed_kn // 10 + 28
|
||||
b5 = (speed_kn % 10) * 10 + course // 100 + 28
|
||||
b6 = course % 100 + 28
|
||||
info = bytes([0x60, b1, b2, b3, b4, b5, b6]) + sym.encode()
|
||||
body = comment
|
||||
if alt_m is not None:
|
||||
v = alt_m + 10000
|
||||
body = chr(v // 8281 + 33) + chr(v // 91 % 91 + 33) + chr(v % 91 + 33) + "}" + body
|
||||
if marker:
|
||||
body = marker + body + suffix
|
||||
return _frame(call, dest, path, info + body.encode("latin-1"))
|
||||
|
||||
|
||||
def _frame(call, dest, path, info):
|
||||
from ax25 import addr, crc16
|
||||
hops = list(path)
|
||||
f = addr(dest) + addr(call, last=not hops)
|
||||
for i, h in enumerate(hops):
|
||||
f += addr(h, last=i == len(hops) - 1)
|
||||
f += b"\x03\xF0" + info
|
||||
c = crc16(f)
|
||||
return f + bytes([c & 0xFF, c >> 8])
|
||||
|
||||
|
||||
# ------------------------------------------- decoder (mirror of aprsrx_app.c) --
|
||||
|
||||
def sub(v, d):
|
||||
q = 0
|
||||
while v >= d:
|
||||
v -= d; q += 1
|
||||
return q, v
|
||||
|
||||
def put2(v):
|
||||
q, v = sub(v, 10)
|
||||
return chr(48 + q) + chr(48 + v)
|
||||
|
||||
def put3(v):
|
||||
q, v = sub(v, 100)
|
||||
return chr(48 + q) + put2(v)
|
||||
|
||||
def mic_digit(c):
|
||||
if 0x30 <= c <= 0x39: return c - 0x30
|
||||
if 0x41 <= c <= 0x4A: return c - 0x41
|
||||
if 0x50 <= c <= 0x59: return c - 0x50
|
||||
return 0 # K, L, Z: position ambiguity
|
||||
|
||||
def mic_bit(c):
|
||||
return c >= 0x50 or (0x41 <= c <= 0x4B)
|
||||
|
||||
def text_row(o, f, i, end):
|
||||
while len(o) < 32 and i < end:
|
||||
ch = f[i]
|
||||
o += "." if ch < 0x20 or ch > 0x7E else chr(ch)
|
||||
i += 1
|
||||
return o, i
|
||||
|
||||
def is_mice(f, i, end):
|
||||
return end - i >= 9 and f[i] in (0x60, 0x27, 0x1C, 0x1D)
|
||||
|
||||
def draw_mice(f, i, end):
|
||||
c = [f[k] >> 1 for k in range(6)]
|
||||
# row 1: position
|
||||
o = ""
|
||||
for k in range(6):
|
||||
o += chr(48 + mic_digit(c[k]))
|
||||
if k == 1: o += " "
|
||||
if k == 3: o += "."
|
||||
o += "N" if mic_bit(c[3]) else "S"
|
||||
deg = (f[i + 1] - 28) & 0xFF
|
||||
if mic_bit(c[4]): deg += 100
|
||||
if 180 <= deg <= 189: deg -= 80
|
||||
elif 190 <= deg <= 199: deg -= 190
|
||||
mn = (f[i + 2] - 28) & 0xFF
|
||||
if mn >= 60: mn -= 60
|
||||
o += " " + put3(deg) + " " + put2(mn) + "." + put2((f[i + 3] - 28) & 0xFF)
|
||||
o += "W" if mic_bit(c[5]) else "E"
|
||||
r1 = o
|
||||
# row 2: speed, course, symbol, message
|
||||
dc = (f[i + 5] - 28) & 0xFF
|
||||
q, dc = sub(dc, 10)
|
||||
sp = ((f[i + 4] - 28) & 0xFF) * 10 + q
|
||||
crs = dc * 100 + ((f[i + 6] - 28) & 0xFF)
|
||||
if sp >= 800: sp -= 800
|
||||
if crs >= 400: crs -= 400
|
||||
kmh = (sp * 1897) >> 10
|
||||
o = "%dkm/h %d " % (kmh, crs) + chr(f[i + 7]) + chr(f[i + 8]) + " "
|
||||
idx = mic_bit(c[0]) * 4 + mic_bit(c[1]) * 2 + mic_bit(c[2])
|
||||
custom = any(0x41 <= x <= 0x4B for x in c[:3])
|
||||
o += ("Custom-" + chr(48 + 7 - idx)) if custom and idx else MSG[idx]
|
||||
r2 = o
|
||||
# row 3: comment without the device markers, altitude first
|
||||
j = i + 9
|
||||
while end > j and f[end - 1] in (0x0D, 0x0A): end -= 1
|
||||
if j < end:
|
||||
m = f[j]
|
||||
if m in (0x60, 0x27):
|
||||
j += 1
|
||||
if end - j >= 2: end -= 2
|
||||
elif m in (0x3E, 0x5D):
|
||||
j += 1
|
||||
if end > j and f[end - 1] in (0x3D, 0x5E): end -= 1
|
||||
o = ""
|
||||
if end - j >= 4 and f[j + 3] == 0x7D:
|
||||
alt = (((f[j] - 33) * 91 + (f[j + 1] - 33)) * 91 + (f[j + 2] - 33)) - 10000
|
||||
o = "%dm " % alt
|
||||
j += 4
|
||||
r3, _ = text_row(o, f, j, end)
|
||||
return r1, r2, r3
|
||||
|
||||
|
||||
def safe(ch):
|
||||
return "." if ch < 0x20 or ch > 0x7E else chr(ch)
|
||||
|
||||
def draw_pos(f, i, end):
|
||||
"""Uncompressed position ('!' '=' or, after a 7-char timestamp, '/' '@'):
|
||||
None when the info field is anything else (shown as raw text)."""
|
||||
t = f[i]
|
||||
p = i + 1
|
||||
if t in (0x2F, 0x40): p += 7
|
||||
elif t not in (0x21, 0x3D): return None
|
||||
if end < p + 19 or f[p + 4] != 0x2E or f[p + 14] != 0x2E: return None
|
||||
q = f[p:]
|
||||
o = ""
|
||||
for k in range(18):
|
||||
if k == 8: continue
|
||||
if k in (2, 9, 12): o += " "
|
||||
o += safe(q[k])
|
||||
r1 = o
|
||||
o = ""
|
||||
if p > i + 1:
|
||||
o += "".join(safe(f[k]) for k in range(i + 1, p)) + " "
|
||||
o += safe(q[8]) + safe(q[18]) + " "
|
||||
r2, j = text_row(o, f, p + 19, end)
|
||||
r3, _ = text_row("", f, j, end)
|
||||
return r1, r2, r3
|
||||
|
||||
|
||||
def info_start(f):
|
||||
end = len(f) - 2
|
||||
e = 13
|
||||
while not (f[e] & 1) and e + 7 < end:
|
||||
e += 7
|
||||
return e + 3, end
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- tests --
|
||||
|
||||
def main():
|
||||
ok = True
|
||||
# Import the generator without writing assets; check every destination code.
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from gen_assets import a, mic_code, MIC_MSG
|
||||
blob, fields, _ = a.build()
|
||||
offsets = {name: (offset, size) for name, offset, size in fields}
|
||||
symbol_offset, symbol_size = offsets["SYM_MAP"]
|
||||
for slot in range(symbol_offset, symbol_offset + symbol_size, 3):
|
||||
assert not blob[slot] or blob[slot + 1] != 0
|
||||
# Verify the serialized front-end state independently of its generator.
|
||||
import struct
|
||||
init_offset, init_size = offsets["DEMOD_INIT"]
|
||||
assert init_size == 116
|
||||
words = struct.unpack_from('<29I', blob, init_offset)
|
||||
assert words[0] == 32768 and words[9:11] == (64, 64)
|
||||
assert all(value == 0 for i, value in enumerate(words) if i not in (0, 9, 10))
|
||||
assert offsets["COS1200"][0] == init_offset + init_size
|
||||
assert offsets["COS2200"][0] == init_offset + init_size + 8
|
||||
assert len(blob) <= 3840
|
||||
print(f"Demodulator initial state and table layout OK; assets {len(blob)}/3840 bytes")
|
||||
ui_size = dict(a.extra)["UI_SIZE"]
|
||||
for name in ("T_MIC", "T_MSG"):
|
||||
offset, size = offsets[name]
|
||||
assert offset + size <= ui_size
|
||||
lut_offset, lut_size = offsets["T_MIC"]
|
||||
assert lut_size == 128
|
||||
for c in range(128):
|
||||
packed = blob[lut_offset + c]
|
||||
assert packed == mic_code(c)
|
||||
assert packed & 15 == mic_digit(c)
|
||||
assert bool(packed & 16) == mic_bit(c)
|
||||
assert bool(packed & 32) == (0x41 <= c <= 0x4B)
|
||||
print("Mic-E asset lookup: all 128 codes equivalent")
|
||||
msg_offset, _ = offsets["T_MSG"]
|
||||
stride = dict(a.extra)["T_MSG_STRIDE"]
|
||||
for index, message in enumerate(MIC_MSG):
|
||||
start = msg_offset + index * stride
|
||||
assert blob[start:start + stride].split(b'\0', 1)[0].decode('ascii') == message
|
||||
print(f"UI asset read: all messages and lookup covered by {ui_size} bytes")
|
||||
# the FT3D frame received on 2026-09-30 (screen: >TXUPX9, `x,5l .[/`_0.)
|
||||
ft3d = _frame("F4HWN", "TXUPX9", (), b"`x,5l \x1c[/`_0\r")
|
||||
i, end = info_start(ft3d)
|
||||
rows = draw_mice(ft3d, i, end)
|
||||
print("FT3D ", rows)
|
||||
ok &= rows == ("48 50.89N 002 16.25E", "0km/h 0 [/ Off Duty", "")
|
||||
|
||||
cases = [
|
||||
# lat, lon, speed kn, course, msg, custom, alt, comment -> expected rows
|
||||
(48.848306, 2.270844, 0, 0, 7, False, None, "", "48 50.90N 002 16.25E"),
|
||||
(-33.8688, 151.2093, 25, 271, 6, False, 58, "Hello", "33 52.13S 151 12.56E"),
|
||||
(40.7128, -74.0060, 5, 90, 0, False, None, "", "40 42.77N 074 00.36W"),
|
||||
(51.5, -0.1276, 799, 359, 3, True, -12, "x", "51 30.00N 000 07.66W"),
|
||||
(10.0, 105.5, 12, 0, 5, False, 1234, "", "10 00.00N 105 30.00E"),
|
||||
(1.0, -179.99, 100, 180, 1, False, None, "", "01 00.00N 179 59.40W"),
|
||||
]
|
||||
for lat, lon, sp, crs, msg, cu, alt, com, want in cases:
|
||||
f = encode("F4HWN-7", lat, lon, sp, crs, "[/", msg, cu, com, alt_m=alt)
|
||||
i, end = info_start(f)
|
||||
r1, r2, r3 = draw_mice(f, i, end)
|
||||
kmh = (sp * 1897) >> 10
|
||||
m = ("Custom-%d" % (7 - msg)) if cu and msg else MSG[msg]
|
||||
w2 = "%dkm/h %d [/ %s" % (kmh, crs, m)
|
||||
w3 = ("%dm " % alt if alt is not None else "") + com
|
||||
good = (r1, r2, r3) == (want, w2, w3) and all(len(r) <= 32 for r in (r1, r2, r3))
|
||||
ok &= good
|
||||
print("OK " if good else "FAIL", (r1, r2, r3), "" if good else (want, w2, w3))
|
||||
# standard positions (aprsrx_app.c drawPos)
|
||||
pos_cases = [
|
||||
("F1PRY-14", "@300158z4921.07N/00150.50EN APRS OISE V=12.5V",
|
||||
("49 21.07N 001 50.50E", "300158z /N APRS OISE V=12.5V", "")),
|
||||
("F4HWN", "!4850.90N/00216.25E-UV-K5 APRS RX test",
|
||||
("48 50.90N 002 16.25E", "/- UV-K5 APRS RX test", "")),
|
||||
("F4HWN", "=4850.90S\\12345.67W>" + "x" * 60,
|
||||
("48 50.90S 123 45.67W", "\\> " + "x" * 29, "x" * 31)),
|
||||
("F4HWN", "!/5L!!<*e7>7P[", None), # compressed: raw text
|
||||
("F4HWN", "!4850.90N/00216.25", None), # too short
|
||||
("F4HWN", ">status text", None),
|
||||
]
|
||||
for call, info, want in pos_cases:
|
||||
f = _frame(call, "APZK5", (), info.encode("latin-1"))
|
||||
i, end = info_start(f)
|
||||
got = draw_pos(f, i, end)
|
||||
good = got == want and (got is None or all(len(r) <= 32 for r in got))
|
||||
ok &= good
|
||||
print("OK " if good else "FAIL", got, "" if good else want)
|
||||
print("ALL OK" if ok else "FAILURES")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,328 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pure-Python model of the APRS receive chain, and the integer demodulator the
|
||||
radio app will run (written to be transliterated to C: ints, shifts, no division).
|
||||
|
||||
Chain: source (Flipper square-wave FSK from the .sub durations, or a real
|
||||
station's sine AFSK with a given twist) -> discriminator output in Hz + offset +
|
||||
white noise -> radio audio path (RAW: 5 kHz low-pass as in EPIRB 406; STD: 300 Hz
|
||||
high-pass, 750 us de-emphasis, 3 kHz low-pass) -> 12-bit ADC on PA4 around 2048 at
|
||||
9.6 kHz (optional clock error) -> Demod -> frames.
|
||||
|
||||
Demod (per ADC sample):
|
||||
DC removal -> band-pass -> 4 sliding correlators over one bit (8 samples):
|
||||
I/Q at 1200 and 2200 Hz -> magnitudes (max + 3/8 min) -> per-tone peak AGC
|
||||
-> 3 slicers, decision sign(wa*Mm*Ps - wb*Ms*Pm) with wa:wb = 2:3, 1:1, 3:2,
|
||||
each with its own DPLL (16-bit phase per bit, nudged 1/4 toward mid-bit on
|
||||
each transition), NRZI, HDLC (flag, destuff, abort), FCS and UI check.
|
||||
|
||||
model_rx.py runs the test sweep, prints a table
|
||||
"""
|
||||
import math, os, random, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from ax25 import test_frames, hdlc_bits, crc16, decode
|
||||
from flipper_aprs import nrzi_tones, edges
|
||||
|
||||
FS_SIM = 96000
|
||||
FS_ADC = 9600
|
||||
BIAS = 2048
|
||||
LSB_PER_HZ = 0.065 # measured on the K1 (EPIRB 406 lab): ~2.3 kHz -> ~150 LSB
|
||||
FLIPPER_DEV = 2380.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- channel ----
|
||||
|
||||
def flipper_wave(frame):
|
||||
"""Discriminator output (Hz) of the Flipper square-wave FSK, box-averaged
|
||||
per simulation sample from the exact (rounded to us, as in the .sub) edges."""
|
||||
ed, end = edges(nrzi_tones(hdlc_bits(frame)))
|
||||
ed = [float(round(t)) for t in ed]
|
||||
n = int(end * 1e-6 * FS_SIM)
|
||||
T = 1e6 / FS_SIM
|
||||
out, lv, k = [], 1, 0
|
||||
for i in range(n):
|
||||
t0, t1 = i * T, (i + 1) * T
|
||||
acc, t = 0.0, t0
|
||||
while k < len(ed) and ed[k] < t1:
|
||||
acc += (ed[k] - t) * (1 if lv else -1)
|
||||
t = ed[k]
|
||||
lv ^= 1
|
||||
k += 1
|
||||
acc += (t1 - t) * (1 if lv else -1)
|
||||
out.append(FLIPPER_DEV * acc / T)
|
||||
return out
|
||||
|
||||
|
||||
def sine_wave(frame, dev=3000.0, twist_db=0.0):
|
||||
"""A real station: phase-continuous sine AFSK, space tone twist_db louder."""
|
||||
tones = nrzi_tones(hdlc_bits(frame))
|
||||
spb = FS_SIM / 1200.0
|
||||
out, ph = [], 0.0
|
||||
lead = [0.0] * int(0.1 * FS_SIM)
|
||||
a_space = dev * 10 ** (twist_db / 20)
|
||||
for i in range(int(len(tones) * spb)):
|
||||
f = tones[int(i / spb)]
|
||||
ph += f / FS_SIM
|
||||
out.append((dev if f == 1200.0 else a_space) * math.sin(2 * math.pi * ph))
|
||||
return lead + out
|
||||
|
||||
|
||||
def biquad_lp(x, fc):
|
||||
w = math.tan(math.pi * fc / FS_SIM)
|
||||
q = 1 / math.sqrt(2)
|
||||
n = 1 / (1 + w / q + w * w)
|
||||
b0 = w * w * n; b1 = 2 * b0; a1 = 2 * (w * w - 1) * n; a2 = (1 - w / q + w * w) * n
|
||||
y, x1, x2, y1, y2 = [], 0.0, 0.0, 0.0, 0.0
|
||||
for v in x:
|
||||
o = b0 * v + b1 * x1 + b0 * x2 - a1 * y1 - a2 * y2
|
||||
x2, x1, y2, y1 = x1, v, y1, o
|
||||
y.append(o)
|
||||
return y
|
||||
|
||||
|
||||
def rc_hp(x, fc):
|
||||
a = math.exp(-2 * math.pi * fc / FS_SIM)
|
||||
y, acc, prev = [], 0.0, 0.0
|
||||
for v in x:
|
||||
acc = a * (acc + v - prev)
|
||||
prev = v
|
||||
y.append(acc)
|
||||
return y
|
||||
|
||||
|
||||
def deemph(x, tau_us=750.0, ref_hz=1200.0):
|
||||
"""1-pole de-emphasis, unity gain at ref_hz."""
|
||||
k = 1 - math.exp(-1 / (FS_SIM * tau_us * 1e-6))
|
||||
g = math.sqrt(1 + (2 * math.pi * ref_hz * tau_us * 1e-6) ** 2)
|
||||
y, acc = [], 0.0
|
||||
for v in x:
|
||||
acc += k * (v - acc)
|
||||
y.append(acc * g)
|
||||
return y
|
||||
|
||||
|
||||
def channel(sig, mode="raw", noise_hz=0.0, off_hz=0.0, ppm=0.0, seed=1,
|
||||
gap_ms=150):
|
||||
rnd = random.Random(seed)
|
||||
gap = int(gap_ms * 1e-3 * FS_SIM)
|
||||
# no carrier around the burst: the discriminator outputs full-scale noise
|
||||
x = [rnd.gauss(0, 6000) for _ in range(gap)]
|
||||
x += [v + off_hz + rnd.gauss(0, noise_hz) for v in sig]
|
||||
x += [rnd.gauss(0, 6000) for _ in range(gap)]
|
||||
if mode == "raw":
|
||||
x = rc_hp(biquad_lp(x, 5000), 30)
|
||||
else:
|
||||
x = biquad_lp(deemph(rc_hp(x, 300)), 3000)
|
||||
step = FS_SIM / FS_ADC * (1 + ppm * 1e-6)
|
||||
out, p = [], 0.0
|
||||
while p < len(x) - 1:
|
||||
i = int(p); f = p - i
|
||||
v = x[i] * (1 - f) + x[i + 1] * f
|
||||
out.append(min(4095, max(0, int(round(BIAS + LSB_PER_HZ * v + rnd.gauss(0, 1))))))
|
||||
p += step
|
||||
return out
|
||||
|
||||
|
||||
# ------------------------------------------------------ integer demodulator --
|
||||
|
||||
def _tab(f, fs):
|
||||
per = fs // math.gcd(fs, int(f))
|
||||
c = [int(round(127 * math.cos(2 * math.pi * f * n / fs))) for n in range(per)]
|
||||
s = [int(round(127 * math.sin(2 * math.pi * f * n / fs))) for n in range(per)]
|
||||
return c, s
|
||||
|
||||
|
||||
def is_ui(f):
|
||||
"""An APRS frame is a UI frame: the address field ends (bit 0 of an SSID
|
||||
byte) and is followed by control 0x03 and PID 0xF0. Rejects the rare noise
|
||||
burst that passes the 16-bit FCS in continuous decoding."""
|
||||
end = len(f) - 2
|
||||
e = 13
|
||||
while not (f[e] & 1) and e + 7 < end:
|
||||
e += 7
|
||||
return bool(f[e] & 1) and e + 2 < end and f[e + 1] == 0x03 and f[e + 2] == 0xF0
|
||||
|
||||
|
||||
class Slicer:
|
||||
"""One decision threshold + its own DPLL and HDLC. The decision is
|
||||
sign(wa*Mm*Ps - wb*Ms*Pm): wa/wb != 1 favours one tone, which recovers
|
||||
frames whose twist the per-tone AGC does not fully cancel (Direwolf's
|
||||
multi-slicer idea)."""
|
||||
def __init__(self, dm, wa, wb):
|
||||
self.dm, self.wa, self.wb = dm, wa, wb
|
||||
self.dprev = 0; self.phase = 0; self.last = 0
|
||||
self.sr = 0; self.ones = 0; self.byte = 0; self.nb = 0
|
||||
self.buf = bytearray(); self.inframe = False
|
||||
self.good = 0
|
||||
|
||||
def step(self, a, b):
|
||||
dm = self.dm
|
||||
d = a * self.wa - b * self.wb
|
||||
# DPLL: sample at phase wrap (mid-bit), transitions pulled to mid-phase
|
||||
self.phase += dm.step
|
||||
if (d > 0) != (self.dprev > 0):
|
||||
self.phase += (dm.ctr - self.phase) >> dm.pll
|
||||
self.dprev = d
|
||||
if self.phase >= 65536:
|
||||
self.phase -= 65536
|
||||
lv = 1 if d > 0 else 0
|
||||
self.bit(1 if lv == self.last else 0)
|
||||
self.last = lv
|
||||
|
||||
def bit(self, b):
|
||||
self.sr = (self.sr >> 1) | (b << 7)
|
||||
if self.sr == 0x7E:
|
||||
if self.inframe and len(self.buf) >= 18 and \
|
||||
crc16(self.buf[:-2]) == self.buf[-2] | (self.buf[-1] << 8) and \
|
||||
is_ui(self.buf):
|
||||
self.good += 1
|
||||
self.dm.frame(bytes(self.buf))
|
||||
self.buf = bytearray(); self.inframe = True
|
||||
self.ones = self.byte = self.nb = 0
|
||||
return
|
||||
if b:
|
||||
self.ones += 1
|
||||
if self.ones > 6: # abort / noise: wait for a flag
|
||||
self.inframe = False
|
||||
return
|
||||
else:
|
||||
if self.ones == 5: # stuffed zero
|
||||
self.ones = 0
|
||||
return
|
||||
self.ones = 0
|
||||
if not self.inframe:
|
||||
return
|
||||
self.byte |= b << self.nb
|
||||
self.nb += 1
|
||||
if self.nb == 8:
|
||||
if len(self.buf) < 330:
|
||||
self.buf.append(self.byte)
|
||||
else:
|
||||
self.inframe = False
|
||||
self.byte = self.nb = 0
|
||||
|
||||
|
||||
class Demod:
|
||||
def __init__(self, fs=FS_ADC, agc=True, bp=True, att=5, dec=10, pll=2, ctr=36864,
|
||||
slicers=((2, 3), (1, 1), (3, 2))):
|
||||
self.ctr, self.bp, self.att, self.dec, self.pll = ctr, bp, att, dec, pll
|
||||
# 2nd-order band-pass (RBJ, constant 0 dB peak) centred on
|
||||
# sqrt(1200*2200) = 1625 Hz, Q = 0.9: coefficients in Q14
|
||||
w0 = 2 * math.pi * 1625 / fs
|
||||
al = math.sin(w0) / (2 * 0.9)
|
||||
a0 = 1 + al
|
||||
self.b0 = int(round(al / a0 * 16384))
|
||||
self.a1 = int(round(-2 * math.cos(w0) / a0 * 16384))
|
||||
self.a2 = int(round((1 - al) / a0 * 16384))
|
||||
self.x1 = self.x2 = self.y1 = self.y2 = 0
|
||||
self.N = fs // 1200 # correlation window = one bit
|
||||
self.cm, _ = _tab(1200, fs) # 8 entries at 9.6 kHz
|
||||
self.cs, _ = _tab(2200, fs) # 48 entries at 9.6 kHz
|
||||
self.im = self.qm = self.is_ = self.qs = 0
|
||||
self.ring = [[0, 0, 0, 0] for _ in range(self.N)]
|
||||
self.r = 0; self.km = 0; self.ks = 0
|
||||
self.dc = BIAS << 4
|
||||
self.pm = self.ps = 64
|
||||
self.agc = agc
|
||||
self.step = (65536 * 1200 + fs // 2) // fs
|
||||
self.sl = [Slicer(self, wa, wb) for wa, wb in slicers]
|
||||
self.frames = []
|
||||
self.maxab = 0
|
||||
|
||||
def frame(self, f):
|
||||
if f not in self.frames: # the same frame from several slicers
|
||||
self.frames.append(f)
|
||||
|
||||
@staticmethod
|
||||
def mag(i, q):
|
||||
a, b = abs(i), abs(q)
|
||||
if a < b: a, b = b, a
|
||||
return a + (b >> 2) + (b >> 3)
|
||||
|
||||
def sample(self, adc):
|
||||
self.dc += ((adc << 4) - self.dc) >> 6
|
||||
x = adc - (self.dc >> 4)
|
||||
if self.bp:
|
||||
y = (self.b0 * (x - self.x2) - self.a1 * self.y1 - self.a2 * self.y2) >> 14
|
||||
self.x2, self.x1, self.y2, self.y1 = self.x1, x, self.y1, y
|
||||
x = y
|
||||
# sin = cos read 3/4 period ahead: +6 of 8 (1200 Hz), +12 of 48 (2200 Hz:
|
||||
# 11 cycles per 48 samples, 12 samples = 2.75 cycles)
|
||||
cm, cs = self.cm, self.cs
|
||||
p = ((x * cm[self.km]) >> 8, (x * cm[(self.km + 6) & 7]) >> 8,
|
||||
(x * cs[self.ks]) >> 8, (x * cs[(self.ks + 12) % 48]) >> 8)
|
||||
o = self.ring[self.r]
|
||||
self.im += p[0] - o[0]; self.qm += p[1] - o[1]
|
||||
self.is_ += p[2] - o[2]; self.qs += p[3] - o[3]
|
||||
self.ring[self.r] = p
|
||||
self.r = (self.r + 1) % self.N
|
||||
self.km = (self.km + 1) % len(self.cm)
|
||||
self.ks = (self.ks + 1) % len(self.cs)
|
||||
mm, ms = self.mag(self.im, self.qm), self.mag(self.is_, self.qs)
|
||||
if self.agc:
|
||||
self.pm += (mm - self.pm) >> self.att if mm > self.pm else -(self.pm >> self.dec)
|
||||
self.ps += (ms - self.ps) >> self.att if ms > self.ps else -(self.ps >> self.dec)
|
||||
if self.pm < 16: self.pm = 16
|
||||
if self.ps < 16: self.ps = 16
|
||||
a, b = mm * self.ps, ms * self.pm
|
||||
else:
|
||||
a, b = mm, ms
|
||||
if a > self.maxab: self.maxab = a
|
||||
if b > self.maxab: self.maxab = b
|
||||
for s in self.sl:
|
||||
s.step(a, b)
|
||||
|
||||
|
||||
def run(adc, **kw):
|
||||
d = Demod(**kw)
|
||||
for s in adc:
|
||||
d.sample(s)
|
||||
return d.frames
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ sweep ----
|
||||
|
||||
def main():
|
||||
fr = test_frames()
|
||||
waves = {}
|
||||
def wave(name, src, twist=0.0):
|
||||
key = (name, src, twist)
|
||||
if key not in waves:
|
||||
waves[key] = flipper_wave(fr[name]) if src == "flipper" \
|
||||
else sine_wave(fr[name], twist_db=twist)
|
||||
return waves[key]
|
||||
|
||||
cases = []
|
||||
for mode in ("raw", "std"):
|
||||
for name in ("pos", "long", "badfcs"):
|
||||
cases.append(("flipper", 0.0, mode, name, 0, 0, 0))
|
||||
for noise in (1500, 3000, 4500, 6000):
|
||||
cases.append(("flipper", 0.0, mode, "pos", noise, 0, 0))
|
||||
for off in (-4000, 4000):
|
||||
cases.append(("flipper", 0.0, mode, "pos", 1500, off, 0))
|
||||
for ppm in (-10000, 10000):
|
||||
cases.append(("flipper", 0.0, mode, "long", 1500, 0, ppm))
|
||||
for tw in (-6.0, 0.0, 6.0):
|
||||
cases.append(("sine", tw, mode, "pos", 1500, 0, 0))
|
||||
|
||||
print("%-7s %5s %-4s %-6s %6s %6s %7s | %-7s %-7s" %
|
||||
("source", "twist", "mode", "frame", "noise", "off", "ppm", "agc", "no-agc"))
|
||||
for src, tw, mode, name, noise, off, ppm in cases:
|
||||
oks = []
|
||||
for seed in (1, 2, 3):
|
||||
adc = channel(wave(name, src, tw), mode, noise, off, ppm, seed)
|
||||
for agc in (True, False):
|
||||
got = run(adc, agc=agc)
|
||||
want = [] if name == "badfcs" else [fr[name]]
|
||||
oks.append((agc, got == want))
|
||||
res = ["%d/3" % sum(ok for a, ok in oks if a == agc) for agc in (True, False)]
|
||||
print("%-7s %5.0f %-4s %-6s %6d %6d %7d | %-7s %-7s" %
|
||||
(src, tw, mode, name, noise, off, ppm, res[0], res[1]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--one":
|
||||
f = test_frames()["pos"]
|
||||
got = run(channel(flipper_wave(f)))
|
||||
print([decode(g) for g in got])
|
||||
else:
|
||||
main()
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare demodulator variants on the hard cases of the sweep (dev tool).
|
||||
Run from a file (multiprocessing re-imports __main__)."""
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from multiprocessing import Pool
|
||||
from model_rx import *
|
||||
|
||||
FR = test_frames()
|
||||
CASES = []
|
||||
for mode in ("raw", "std"):
|
||||
for noise in (3000, 4000):
|
||||
CASES.append(("flipper", 0.0, mode, "pos", noise, 0, 0))
|
||||
CASES.append(("flipper", 0.0, mode, "long", 2000, 0, 10000))
|
||||
CASES.append(("flipper", 0.0, mode, "long", 2000, 0, -10000))
|
||||
for tw in (-9.0, -6.0, -3.0, 0.0, 3.0, 6.0, 9.0):
|
||||
CASES.append(("sine", tw, mode, "pos", 2500, 0, 0))
|
||||
CASES.append(("flipper", 0.0, mode, "badfcs", 1000, 0, 0))
|
||||
|
||||
VARIANTS = {
|
||||
"s1": dict(slicers=((1, 1),)),
|
||||
"s3x2": dict(slicers=((1, 2), (1, 1), (2, 1))),
|
||||
"s3x1.5": dict(slicers=((2, 3), (1, 1), (3, 2))),
|
||||
"s5": dict(slicers=((1, 2), (2, 3), (1, 1), (3, 2), (2, 1))),
|
||||
"s3x3": dict(slicers=((1, 3), (1, 1), (3, 1))),
|
||||
}
|
||||
SEEDS = (1, 2, 3, 4)
|
||||
|
||||
|
||||
def job(c):
|
||||
src, tw, mode, name, noise, off, ppm = c
|
||||
w = flipper_wave(FR[name]) if src == "flipper" else sine_wave(FR[name], twist_db=tw)
|
||||
row, mx = [], 0
|
||||
for seed in SEEDS:
|
||||
adc = channel(w, mode, noise, off, ppm, seed)
|
||||
want = [] if name == "badfcs" else [FR[name]]
|
||||
r = []
|
||||
for kw in VARIANTS.values():
|
||||
d = Demod(**kw)
|
||||
for s in adc:
|
||||
d.sample(s)
|
||||
r.append(d.frames == want)
|
||||
mx = max(mx, d.maxab)
|
||||
row.append(r)
|
||||
return c, [sum(r[i] for r in row) for i in range(len(VARIANTS))], mx
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with Pool() as p:
|
||||
res = p.map(job, CASES)
|
||||
names = list(VARIANTS)
|
||||
print("%-40s " % "case" + " ".join("%6s" % n for n in names))
|
||||
tot = [0] * len(names)
|
||||
mx = 0
|
||||
for c, r, m in res:
|
||||
print("%-40s " % (" ".join(str(x) for x in c)) + " ".join("%6d" % x for x in r))
|
||||
tot = [a + b for a, b in zip(tot, r)]
|
||||
mx = max(mx, m)
|
||||
print("%-40s " % ("TOTAL /%d" % (len(CASES) * len(SEEDS))) + " ".join("%6d" % x for x in tot))
|
||||
print("max(Mm*Ps, Ms*Pm) = %d (x3 = %d, int32 max 2147483647)" % (mx, 3 * mx))
|
||||
@@ -0,0 +1,126 @@
|
||||
# APRS TX: on-radio APRS position beacon (work in progress)
|
||||
|
||||
Sends one APRS position frame (AX.25 UI frame, Bell 202 AFSK 1200 bauds) on the
|
||||
TX VFO at each press of PTT or MENU. Companion of [APRS RX](../aprsrx/README.md),
|
||||
which is also the test receiver.
|
||||
|
||||
| Step | State |
|
||||
|---|---|
|
||||
| Frame and bit stream model (`test/tx_model.py`) | **Done**: identical to the RX app's AX.25 reference, decoded by the RX model |
|
||||
| Radio app (`aprstx_app.c`) | **v0.1 works on the radio** (2026-09-30); v0.2 edits the position; v0.3 adds the SSID and path; v0.4 adds the corrected 20x20 bitmap and the 48-symbol selector; v0.5 keeps the source fixed while the frame body scrolls (not built here) |
|
||||
| On-air test: APRS RX on a second radio, FT3D | **Done** (2026-09-30, v0.1: frames received by a UV-K1 running APRS RX and by the FT3D, first try, default level 66 and twist 0) |
|
||||
|
||||
## Station settings (`gen_assets.py`, then rebuild)
|
||||
|
||||
| Setting | Default |
|
||||
|---|---|
|
||||
| Source | the boot-message callsign (API `boot_callsign`, 1-6 letters/digits) + `SSID` = 7 (0-15, 0 = none): the **default**, until one is chosen on the radio |
|
||||
| `DEST` | `APZK5` (APZ = experimental software) |
|
||||
| `PATH` | `1` = `WIDE1-1`, an index in `PATHS`: `DIRECT` (no digipeater), `WIDE1-1`, `WIDE1-1,WIDE2-1`; the **default**, until one is chosen on the radio |
|
||||
| `LAT`, `LON` | `4850.90N`, `00216.25E`: the **default** position, until one is edited on the radio (key 3) |
|
||||
| `SYMBOL` | `/[` (person): the default, until one of the 48 symbols is chosen on the radio |
|
||||
| `COMMENT` | `UV-K5/K1 F4HWN Firmware` (43 characters at most) |
|
||||
|
||||
Frame sent: `F4HWN-7>APZK5,WIDE1-1:!4850.90N/00216.25E[UV-K5/K1 F4HWN Firmware` (68 bytes,
|
||||
~0.79 s on the air: 50 ms of tone settle, 40 flags = 267 ms, the frame, 3 flags).
|
||||
If the boot message is not a plain callsign (empty, more than 6 characters once
|
||||
spaces are dropped, or with a `/`), the app shows `No boot callsign` and does not
|
||||
transmit.
|
||||
|
||||
In the normal view, the TX frequency is aligned left below the separator and
|
||||
the selected APRS symbol is shown as a 20x20 bitmap at x=108, y=33-52. The 48-symbol
|
||||
Yaesu bitmap set is stored in the app assets; the editor keeps the full width.
|
||||
|
||||
## Keys (UV-K5 and UV-K1)
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| PTT or MENU | Send one frame |
|
||||
| UP / DOWN (held) | Scroll the frame body above the separator 1 px per 40 ms loop (UV-K1: LEFT/RIGHT, as `nav_dir`), while the source callsign stays fixed in bold on line 0 as in APRS RX. In the status bar (x = 72-76), ▲ while rows are hidden above and ▼ while rows are hidden below; while F is armed its icon (x = 70-78, centred on them) takes their place, and they are not drawn while transmitting (the `TRANSMIT` capsule ends at x = 72). The body uses the small font, 18 characters a row, 3 visible rows at a time: `>DEST,WIDE…` (a row breaks after a comma), latitude, longitude, then symbol and comment |
|
||||
| 1 / F then 1 | Tone level (deviation) up / down: REG_70 gain 10-127, step 4, default 66 (the firmware's tone gain) |
|
||||
| 2 / F then 2 | Twist `tw` up / down, -4..+8: 2200 Hz gain = level × (8 + tw) / 8 (-6..+6 dB) |
|
||||
| F | Arm the next key's down direction (1, 2), as FoxHunt's F; icon in the status bar while armed |
|
||||
| * | Scroll view / compact view (saved): compact shows the source in bold, then the path, the position `48 50.90N 002 16.25E`, the symbol and the comment in the tiny 3x5 font, 32 characters a row, without scroll |
|
||||
| 3 | Edit the position, SSID, path and symbol |
|
||||
| EXIT | Quit (level, twist, position, SSID, path, symbol and view are saved) |
|
||||
|
||||
### Editor (v0.3)
|
||||
|
||||
```
|
||||
LAT 48 50.90 N
|
||||
LON 002 16.25 E
|
||||
SSID 7
|
||||
PATH WIDE1-1
|
||||
SYMB /[
|
||||
|
||||
0-9, * change, F+* back
|
||||
UP/DN move MENU ok EXIT
|
||||
```
|
||||
|
||||
The field under the cursor is drawn in bold: a digit or the N/S, E/W letter of
|
||||
the position, or the whole SSID, path or symbol value. Type the 13 digits in a row, as a
|
||||
frequency: `485090` then `0021625` (degrees, minutes, hundredths of a minute: the
|
||||
APRS format, read as is from a GPS or aprs.fi in degrees-minutes). The cursor
|
||||
skips the separators and N/S, and goes on from the latitude to the longitude;
|
||||
UP/DOWN also stop on N/S and E/W, then take it on to SSID, PATH and SYMB.
|
||||
When SYMB is selected, both shortened help rows remain visible and the edited
|
||||
20x20 symbol is previewed at x=108, the same position as on the normal screen.
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| 0-9 | Digit at the cursor, then the next one (on a position digit) |
|
||||
| * | N/S (latitude line), E/W (longitude line), next SSID 0-15 (SSID), next path (PATH), or next symbol (SYMB) |
|
||||
| F then * | Previous SSID, path or symbol (the `F` icon in the status bar while F is armed, as in FoxHunt; F again disarms it) |
|
||||
| UP / DOWN | Move the cursor: 6 digits, N/S, 7 digits, E/W, SSID, PATH, SYMB |
|
||||
| MENU | Check (degrees ≤ 90 / 180, minutes < 60) and keep: the frame is rebuilt; `Invalid position` otherwise |
|
||||
| EXIT | Cancel |
|
||||
|
||||
Paths: `DIRECT` (heard only by the stations and iGates in range), `WIDE1-1` (one
|
||||
repeat by a nearby digipeater), `WIDE1-1,2-1` (`WIDE1-1,WIDE2-1`: the usual
|
||||
mobile / portable path, two repeats).
|
||||
|
||||
Everything is saved on exit (`cfg_save`, 13 bytes: magic 0xA8, level, twist, 13
|
||||
digits and the hemispheres in nibbles, `0x80 | path << 4 | SSID`, then the view:
|
||||
14 compact, anything else scroll, then the symbol index). An older config with
|
||||
byte 12 erased uses the default person symbol. A v0.2 config (10 bytes: byte 10 erased)
|
||||
keeps its position and takes the default SSID and path; an 11-byte config
|
||||
(byte 11 erased) opens in the scroll view; a v0.1 config (magic 0xA7) is
|
||||
ignored: defaults.
|
||||
|
||||
## How it transmits
|
||||
|
||||
As the Beacon app keys MCW: `tx_set_params` (carrier + PA on the TX VFO),
|
||||
REG_51 = 0 (no CTCSS/DCS under the AFSK), `tx_tone(1200)` (tone path on, mic ADC
|
||||
off, 50 ms settle). Then each bit is timed from SysTick, 40000 cycles at 48 MHz
|
||||
(exact), and at each NRZI transition the tone frequency REG_71 (1200 Hz = 12389,
|
||||
2200 Hz = 22714, the driver's `scale_freq`) and gain REG_70 are rewritten: two
|
||||
SPI writes, a few tens of µs out of 833. `tx_mute` + `tx_end` restore RX.
|
||||
|
||||
## Unknowns for the first on-air test (all fine with the defaults on 2026-09-30)
|
||||
|
||||
1. **Phase continuity** when REG_71 is rewritten (an NCO keeps it, probably).
|
||||
The RX model decodes even with a phase reset at every transition (3/3 clean,
|
||||
2/3 with noise in STD); a hardware TNC such as the FT3D may be stricter.
|
||||
2. **Twist.** The tone probably enters after the pre-emphasis, so the signal is
|
||||
flat and a receiver with de-emphasis sees 2200 Hz ~5 dB low. If the FT3D
|
||||
misses frames that APRS RX decodes, raise `tw` (key 2): +4 is +3.5 dB.
|
||||
3. **Deviation** at level 66: aim for ~3 kHz; 1 up, F then 1 down.
|
||||
|
||||
## Test plan
|
||||
|
||||
1. Simplex test frequency (not 144.800 for the first frames), low power.
|
||||
2. Receivers: APRS RX on a second radio (its per-slicer counters `s a/b/c`
|
||||
show the twist: frames only on the space-favouring slicer = 2200 Hz low) and
|
||||
the FT3D (APRS on band B, same frequency).
|
||||
3. PTT, check both; adjust level and twist; then 144.800.
|
||||
|
||||
`test/tx_model.py` (needs `../aprsrx/test`): frame from the real assets vs
|
||||
`ax25.build`, bit stream vs `ax25.hdlc_bits`, bad boot callsigns refused, an
|
||||
edited position (south/west) vs `ax25.build`, every path with SSID 0, 15 and 9
|
||||
and a selected symbol vs `ax25.build`, the config round trip (and a v0.2 config
|
||||
falling back to the default SSID, path and symbol), the editor rows and bold column, the editor keys (13
|
||||
digits typed in a row, cursor stops, `*` and F then `*` on each field; the
|
||||
UP/DOWN keys through `nav_dir()`, with SET_NAV on and off), the position limits,
|
||||
then
|
||||
AFSK (continuous phase or reset, tw 0/+4, TX path twist 0/+5 dB) decoded by the
|
||||
RX model through its RAW and STD audio paths, with and without noise.
|
||||
@@ -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,
|
||||
"APRS TX overlay app overflows the 4 KiB overlay")
|
||||
|
||||
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* APRS TX - sends one APRS position frame (AX.25 UI frame, Bell 202 AFSK
|
||||
* 1200 bauds) on the TX VFO at each press of PTT or MENU.
|
||||
*
|
||||
* The source is the boot-message callsign (API boot_callsign) with an SSID;
|
||||
* destination, the WIDE path entries, symbol and comment are AX.25-encoded in
|
||||
* gen_assets.py (assets). The position, the SSID, the path (DIRECT, WIDE1-1
|
||||
* or WIDE1-1,WIDE2-1) and the symbol are edited on the radio (key 3) and saved; gen_assets.py
|
||||
* holds the defaults. The frame is built at launch and after each edit, FCS
|
||||
* included.
|
||||
*
|
||||
* AFSK comes from the BK4829 tone generator, as the Beacon app keys MCW: the tone
|
||||
* frequency (REG_71) is rewritten at each NRZI transition, 1200 Hz mark / 2200 Hz
|
||||
* space, bits timed to the cycle (40000 cycles at 48 MHz) from SysTick. The
|
||||
* mic ADC is off while a tone is sent; any CTCSS/DCS of the VFO is cleared
|
||||
* (REG_51) so nothing rides under the AFSK. test/tx_model.py mirrors the frame
|
||||
* and bit stream and decodes them with the APRS RX model.
|
||||
*
|
||||
* Keys (UV-K5 and UV-K1): PTT or MENU send · UP/DOWN (held) scroll the frame
|
||||
* 1 px per loop · 1 / F then 1 tone level up / down (deviation, REG_70 gain
|
||||
* 10-127) · 2 / F then 2 twist up / down: 2200 Hz gain = level x (8 + tw) / 8,
|
||||
* tw -4..+8 (-6..+6 dB), for a receiver's de-emphasis · 3 edit the position,
|
||||
* SSID, path and symbol · * scroll view / compact view · EXIT quit. F as FoxHunt's: the next key goes down, an icon
|
||||
* in the status bar while armed.
|
||||
* Editor (the field under the cursor in bold): 0-9 type a digit (the cursor
|
||||
* skips the separators and N/S, and moves on) · * toggle N/S or E/W on the
|
||||
* position lines, next SSID, path or symbol on theirs · F then * the previous
|
||||
* value · UP/DOWN move the cursor (6 digits, N/S, 7 digits, E/W, SSID, path, symbol) ·
|
||||
* MENU check and keep · EXIT cancel.
|
||||
* Level, twist, position, SSID, path, symbol and view are saved on exit (cfg_save, 13
|
||||
* bytes).
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "aprstx_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
/* ---- SysTick (core at 48 MHz, 10 ms period) ---- */
|
||||
#define SYST_LOAD (*(volatile uint32_t *)0xE000E014u)
|
||||
#define SYST_VAL (*(volatile uint32_t *)0xE000E018u)
|
||||
#define CYC_PER_BIT (48000000u / 1200u) /* 40000: exact */
|
||||
|
||||
/* ---- BK4829 ---- */
|
||||
#define REG_51 0x51 /* sub-audio: 0 = no CTCSS/DCS (BK4819_ExitSubAu) */
|
||||
#define REG_70 0x70 /* tone 1: <15> enable, <14:8> gain */
|
||||
#define REG_71 0x71 /* tone 1 frequency, Hz x 10.32444 */
|
||||
#define F_MARK 12389u /* 1200 Hz: (f * 1353245 + 2^16) >> 17, scale_freq */
|
||||
#define F_SPACE 22714u /* 2200 Hz */
|
||||
|
||||
#define LVL_DEF 66 /* BK4819_TransmitTone's tone gain */
|
||||
#define LVL_MIN 10
|
||||
#define LVL_MAX 127
|
||||
#define LVL_STEP 4
|
||||
#define TW_MIN (-4) /* 2200 Hz gain x 0.5 (-6 dB) */
|
||||
#define TW_MAX 8 /* 2200 Hz gain x 2 (+6 dB) */
|
||||
|
||||
/* ---- AX.25 ---- */
|
||||
#define TXDELAY_FLAGS 40u /* 267 ms of flags before the frame */
|
||||
#define TAIL_FLAGS 3u
|
||||
#define CALL_MAX 6u
|
||||
#define INFO_LEN (1u + 8u + 1u + 9u + 1u + F_COMMENT_LEN) /* !lat/lon[comment */
|
||||
#define PATH_MAX 2u /* WIDE entries: 0 = DIRECT */
|
||||
#define FRAME_MAX (7u + 7u + PATH_MAX * 7u + 2u + INFO_LEN + 2u)
|
||||
|
||||
/* position: 13 digits DDMMhh DDDMMhh, hemispheres bit 0 south, bit 1 west */
|
||||
#define POS_DIGITS 13u
|
||||
#define LAT_DIGITS 6u
|
||||
#define TX_CAPS_X 40u /* "TRANSMIT" capsule in the status bar, after the title */
|
||||
#define F_X 70u /* F icon (9 px), as FoxHunt / FM / Beacon */
|
||||
#define SCROLL_X 72u /* scroll marks (5 px), centred on the F icon that takes
|
||||
their place while armed; not while transmitting:
|
||||
the capsule ends at x = 72 */
|
||||
/* editor cursor stops: 0-5 the latitude digits, N/S, 7-13 the longitude
|
||||
* digits, E/W, SSID, path, symbol */
|
||||
#define CUR_NS LAT_DIGITS
|
||||
#define CUR_EW (POS_DIGITS + 1u)
|
||||
#define CUR_SSID (CUR_EW + 1u)
|
||||
#define CUR_PATH (CUR_EW + 2u)
|
||||
#define CUR_SYM (CUR_EW + 3u)
|
||||
#define FIELD_COL 5u /* "SSID " / "PATH ": the value's column */
|
||||
#define CFG_MAGIC 0xA8u /* 0xA7 (v0.1: level and twist only) is ignored */
|
||||
#define CFG_LEN 13u /* magic, level, twist, 13 digits + hemispheres in nibbles,
|
||||
then 0x80 | path << 4 | SSID (v0.2 configs: defaults),
|
||||
the view, then the symbol index (default if erased) */
|
||||
#define LOOP_MS 40u
|
||||
#define REFRESH_LOOPS 125u /* a redraw every 5 s without a key (battery) */
|
||||
|
||||
enum { ST_IDLE = 0, ST_TX, ST_DENIED, ST_NOCALL, ST_BADPOS };
|
||||
|
||||
/* The app state in one struct: a function loads one base address instead of one
|
||||
* literal-pool word per global. Bytes, then the halfword, then words, so every
|
||||
* field stays within the Thumb load/store immediate ranges (ldrb <= 31, ldrh <= 62,
|
||||
* ldr <= 124); the arrays, addressed from their base, go last. The loader zeroes
|
||||
* the overlay (.bss) at every launch. */
|
||||
static struct {
|
||||
uint8_t flen, prevKey, space, ones, lvl, status;
|
||||
int8_t tw;
|
||||
bool running, edit, redraw; /* redraw: draw() at the next loop */
|
||||
bool fArm; /* editor: F pressed, the next key goes back */
|
||||
uint8_t hemi, ehemi, cur; /* hemispheres, edited ones, editor cursor */
|
||||
uint8_t ssid, essid, path, epath; /* source SSID, path, and the edited ones */
|
||||
uint8_t sym, esym; /* transmitted symbol and edited index */
|
||||
uint8_t tick; /* loops since the last draw() */
|
||||
uint8_t top, lim, vrow; /* scroll (px) and its limit, row being drawn */
|
||||
uint8_t cw; /* view: 0 scroll, CW_COMPACT compact */
|
||||
uint16_t sent; /* frames sent */
|
||||
const app_api_t *A;
|
||||
uint8_t *frm; /* the frame (app_main's stack) */
|
||||
uint32_t tPrev, tCyc, next; /* SysTick cycle counter, next bit edge */
|
||||
uint8_t pos[POS_DIGITS]; /* the position sent */
|
||||
uint8_t ed[POS_DIGITS]; /* the position being edited */
|
||||
} g;
|
||||
static char str[34];
|
||||
|
||||
/* ---- tiny freestanding helpers: GCC may turn small copies into calls ---- */
|
||||
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;
|
||||
}
|
||||
|
||||
/* ---- formatting ---- */
|
||||
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
|
||||
/* Formatting by repeated subtraction: no division. Values printed stay below 100000. */
|
||||
static uint8_t sub(uint32_t *v,uint32_t d){ uint8_t q=0; while(*v>=d){ *v-=d; q++; } return q; }
|
||||
static const uint16_t P10[]={10000u,1000u,100u,10u,1u};
|
||||
static char *puti(char *o,int32_t v){
|
||||
uint32_t u;
|
||||
if(v<0){*o++='-';u=(uint32_t)(-v);} else u=(uint32_t)v;
|
||||
bool lead=false;
|
||||
for(uint8_t i=0;i<5;i++){
|
||||
uint8_t c=sub(&u,P10[i]);
|
||||
if(c||lead||i==4u){ *o++=(char)('0'+c); lead=true; }
|
||||
}
|
||||
return o;
|
||||
}
|
||||
/* A tiny row holds 32 characters (4 px each): print_tiny does not clip, so
|
||||
* every row built below stays within that. */
|
||||
static void tiny(uint8_t y,char *end){ *end='\0'; g.A->print_tiny(str,0,y,false,true); }
|
||||
|
||||
/* The frame is shown above the separator (y = 31), in one of two views (key *),
|
||||
* as in APRS RX, with the source fixed on line 0 and the body scrolling below:
|
||||
* - scroll (g.cw = 0): rows of 18 characters in the small font (7 px each),
|
||||
* 8 px apart (glyphs 7 px, bit 7 blank), scrolled by g.top pixels: the source
|
||||
* and 3 body rows show at g.top = 0;
|
||||
* - compact (g.cw = CW_COMPACT): the source in the small font, then rows of 32
|
||||
* tiny characters (4 px each), 6 px apart from y = 8, the 4th ending at
|
||||
* y = 30; no scroll, the rest of the frame is not shown. */
|
||||
#define ROW_CHARS 18u
|
||||
#define CW_COMPACT 14u /* compact rows: ROW_CHARS + 14 = 32 characters */
|
||||
|
||||
/* One row str..end of the frame, wrapped every ROW_CHARS + g.cw (neither font
|
||||
* clips). Scroll view: row r goes to line r - top/8 when that is 0..4: line 4
|
||||
* (under the separator, redrawn after) only feeds the pixel shift of draw().
|
||||
* Row 0, the source, is drawn in bold (same 7 px glyphs) on line 0 in both
|
||||
* views. In scroll view draw() redraws it after the pixel shift; compact rows
|
||||
* 1-4 use the tiny font and the next ones are dropped. */
|
||||
static void emit(char *end){
|
||||
const app_api_t *A=g.A;
|
||||
char *p=str;
|
||||
while(p<end){
|
||||
uint8_t w=(uint8_t)(ROW_CHARS+g.cw); /* compare lengths: p + w may lie past str */
|
||||
char *q=(end-p>w)?p+w:end;
|
||||
char c=*q; *q='\0';
|
||||
uint8_t r=g.vrow, ln=(uint8_t)(r-(g.top>>3)); /* top = 0 when compact */
|
||||
if(g.cw && r){ if(r<5u) A->print_tiny(p,0,(uint8_t)(r*6u+2u),false,true); }
|
||||
else if(ln<5u) (r?A->print_normal:A->print_bold)(p,0,0,ln);
|
||||
*q=c;
|
||||
g.vrow=(uint8_t)(r+1u);
|
||||
p=q;
|
||||
}
|
||||
}
|
||||
|
||||
/* Between two parts of a row ("48 50.90N" / "002 16.25E"): a new row in the
|
||||
* scroll view, a space in the compact one. */
|
||||
static char *brk(char *o){
|
||||
if(g.cw){ *o++=' '; return o; }
|
||||
emit(o);
|
||||
return str;
|
||||
}
|
||||
/* The frequency (10 Hz units) drawn as the main screen draws it (ui/main.c,
|
||||
* ENABLE_BIG_FREQ): the big digits up to the kHz on lines 4-5, the last two
|
||||
* digits in the small font on line 5 right after them (from 1 GHz, the whole
|
||||
* string in the big font). The group is aligned left to reserve the rightmost
|
||||
* 20 columns for the APRS symbol. Big digits are 13 px (drawn from +2), the
|
||||
* point 3 px. No division. */
|
||||
static void drawFreq(uint32_t f){
|
||||
const app_api_t *A=g.A;
|
||||
uint16_t m=0;
|
||||
while(f>=100000u){ f-=100000u; m++; }
|
||||
char *o=puti(str,m);
|
||||
*o++='.';
|
||||
for(uint8_t i=0;i<5u;i++) *o++=(char)('0'+sub(&f,P10[i]));
|
||||
*o='\0';
|
||||
uint8_t n=(uint8_t)(o-str), big=(uint8_t)(n-3u); /* digits before "00" */
|
||||
if(m>=1000u){ A->print_string(str,0,0,4,8); return; }
|
||||
A->print_normal(str+n-2u,(uint8_t)(big*13u+3u),0,5); /* "00" */
|
||||
str[n-2u]='\0';
|
||||
A->display_freq(str,0,4,false); /* "144.800" */
|
||||
}
|
||||
|
||||
#define SYM_X 108u
|
||||
_Static_assert(SYM_X+SYM_W<=128u,"APRS symbol must fit on screen");
|
||||
static void drawSymbol(uint8_t x,uint8_t sym){
|
||||
const app_api_t *A=g.A;
|
||||
uint16_t off=(uint16_t)(SYM_BITMAPS+(uint16_t)sym*SYM_BYTES);
|
||||
for(uint8_t page=0;page<3u;page++)
|
||||
A->asset_read((uint16_t)(off+page*SYM_W),A->fb[4u+page]+x,SYM_W);
|
||||
}
|
||||
/* AX.25 address field -> "F4HWN-7" */
|
||||
static char *putCall(char *o,const uint8_t *a){
|
||||
for(uint8_t i=0;i<6u;i++){ char c=(char)(a[i]>>1); if(c!=' ') *o++=c; }
|
||||
uint8_t ssid=(uint8_t)((a[6]>>1)&15u);
|
||||
if(ssid){ *o++='-'; o=puti(o,ssid); }
|
||||
return o;
|
||||
}
|
||||
|
||||
/* ---- position ---- */
|
||||
/* "4850.90N" (6 digits) or "00216.25E" (7 digits), as APRS sends it */
|
||||
static uint8_t *putCoord(uint8_t *o,const uint8_t *d,uint8_t n,char h){
|
||||
for(uint8_t i=0;i<n;i++){ if(i==n-2u) *o++='.'; *o++=(uint8_t)('0'+d[i]); }
|
||||
*o++=(uint8_t)h;
|
||||
return o;
|
||||
}
|
||||
/* degrees <= 90 / 180 (and nothing beyond), minutes < 60 */
|
||||
static bool posOk(const uint8_t *d){
|
||||
uint8_t la=(uint8_t)(d[0]*10u+d[1]), lam=(uint8_t)(d[2]*10u+d[3]);
|
||||
uint8_t lom=(uint8_t)(d[9]*10u+d[10]);
|
||||
uint16_t lo=(uint16_t)(d[6]*100u+d[7]*10u+d[8]);
|
||||
for(uint8_t i=0;i<POS_DIGITS;i++) if(d[i]>9u) return false;
|
||||
if(la>90u || lam>59u || lo>180u || lom>59u) return false;
|
||||
if(la==90u && (lam|d[4]|d[5])) return false;
|
||||
if(lo==180u && (lom|d[11]|d[12])) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---- the frame: dest, source (boot callsign + SSID), path, UI, info, FCS ---- */
|
||||
static void build(void){
|
||||
const app_api_t *A=g.A;
|
||||
uint8_t *f=g.frm;
|
||||
char call[12];
|
||||
A->boot_callsign(call,sizeof call);
|
||||
uint8_t n=0;
|
||||
while(call[n]){ if(call[n]=='/') return; n++; } /* AX.25: letters and digits */
|
||||
if(!n || n>CALL_MAX) return; /* flen stays 0: no TX */
|
||||
|
||||
A->asset_read(F_DEST,f,F_DEST_LEN);
|
||||
for(uint8_t i=0;i<6u;i++) f[7+i]=(uint8_t)((i<n?call[i]:' ')<<1);
|
||||
f[13]=(uint8_t)(0x60u|(g.ssid<<1)|(g.path?0u:1u)); /* end bit if DIRECT */
|
||||
uint8_t k=(uint8_t)(14u+g.path*7u); /* the first path WIDEs */
|
||||
if(g.path){ A->asset_read(F_WIDE,f+14,(uint16_t)(k-14u)); f[k-1]|=1u; }
|
||||
f[k++]=0x03; f[k++]=0xF0; /* UI frame, no layer 3 */
|
||||
uint8_t sym[2];
|
||||
A->asset_read((uint16_t)(SYM_CODES+g.sym*2u),sym,2);
|
||||
f[k++]='!'; /* position, no messaging */
|
||||
k=(uint8_t)(putCoord(f+k,g.pos,LAT_DIGITS,(g.hemi&1u)?'S':'N')-f);
|
||||
f[k++]=sym[0]; /* symbol table */
|
||||
k=(uint8_t)(putCoord(f+k,g.pos+LAT_DIGITS,POS_DIGITS-LAT_DIGITS,(g.hemi&2u)?'W':'E')-f);
|
||||
f[k++]=sym[1]; /* symbol code */
|
||||
A->asset_read(F_COMMENT,f+k,F_COMMENT_LEN); k+=F_COMMENT_LEN;
|
||||
uint16_t c=0xFFFFu; /* CRC-16/X.25 */
|
||||
for(uint8_t i=0;i<k;i++){
|
||||
c^=f[i];
|
||||
for(uint8_t b=0;b<8u;b++) c=(c&1u)?(uint16_t)((c>>1)^0x8408u):(uint16_t)(c>>1);
|
||||
}
|
||||
c^=0xFFFFu;
|
||||
f[k++]=(uint8_t)c; f[k++]=(uint8_t)(c>>8);
|
||||
g.flen=k;
|
||||
}
|
||||
|
||||
/* ---- SysTick cycle counter (call at least every 10 ms; compare with a signed
|
||||
* difference) ---- */
|
||||
static void clkStart(void){ g.tPrev=SYST_VAL; g.tCyc=0; }
|
||||
static uint32_t clkCyc(void){
|
||||
uint32_t v=SYST_VAL;
|
||||
g.tCyc += (v<=g.tPrev) ? g.tPrev-v : g.tPrev+(SYST_LOAD+1u-v);
|
||||
g.tPrev=v;
|
||||
return g.tCyc;
|
||||
}
|
||||
|
||||
/* ---- AFSK ---- */
|
||||
static void setTone(void){
|
||||
const app_api_t *A=g.A;
|
||||
uint32_t v=g.lvl;
|
||||
if(g.space){ v=(v*(uint32_t)(8+g.tw))>>3; if(v>LVL_MAX) v=LVL_MAX; }
|
||||
A->bk_write(REG_71,(uint16_t)(g.space?F_SPACE:F_MARK));
|
||||
A->bk_write(REG_70,(uint16_t)(0x8000u|(v<<8)));
|
||||
}
|
||||
/* one bit on the air: NRZI (a 0 changes the tone), from the next bit edge */
|
||||
static void sendBit(uint8_t b){
|
||||
while((int32_t)(clkCyc()-g.next)<0){}
|
||||
if(!b){ g.space^=1u; setTone(); }
|
||||
g.next+=CYC_PER_BIT;
|
||||
}
|
||||
/* one byte LSB first; inside the frame, a 0 is stuffed after five 1s */
|
||||
static void sendByte(uint8_t v,bool stuff){
|
||||
for(uint8_t i=0;i<8u;i++){
|
||||
uint8_t b=(uint8_t)(v&1u);
|
||||
v>>=1;
|
||||
sendBit(b);
|
||||
if(stuff){
|
||||
if(!b) g.ones=0;
|
||||
else if(++g.ones==5u){ sendBit(0); g.ones=0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* "LAT 48 50.90 N" on a normal-font line (7 px per character). The line's
|
||||
* cursor stops are its n digits then the hemisphere (cur = n); the one under cur
|
||||
* is redrawn in bold (glyphs overwrite their columns). A cursor off the line
|
||||
* (negative, or past the hemisphere) shows none. */
|
||||
static void editRow(const char *label,uint8_t line,const uint8_t *d,uint8_t n,char h,int8_t cur){
|
||||
char *o=put(str,label);
|
||||
uint8_t col=0;
|
||||
for(uint8_t i=0;i<n;i++){
|
||||
if(i==n-4u) *o++=' ';
|
||||
if(i==n-2u) *o++='.';
|
||||
if((int8_t)i==cur) col=(uint8_t)(o-str);
|
||||
*o++=(char)('0'+d[i]);
|
||||
}
|
||||
*o++=' ';
|
||||
if((int8_t)n==cur) col=(uint8_t)(o-str);
|
||||
*o++=h; *o='\0';
|
||||
g.A->print_normal(str,0,0,line);
|
||||
if((uint8_t)cur<=n){ str[0]=str[col]; str[1]='\0'; g.A->print_bold(str,(uint8_t)(col*7u),0,line); }
|
||||
}
|
||||
|
||||
/* A labelled value on a normal-font line, at x; the value is bold when selected. */
|
||||
static void fieldRow(const char *label,const char *val,uint8_t x,uint8_t line,bool sel){
|
||||
char *o=put(put(str,label),val);
|
||||
*o='\0';
|
||||
g.A->print_normal(str,x,0,line);
|
||||
if(sel) g.A->print_bold(val,(uint8_t)(x+FIELD_COL*7u),0,line);
|
||||
}
|
||||
|
||||
/* ---- display ---- */
|
||||
static void draw(void){
|
||||
/* The UI texts live in the assets: one read per frame into this word-aligned
|
||||
* stack block (gen_assets.py keeps every offset aligned). */
|
||||
char s[UI_SIZE] __attribute__((aligned(4)));
|
||||
const app_api_t *A=g.A;
|
||||
char *o;
|
||||
A->asset_read(0,s,UI_SIZE);
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
A->print_inverse(s+T_TITLE,2,0,true,true,(uint8_t)(2u+T_TITLE_CHARS*4u));
|
||||
if(g.status==ST_TX) /* a second capsule while on the air */
|
||||
A->print_inverse(s+T_TX,TX_CAPS_X,0,true,true,(uint8_t)(TX_CAPS_X+T_TX_CHARS*4u));
|
||||
if(g.fArm) /* editor: F armed, as FoxHunt / FM / Beacon */
|
||||
A->asset_read(BMP_F,A->status_line+F_X,BMP_F_LEN);
|
||||
A->draw_battery();
|
||||
|
||||
if(g.edit){
|
||||
int8_t c=(int8_t)g.cur; /* each line takes its own stops from it */
|
||||
editRow(s+T_LAT,0,g.ed,LAT_DIGITS,(g.ehemi&1u)?'S':'N',c);
|
||||
editRow(s+T_LON,1,g.ed+LAT_DIGITS,POS_DIGITS-LAT_DIGITS,(g.ehemi&2u)?'W':'E',
|
||||
(int8_t)(c-(int8_t)(CUR_NS+1u)));
|
||||
char v[3];
|
||||
*puti(v,g.essid)='\0';
|
||||
fieldRow(s+T_SSID,v,0,2,g.cur==CUR_SSID);
|
||||
fieldRow(s+T_PATH,s+T_PATHS+g.epath*T_PATHS_STRIDE,0,3,g.cur==CUR_PATH);
|
||||
A->asset_read((uint16_t)(SYM_CODES+g.esym*2u),v,2); v[2]='\0';
|
||||
fieldRow(s+T_SYM,v,0,4,g.cur==CUR_SYM);
|
||||
/* the icon stays on whatever the cursor: the help lines end at x = 100,
|
||||
* before SYM_X */
|
||||
drawSymbol(SYM_X,g.esym);
|
||||
tiny(40,put(str,s+T_HELP1));
|
||||
tiny(48,put(str,g.status==ST_BADPOS?s+T_BADPOS:s+T_HELP2));
|
||||
} else {
|
||||
g.vrow=0;
|
||||
if(g.flen){
|
||||
const uint8_t *f=g.frm;
|
||||
o=putCall(str,f+7);
|
||||
emit(o); /* source */
|
||||
/* ">APZK5,WIDE1-1," / "WIDE2-1": a row breaks after a comma when
|
||||
* the next call (up to 9 characters) might not fit */
|
||||
o=str; *o++='>'; o=putCall(o,f);
|
||||
uint8_t e=13; /* the last address byte */
|
||||
while(!(f[e]&1u)){
|
||||
e+=7; *o++=',';
|
||||
if(o>str+ROW_CHARS+g.cw-9u){ emit(o); o=str; }
|
||||
o=putCall(o,f+e-6);
|
||||
}
|
||||
emit(o);
|
||||
/* info "!DDMM.hhN/DDDMM.hhE[comment" (built here, always this
|
||||
* layout): "48 50.90N" / "002 16.25E" (brk) / "/[ " + the comment */
|
||||
const uint8_t *q=f+e+4u; /* after control, PID and '!' */
|
||||
uint8_t end=(uint8_t)(g.flen-2u);
|
||||
o=str;
|
||||
for(uint8_t k=0;k<18u;k++){
|
||||
if(k==8u){ o=brk(o); continue; } /* symbol table: later */
|
||||
if(k==2u||k==12u) *o++=' ';
|
||||
*o++=(char)q[k];
|
||||
}
|
||||
emit(o);
|
||||
o=str; *o++=(char)q[8]; *o++=(char)q[18]; *o++=' ';
|
||||
for(uint8_t i=(uint8_t)(q+19u-f);i<end;i++){
|
||||
*o++=(char)f[i];
|
||||
if(o==str+ROW_CHARS+g.cw){ emit(o); o=str; }
|
||||
}
|
||||
emit(o);
|
||||
}
|
||||
/* the scroll limit: the last row fully shown, its blank bit 7 on the
|
||||
* separator; 0 when compact or when the frame fits */
|
||||
uint8_t lim=0;
|
||||
if(!g.cw && g.vrow>4u) lim=(uint8_t)(g.vrow*8u-32u);
|
||||
g.lim=lim;
|
||||
/* an up mark while rows hide above, a down mark while rows hide below */
|
||||
uint8_t mk=0;
|
||||
if(g.top) mk=1u;
|
||||
if(g.top<lim) mk+=2u;
|
||||
if(mk && !g.fArm && g.status!=ST_TX)
|
||||
A->asset_read((uint16_t)(BMP_SCROLL+(mk-1u)*BMP_SCROLL_W),A->status_line+SCROLL_X,BMP_SCROLL_W);
|
||||
|
||||
/* Pixel scroll: lines 0-3 go up top%8 px, the bits below coming from
|
||||
* the next line (an 8-bit shift of the next line gives 0 at top%8 = 0).
|
||||
* Line 4 was only the source of line 3: cleared for the frequency. */
|
||||
uint8_t sh=g.top&7u, *b=A->fb[0];
|
||||
for(uint16_t k=0;k<512u;k++) b[k]=(uint8_t)((b[k]>>sh)|(b[k+128u]<<(8u-sh)));
|
||||
memset(b+512,0,128);
|
||||
|
||||
/* Keep the source fixed while the body scrolls underneath it, as APRS
|
||||
* RX does. Any body pixels shifted into line 0 are deliberately hidden. */
|
||||
if(g.flen){
|
||||
memset(b,0,128);
|
||||
*putCall(str,g.frm+7)='\0';
|
||||
A->print_bold(str,0,0,0);
|
||||
}
|
||||
|
||||
/* dotted separator above the frequency: y = 31, bit 7 of line 3, over
|
||||
* whatever the shift brought there (the big digits start at y = 33: one
|
||||
* blank px between) */
|
||||
for(uint8_t x=0;x<128u;x++) b[384u+x]=(uint8_t)((b[384u+x]&0x7Fu)|((~x&1u)<<7));
|
||||
drawFreq(A->tx_freq());
|
||||
drawSymbol(SYM_X,g.sym);
|
||||
|
||||
/* bottom line: "lvl 66 tw +0 sent 3", or why nothing was sent */
|
||||
if(g.status==ST_DENIED||g.status==ST_NOCALL)
|
||||
o=put(str,g.status==ST_DENIED?s+T_DENIED:s+T_NOCALL);
|
||||
else {
|
||||
o=put(str,s+T_LVL); o=puti(o,g.lvl);
|
||||
o=put(o,s+T_TW); if(g.tw>0) *o++='+'; o=puti(o,g.tw);
|
||||
o=put(o,s+T_SENT); o=puti(o,g.sent);
|
||||
}
|
||||
tiny(49,o);
|
||||
}
|
||||
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
}
|
||||
|
||||
/* ---- one frame on the air ---- */
|
||||
static void send(void){
|
||||
const app_api_t *A=g.A;
|
||||
if(!g.flen){ g.status=ST_NOCALL; return; }
|
||||
if(A->tx_state()){ g.status=ST_DENIED; return; }
|
||||
g.status=ST_TX;
|
||||
draw();
|
||||
|
||||
A->tx_set_params(); /* carrier + PA on the TX VFO */
|
||||
A->bk_write(REG_51,0); /* no CTCSS/DCS under the AFSK */
|
||||
A->tx_tone(1200); /* tone path on, mic off, 50 ms settle */
|
||||
g.space=0;
|
||||
setTone(); /* mark at our level */
|
||||
clkStart();
|
||||
g.next=CYC_PER_BIT;
|
||||
for(uint8_t i=0;i<TXDELAY_FLAGS;i++) sendByte(0x7E,false);
|
||||
g.ones=0;
|
||||
for(uint8_t i=0;i<g.flen;i++) sendByte(g.frm[i],true);
|
||||
for(uint8_t i=0;i<TAIL_FLAGS;i++) sendByte(0x7E,false);
|
||||
while((int32_t)(clkCyc()-g.next)<0){} /* the last bit plays out */
|
||||
A->tx_mute(true);
|
||||
A->tx_end(); /* PA off + RX restored */
|
||||
|
||||
if(g.sent<9999u) g.sent++;
|
||||
g.status=ST_IDLE;
|
||||
}
|
||||
|
||||
/* ---- input ---- */
|
||||
static void handleKeys(void){
|
||||
const app_api_t *A=g.A;
|
||||
uint8_t key=A->get_key();
|
||||
/* UP/DOWN, held, out of the editor: scroll 1 px per loop (25 px/s), from
|
||||
* the source row to the last row fully shown (its blank bit 7 on the
|
||||
* separator) */
|
||||
if(!g.edit){
|
||||
int8_t d=A->nav_dir(key); /* g.lim: draw(), 0 when compact */
|
||||
if((d<0 && g.top) || (d>0 && g.top<g.lim)){ g.top=(uint8_t)(g.top+d); g.redraw=true; }
|
||||
}
|
||||
if(key==APP_KEY_INVALID||key==g.prevKey){ g.prevKey=key; return; }
|
||||
g.prevKey=key;
|
||||
g.redraw=true; /* a key changes the screen, a send too */
|
||||
A->backlight_on();
|
||||
if(key==APP_KEY_F){ g.fArm=!g.fArm; return; }
|
||||
bool back=g.fArm; /* F then a key: it goes backwards (down) */
|
||||
g.fArm=false;
|
||||
if(g.edit){ /* position, SSID, path and symbol editor */
|
||||
g.status=ST_IDLE;
|
||||
uint8_t cur=g.cur;
|
||||
bool lon=cur>CUR_NS && cur<=CUR_EW; /* on the longitude line */
|
||||
if(key<=APP_KEY_9){
|
||||
if(cur<CUR_NS || (lon && cur<CUR_EW)){ /* a digit: its stop is one past it on LON */
|
||||
g.ed[cur-lon]=key;
|
||||
if(++cur==CUR_NS) cur++; /* typing goes on past N/S ... */
|
||||
if(cur==CUR_EW) cur--; /* ... and stays on the last digit */
|
||||
g.cur=cur;
|
||||
}
|
||||
}
|
||||
else if(key==APP_KEY_STAR){
|
||||
if(cur<=CUR_EW) g.ehemi^=(uint8_t)(lon?2u:1u); /* on a digit or the letter */
|
||||
else if(cur==CUR_SSID) g.essid=(uint8_t)((g.essid+(back?15u:1u))&15u);
|
||||
else if(cur==CUR_PATH){
|
||||
if(back) g.epath=(uint8_t)(g.epath?g.epath-1u:PATH_MAX);
|
||||
else g.epath=(uint8_t)(g.epath<PATH_MAX?g.epath+1u:0u);
|
||||
} else if(back) g.esym=(uint8_t)(g.esym?g.esym-1u:SYM_COUNT-1u);
|
||||
else g.esym=(uint8_t)(g.esym+1u<SYM_COUNT?g.esym+1u:0u);
|
||||
}
|
||||
else if(key==APP_KEY_MENU){
|
||||
if(!posOk(g.ed)){ g.status=ST_BADPOS; return; }
|
||||
memcpy(g.pos,g.ed,POS_DIGITS);
|
||||
g.hemi=g.ehemi; g.ssid=g.essid; g.path=g.epath; g.sym=g.esym;
|
||||
g.edit=false;
|
||||
build();
|
||||
if(!g.flen) g.status=ST_NOCALL;
|
||||
}
|
||||
else if(key==APP_KEY_EXIT) g.edit=false;
|
||||
else {
|
||||
int8_t d=A->nav_dir(key);
|
||||
if(d<0 && g.cur) g.cur--;
|
||||
if(d>0 && g.cur<CUR_SYM) g.cur++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(key==APP_KEY_EXIT) g.running=false;
|
||||
else if(key==APP_KEY_MENU||key==APP_KEY_PTT) send();
|
||||
else if(key==APP_KEY_1){
|
||||
if(back){ if(g.lvl>=LVL_MIN+LVL_STEP) g.lvl=(uint8_t)(g.lvl-LVL_STEP); }
|
||||
else if(g.lvl<=LVL_MAX-LVL_STEP) g.lvl=(uint8_t)(g.lvl+LVL_STEP);
|
||||
}
|
||||
else if(key==APP_KEY_2){
|
||||
if(back){ if(g.tw>TW_MIN) g.tw--; }
|
||||
else if(g.tw<TW_MAX) g.tw++;
|
||||
}
|
||||
else if(key==APP_KEY_STAR){ g.cw^=CW_COMPACT; g.top=0; } /* the view */
|
||||
else if(key==APP_KEY_3){
|
||||
memcpy(g.ed,g.pos,POS_DIGITS);
|
||||
g.ehemi=g.hemi; g.essid=g.ssid; g.epath=g.path; g.esym=g.sym;
|
||||
g.cur=0;
|
||||
g.top=0; /* the frame may change: back to its top */
|
||||
g.edit=true;
|
||||
if(g.status!=ST_NOCALL) g.status=ST_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((section(".text.entry"),used))
|
||||
void app_main(const app_api_t *api){
|
||||
uint8_t frm[FRAME_MAX]; /* on the stack: the 4 KiB overlay also holds .bss */
|
||||
uint8_t c[CFG_LEN];
|
||||
g.A=api;
|
||||
g.frm=frm;
|
||||
g.prevKey=APP_KEY_INVALID; /* the rest of g starts at 0 (overlay zeroed by the loader) */
|
||||
g.lvl=LVL_DEF;
|
||||
g.ssid=CFG_SSID;
|
||||
g.path=CFG_PATH;
|
||||
g.sym=SYM_INDEX;
|
||||
api->asset_read(POS_DEF,g.pos,POS_DIGITS);
|
||||
api->asset_read(POS_DEF+POS_DIGITS,&g.hemi,1);
|
||||
api->cfg_load(c,CFG_LEN);
|
||||
if(c[0]==CFG_MAGIC){
|
||||
if(c[1]>=LVL_MIN && c[1]<=LVL_MAX) g.lvl=c[1];
|
||||
if((int8_t)c[2]>=TW_MIN && (int8_t)c[2]<=TW_MAX) g.tw=(int8_t)c[2];
|
||||
for(uint8_t i=0;i<POS_DIGITS;i++) g.ed[i]=(uint8_t)((c[3+i/2u]>>((i&1u)*4u))&15u);
|
||||
uint8_t h=(uint8_t)(c[9]>>4);
|
||||
if(posOk(g.ed) && h<=3u){ memcpy(g.pos,g.ed,POS_DIGITS); g.hemi=h; }
|
||||
h=c[10]; /* 0x80 | path << 4 | SSID; erased (0xFF) or 0: defaults */
|
||||
if((h&0xC0u)==0x80u && ((h>>4)&3u)<=PATH_MAX){ g.ssid=(uint8_t)(h&15u); g.path=(uint8_t)((h>>4)&3u); }
|
||||
if(c[11]==CW_COMPACT) g.cw=CW_COMPACT;
|
||||
if(c[12]<SYM_COUNT) g.sym=c[12];
|
||||
}
|
||||
build();
|
||||
if(!g.flen) g.status=ST_NOCALL;
|
||||
api->backlight_on();
|
||||
|
||||
g.running=true;
|
||||
g.redraw=true;
|
||||
while(g.running){
|
||||
handleKeys();
|
||||
if(!g.running) break;
|
||||
/* The screen only changes on a key (or a send it starts): redraw then,
|
||||
* and every 5 s for the battery, not at each loop. The state stays in g:
|
||||
* locals here would live on the stack across the inlined calls. */
|
||||
if(++g.tick>=REFRESH_LOOPS) g.redraw=true;
|
||||
if(g.redraw){ g.redraw=false; g.tick=0; draw(); }
|
||||
api->battery_sample();
|
||||
api->delay_ms(LOOP_MS);
|
||||
api->backlight_update(); /* normal BLTime timeout; keys re-arm it */
|
||||
}
|
||||
|
||||
c[0]=CFG_MAGIC; c[1]=g.lvl; c[2]=(uint8_t)g.tw;
|
||||
for(uint8_t i=3;i<CFG_LEN;i++) c[i]=0;
|
||||
for(uint8_t i=0;i<POS_DIGITS;i++) c[3+i/2u]|=(uint8_t)(g.pos[i]<<((i&1u)*4u));
|
||||
c[9]|=(uint8_t)(g.hemi<<4); /* the 14th nibble */
|
||||
c[10]=(uint8_t)(0x80u|(g.path<<4)|g.ssid);
|
||||
c[11]=g.cw;
|
||||
c[12]=g.sym;
|
||||
api->cfg_save(c,CFG_LEN);
|
||||
}
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/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="APRS TX" # <-- the only per-app line
|
||||
APP_VER="0.5"
|
||||
APP_API_MIN=2
|
||||
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 -fno-jump-tables -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/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 assets ; APP_VER="$APP_VER" python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
|
||||
--shortcut none --assets "${APP}_assets.bin" >/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"
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
# APRS TX read-only assets: the screen texts and the fixed part of the frame.
|
||||
#
|
||||
# The UI texts come first, each padded to a multiple of 4 bytes: draw() reads
|
||||
# them in one asset_read into a word-aligned stack block, so every text costs a
|
||||
# 2-byte sp-relative add instead of a literal-pool load and a pool word.
|
||||
# The frame parts are AX.25-encoded here (addresses shifted left, SSID bytes):
|
||||
# the app adds the source (boot-message callsign + SSID), the path (the first 0,
|
||||
# 1 or 2 entries of WIDE, end-of-address bit set by the app), the position and
|
||||
# the FCS. SSID, PATH and the position are the defaults, until they are edited
|
||||
# on the radio. Edit the station settings below, then rebuild;
|
||||
# test/tx_model.py checks the resulting frame.
|
||||
#
|
||||
# ./gen_assets.py aprstx_assets.bin aprstx_assets.h
|
||||
import os, re, sys
|
||||
sys.dont_write_bytecode = True
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.join(HERE, ".."))
|
||||
from app_assets import Assets
|
||||
from aprs_symbols_20 import BITMAPS, BYTES_PER_ICON, CODES, WIDTH
|
||||
|
||||
# ---- station settings ----
|
||||
SSID = 7 # F4HWN-7 (0-15, 0 = no SSID)
|
||||
DEST = "APZK5" # APZ = experimental software
|
||||
PATH = 1 # index in PATHS below
|
||||
WIDE = ["WIDE1-1", "WIDE2-1"] # path n = the first n entries
|
||||
PATHS = ["DIRECT", "WIDE1-1", "WIDE1-1,2-1"] # shown on the radio, per path
|
||||
LAT, LON = "4850.90N", "00216.25E" # default position: DDMM.hhN, DDDMM.hhE
|
||||
SYMBOL = "/[" # table, code: '[' = person
|
||||
COMMENT = "UV-K5/K1 F4HWN Firmware"
|
||||
# info field: "!" + LAT + SYMBOL[0] + LON + SYMBOL[1] + COMMENT
|
||||
|
||||
TITLE = "APRS TX"
|
||||
TX_CAPS = "TRANSMIT" # status-bar capsule while on the air
|
||||
|
||||
UI = [
|
||||
("T_TITLE", TITLE),
|
||||
("T_TX", TX_CAPS),
|
||||
("T_DENIED", "TX denied"),
|
||||
("T_NOCALL", "No boot callsign"),
|
||||
("T_LVL", "lvl "),
|
||||
("T_TW", " tw "),
|
||||
("T_SENT", " sent "),
|
||||
("T_LAT", "LAT "), # 2 spaces: digits aligned with LON
|
||||
("T_LON", "LON "),
|
||||
("T_SSID", "SSID "), # FIELD_COL (5) characters: the value
|
||||
("T_PATH", "PATH "), # starts at the same column
|
||||
("T_SYM", "SYMB "), # selected APRS table/code pair
|
||||
("T_HELP1", "0-9, * change, F+* back"), # * on the bold field
|
||||
("T_HELP2", "UP/DN move MENU ok EXIT"),
|
||||
("T_BADPOS", "Invalid position"),
|
||||
]
|
||||
|
||||
|
||||
def addr(call, last=False):
|
||||
call, _, ssid = call.upper().partition("-")
|
||||
if not (1 <= len(call) <= 6 and call.isalnum()):
|
||||
sys.exit(f"bad AX.25 address {call!r}")
|
||||
b = bytes(ord(c) << 1 for c in call.ljust(6))
|
||||
return b + bytes([0x60 | (int(ssid or 0) << 1) | (1 if last else 0)])
|
||||
|
||||
|
||||
if not 0 <= SSID <= 15:
|
||||
sys.exit("SSID must be 0-15")
|
||||
if len(WIDE) != 2 or len(PATHS) != len(WIDE) + 1 or not 0 <= PATH < len(PATHS):
|
||||
sys.exit("PATHS lists DIRECT then 1 and 2 WIDE entries (frame buffer); PATH indexes it")
|
||||
if any(len(s) > 18 - 5 for s in PATHS):
|
||||
sys.exit("a PATHS name does not fit the line after 'PATH '")
|
||||
if not (re.fullmatch(r"\d{4}\.\d{2}[NS]", LAT) and re.fullmatch(r"\d{5}\.\d{2}[EW]", LON)):
|
||||
sys.exit("LAT must be DDMM.hhN/S, LON DDDMM.hhE/W")
|
||||
if len(SYMBOL) != 2 or len(COMMENT) > 43:
|
||||
sys.exit("SYMBOL is 2 characters, COMMENT at most 43 (frame buffer)")
|
||||
# 13 digits (DDMMhh DDDMMhh), then the hemispheres: bit 0 south, bit 1 west
|
||||
POS = [int(c) for c in LAT + LON if c.isdigit()]
|
||||
HEMI = (LAT[-1] == "S") | (LON[-1] == "W") << 1
|
||||
|
||||
a = Assets("APRSTX")
|
||||
ui_size = 0
|
||||
for name, s in UI:
|
||||
pad = -(len(s) + 1) % 4 # keep every offset word-aligned
|
||||
a.text(name, s + "\0" * pad)
|
||||
ui_size += len(s) + 1 + pad
|
||||
PATHS_STRIDE = -(-(max(len(s) for s in PATHS) + 1) // 4) * 4 # word-aligned entries
|
||||
a.table("T_PATHS", PATHS, stride=PATHS_STRIDE)
|
||||
ui_size += len(PATHS) * PATHS_STRIDE
|
||||
a.const("UI_SIZE", ui_size) # the UI block read by draw()
|
||||
a.const("T_TITLE_CHARS", len(TITLE))
|
||||
a.const("T_TX_CHARS", len(TX_CAPS))
|
||||
a.const("CFG_SSID", SSID)
|
||||
a.const("CFG_PATH", PATH)
|
||||
a.raw("F_DEST", addr(DEST))
|
||||
a.raw("F_WIDE", b"".join(addr(h) for h in WIDE))
|
||||
a.u8("POS_DEF", POS + [HEMI])
|
||||
a.raw("F_COMMENT", COMMENT.encode("ascii"))
|
||||
a.raw("SYM_CODES", "".join(CODES).encode("ascii"))
|
||||
a.raw("SYM_BITMAPS", BITMAPS)
|
||||
a.const("SYM_INDEX", CODES.index(SYMBOL))
|
||||
a.const("SYM_COUNT", len(CODES))
|
||||
a.const("SYM_W", WIDTH)
|
||||
a.const("SYM_BYTES", BYTES_PER_ICON)
|
||||
a.u8("BMP_F", [0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e]) # F armed, as FoxHunt / FM / Beacon
|
||||
# scroll marks (status bar, x = 75, as APRS RX): up only, down only, both; up
|
||||
# in bits 0-2 (rows hidden above), down in bits 4-6 (rows hidden below)
|
||||
UP, DN = [0x04, 0x06, 0x07, 0x06, 0x04], [0x10, 0x30, 0x70, 0x30, 0x10]
|
||||
a.u8("BMP_SCROLL", UP + DN + [u | d for u, d in zip(UP, DN)])
|
||||
a.const("BMP_SCROLL_W", 5)
|
||||
a.main()
|
||||
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Model of the APRS TX app: the frame and bit stream of aprstx_app.c rebuilt
|
||||
from the real assets (gen_assets.py), checked against the AX.25 reference of
|
||||
the RX app (ax25.py), then sent as AFSK and decoded by the APRS RX model
|
||||
(model_rx.py) through a receiver audio path.
|
||||
|
||||
The tone generator is modelled with a phase-continuous NCO (expected: REG_71 is
|
||||
rewritten, not the phase) and, as the worst case, with a phase reset at each
|
||||
tone change. The TX tone path is assumed flat (no pre-emphasis): a receiver with
|
||||
de-emphasis (STD path, FT3D) then sees 2200 Hz ~5 dB low, which the app's twist
|
||||
setting (2200 Hz gain x (8 + tw) / 8) compensates.
|
||||
|
||||
tx_model.py runs the checks and the decode sweep
|
||||
"""
|
||||
import math, os, re, subprocess, sys, tempfile
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.join(HERE, "..", "..", "aprsrx", "test"))
|
||||
from ax25 import build, hdlc_bits, crc16, decode
|
||||
import model_rx as M
|
||||
|
||||
CALL = "F4HWN" # boot message
|
||||
TXDELAY_FLAGS, TAIL_FLAGS = 40, 3
|
||||
LEAD_S = 0.050 + 1 / 1200 # tx_tone's 50 ms settle, then the first bit edge
|
||||
DEV_MARK = 3000.0 # Hz of deviation for the mark tone at the chosen level
|
||||
|
||||
|
||||
def assets():
|
||||
d = tempfile.mkdtemp()
|
||||
b, h = os.path.join(d, "a.bin"), os.path.join(d, "a.h")
|
||||
subprocess.run([sys.executable, os.path.join(HERE, "..", "gen_assets.py"), b, h], check=True)
|
||||
defs = dict(re.findall(r"#define (\w+) (\d+)u", open(h).read()))
|
||||
return open(b, "rb").read(), {k: int(v) for k, v in defs.items()}
|
||||
|
||||
|
||||
def put_coord(d, h):
|
||||
"""aprstx_app.c putCoord(): 4850.90N / 00216.25E"""
|
||||
n = len(d)
|
||||
return "".join(("." if i == n - 2 else "") + str(x) for i, x in enumerate(d)) + h
|
||||
|
||||
|
||||
def pos_ok(d):
|
||||
"""aprstx_app.c posOk()"""
|
||||
if any(x > 9 for x in d): return False
|
||||
la, lam, lo, lom = d[0] * 10 + d[1], d[2] * 10 + d[3], d[6] * 100 + d[7] * 10 + d[8], d[9] * 10 + d[10]
|
||||
if la > 90 or lam > 59 or lo > 180 or lom > 59: return False
|
||||
if la == 90 and (lam | d[4] | d[5]): return False
|
||||
if lo == 180 and (lom | d[11] | d[12]): return False
|
||||
return True
|
||||
|
||||
|
||||
CW_COMPACT = 14 # aprstx_app.c: the compact view
|
||||
|
||||
|
||||
def cfg_pack(lvl, tw, pos, hemi, ssid, path, cw=0, sym=5):
|
||||
c = [0xA8, lvl, tw & 0xFF] + [0] * 10
|
||||
for i, x in enumerate(pos): c[3 + i // 2] |= x << ((i & 1) * 4)
|
||||
c[9] |= hemi << 4
|
||||
c[10] = 0x80 | path << 4 | ssid
|
||||
c[11] = cw
|
||||
c[12] = sym
|
||||
return bytes(c)
|
||||
|
||||
|
||||
def cfg_unpack(c, D):
|
||||
"""aprstx_app.c app_main(): position, hemispheres, SSID, path (the
|
||||
defaults when byte 10 is erased or from a v0.2 config), view (scroll unless
|
||||
byte 11 is CW_COMPACT) and symbol (default when byte 12 is erased)"""
|
||||
pos = [(c[3 + i // 2] >> ((i & 1) * 4)) & 15 for i in range(13)]
|
||||
cw = CW_COMPACT if c[11] == CW_COMPACT else 0
|
||||
h = c[10]
|
||||
if (h & 0xC0) == 0x80 and (h >> 4) & 3 <= 2:
|
||||
return pos, c[9] >> 4, h & 15, (h >> 4) & 3, cw, c[12] if c[12] < D["SYM_COUNT"] else D["SYM_INDEX"]
|
||||
return pos, c[9] >> 4, D["CFG_SSID"], D["CFG_PATH"], cw, c[12] if c[12] < D["SYM_COUNT"] else D["SYM_INDEX"]
|
||||
|
||||
|
||||
def edit_row(label, d, h, cur):
|
||||
"""aprstx_app.c editRow(): the text and the bold column (stops: the n
|
||||
digits, then the hemisphere at cur = n)"""
|
||||
o, col, n = label, None, len(d)
|
||||
for i, x in enumerate(d):
|
||||
if i == n - 4: o += " "
|
||||
if i == n - 2: o += "."
|
||||
if i == cur: col = len(o)
|
||||
o += str(x)
|
||||
o += " "
|
||||
if cur == n: col = len(o)
|
||||
return o + h, col
|
||||
|
||||
|
||||
CUR_NS, CUR_EW, CUR_SSID, CUR_PATH, CUR_SYM = 6, 14, 15, 16, 17
|
||||
|
||||
|
||||
def nav_dir(key, set_nav):
|
||||
"""app_overlay.c app_nav_dir(): the raw UP ('U') / DOWN ('D') key as a value
|
||||
direction, +1 / -1 with SET_NAV, -1 / +1 without; 0 for any other key"""
|
||||
d = 1 if key == "U" else -1 if key == "D" else 0
|
||||
return d if set_nav else -d
|
||||
|
||||
|
||||
def editor_keys(keys, ed, hemi=0, ssid=7, path=1, sym=5, cur=0, set_nav=True):
|
||||
"""aprstx_app.c handleKeys() in the editor: digits 0-9, '*', raw UP 'U' /
|
||||
DOWN 'D' through nav_dir(), 'F'"""
|
||||
ed = list(ed)
|
||||
farm = False
|
||||
for k in keys:
|
||||
if k == "F":
|
||||
farm = not farm
|
||||
continue
|
||||
back, farm = farm, False
|
||||
lon = CUR_NS < cur <= CUR_EW
|
||||
if k.isdigit():
|
||||
if cur < CUR_NS or (lon and cur < CUR_EW):
|
||||
ed[cur - lon] = int(k)
|
||||
cur += 1
|
||||
if cur == CUR_NS: cur += 1
|
||||
if cur == CUR_EW: cur -= 1
|
||||
elif k == "*":
|
||||
if cur <= CUR_EW: hemi ^= 2 if lon else 1
|
||||
elif cur == CUR_SSID: ssid = (ssid + (15 if back else 1)) & 15
|
||||
elif cur == CUR_PATH:
|
||||
path = path - 1 if back and path else 2 if back else path + 1 if path < 2 else 0
|
||||
elif back: sym = sym - 1 if sym else 47
|
||||
else: sym = sym + 1 if sym < 47 else 0
|
||||
else:
|
||||
d = nav_dir(k, set_nav)
|
||||
if d < 0 and cur: cur -= 1
|
||||
if d > 0 and cur < CUR_SYM: cur += 1
|
||||
return ed, hemi, ssid, path, sym, cur
|
||||
|
||||
|
||||
def build_c(blob, D, call=CALL, pos=None, hemi=None, ssid=None, path=None, sym=None):
|
||||
"""aprstx_app.c build()"""
|
||||
if not call or len(call) > 6 or "/" in call:
|
||||
return None
|
||||
rd = lambda name: blob[D[name]:D[name] + D[name + "_LEN"]]
|
||||
pd = rd("POS_DEF")
|
||||
pos = list(pd[:13]) if pos is None else pos
|
||||
hemi = pd[13] if hemi is None else hemi
|
||||
ssid = D["CFG_SSID"] if ssid is None else ssid
|
||||
path = D["CFG_PATH"] if path is None else path
|
||||
sym = D["SYM_INDEX"] if sym is None else sym
|
||||
pair = rd("SYM_CODES")[sym * 2:sym * 2 + 2].decode()
|
||||
info = "!" + put_coord(pos[:6], "S" if hemi & 1 else "N") + pair[0] + \
|
||||
put_coord(pos[6:], "W" if hemi & 2 else "E") + pair[1] + rd("F_COMMENT").decode()
|
||||
f = bytearray(rd("F_DEST"))
|
||||
f += bytes(ord(call[i]) << 1 if i < len(call) else 0x40 for i in range(6))
|
||||
f.append(0x60 | (ssid << 1) | (0 if path else 1))
|
||||
p = bytearray(rd("F_WIDE")[:7 * path]) # the first path WIDE entries
|
||||
if path: p[-1] |= 1 # end of the address field
|
||||
f += p + b"\x03\xF0" + info.encode()
|
||||
c = 0xFFFF
|
||||
for x in f:
|
||||
c ^= x
|
||||
for _ in range(8):
|
||||
c = (c >> 1) ^ 0x8408 if c & 1 else c >> 1
|
||||
c ^= 0xFFFF
|
||||
return bytes(f + bytes([c & 0xFF, c >> 8]))
|
||||
|
||||
|
||||
def bits_c(frame):
|
||||
"""aprstx_app.c sendByte()/sendBit(): the bits on the air, before NRZI"""
|
||||
out, ones = [], 0
|
||||
def byte(v, stuff):
|
||||
nonlocal ones
|
||||
for _ in range(8):
|
||||
b = v & 1; v >>= 1
|
||||
out.append(b)
|
||||
if stuff:
|
||||
if not b: ones = 0
|
||||
else:
|
||||
ones += 1
|
||||
if ones == 5: out.append(0); ones = 0
|
||||
for _ in range(TXDELAY_FLAGS): byte(0x7E, False)
|
||||
ones = 0
|
||||
for x in frame: byte(x, True)
|
||||
for _ in range(TAIL_FLAGS): byte(0x7E, False)
|
||||
return out
|
||||
|
||||
|
||||
def afsk(bits, tw=0, reset=False, twist_path_db=0.0):
|
||||
"""discriminator output (Hz) of the transmitted AFSK; tw as in the app,
|
||||
twist_path_db an extra 2200 Hz gain of the TX audio path (unknown)"""
|
||||
fs = M.FS_SIM
|
||||
a_sp = DEV_MARK * min(127, 66 * (8 + tw) // 8) / 66 * 10 ** (twist_path_db / 20)
|
||||
out, ph, space = [], 0.0, False
|
||||
for i in range(int(LEAD_S * fs)): # plain mark before the flags
|
||||
ph += 1200 / fs
|
||||
out.append(DEV_MARK * math.sin(2 * math.pi * ph))
|
||||
spb = fs / 1200
|
||||
t = 0.0
|
||||
for b in bits:
|
||||
if not b:
|
||||
space = not space
|
||||
if reset: ph = 0.0
|
||||
f, a = (2200, a_sp) if space else (1200, DEV_MARK)
|
||||
t += spb
|
||||
while len(out) < int(LEAD_S * fs + t):
|
||||
ph += f / fs
|
||||
out.append(a * math.sin(2 * math.pi * ph))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
blob, D = assets()
|
||||
ok = True
|
||||
f = build_c(blob, D)
|
||||
ssid = D["CFG_SSID"]
|
||||
WIDE = ["WIDE1-1", "WIDE2-1"] # gen_assets.py WIDE
|
||||
info = "!4850.90N/00216.25E[UV-K5/K1 F4HWN Firmware" # gen_assets.py defaults
|
||||
ref = build("%s-%d" % (CALL, ssid) if ssid else CALL, dst="APZK5",
|
||||
path=WIDE[:D["CFG_PATH"]], info=info)
|
||||
print("frame ", decode(f), "(%d bytes)" % len(f))
|
||||
ok &= f == ref
|
||||
print("frame == ax25.build:", f == ref)
|
||||
bc = bits_c(f)
|
||||
ok &= bc == hdlc_bits(f, TXDELAY_FLAGS, TAIL_FLAGS)
|
||||
print("bits == ax25.hdlc_bits:", bc == hdlc_bits(f, TXDELAY_FLAGS, TAIL_FLAGS),
|
||||
"(%d bits, %.0f ms + %.0f ms lead)" % (len(bc), len(bc) / 1.2, LEAD_S * 1000))
|
||||
for bad in ("", "F4HWN/P", "F4HWN73"):
|
||||
ok &= build_c(blob, D, bad) is None
|
||||
print("bad boot callsigns refused:", True)
|
||||
|
||||
# position editor: another position, south/west, the frame and the config
|
||||
pos = [3, 3, 5, 2, 1, 3, 1, 5, 1, 1, 2, 5, 6] # 33 52.13S 151 12.56W
|
||||
f2 = build_c(blob, D, pos=pos, hemi=3)
|
||||
want = build("F4HWN-7", dst="APZK5", path=["WIDE1-1"],
|
||||
info="!3352.13S/15112.56W[UV-K5/K1 F4HWN Firmware")
|
||||
ok &= f2 == want
|
||||
print("edited position frame:", f2 == want, decode(f2))
|
||||
# SSID and path edited: every path, SSID 0 (no suffix) and 15
|
||||
for sid, path in ((0, 0), (15, 1), (9, 2)):
|
||||
f3 = build_c(blob, D, ssid=sid, path=path)
|
||||
want = build("F4HWN-%d" % sid if sid else "F4HWN", dst="APZK5", path=WIDE[:path], info=info)
|
||||
ok &= f3 == want
|
||||
print("SSID %2d, path %d frame:" % (sid, path), f3 == want, decode(f3))
|
||||
f3 = build_c(blob, D, sym=0)
|
||||
want = build("F4HWN-7", dst="APZK5", path=["WIDE1-1"],
|
||||
info="!4850.90N/00216.25E#UV-K5/K1 F4HWN Firmware")
|
||||
ok &= f3 == want
|
||||
print("selected /# symbol frame:", f3 == want, decode(f3))
|
||||
c = cfg_pack(70, -2, pos, 3, 9, 2, CW_COMPACT)
|
||||
good = len(c) == 13 and cfg_unpack(c, D) == (pos, 3, 9, 2, CW_COMPACT, 5)
|
||||
old = cfg_pack(70, -2, pos, 3, 0, 0)[:10] # a v0.2 config: byte 10 erased or 0
|
||||
good &= all(cfg_unpack(old + bytes([b, 0xFF, 0xFF]), D) ==
|
||||
(pos, 3, D["CFG_SSID"], D["CFG_PATH"], 0, D["SYM_INDEX"])
|
||||
for b in (0xFF, 0x00)) # byte 11 erased: scroll view
|
||||
ok &= good
|
||||
print("config round trip, v0.2 config -> default SSID and path, scroll view:", good, c.hex())
|
||||
rows = (edit_row("LAT ", pos[:6], "S", 2), edit_row("LON ", pos[6:], "W", -1),
|
||||
edit_row("LAT ", pos[:6], "S", CUR_NS), # N/S in bold
|
||||
edit_row("LON ", pos[6:], "W", CUR_EW - 7), # E/W in bold
|
||||
edit_row("LON ", pos[6:], "W", CUR_SSID - 7)) # cursor on SSID: none
|
||||
ok &= rows == (("LAT 33 52.13 S", 8), ("LON 151 12.56 W", None), ("LAT 33 52.13 S", 14),
|
||||
("LON 151 12.56 W", 14), ("LON 151 12.56 W", None))
|
||||
print("editor rows:", rows)
|
||||
# nav_dir() as app_overlay.c: UP +1 / DOWN -1 with SET_NAV, swapped without
|
||||
good = [nav_dir(k, sn) for sn in (True, False) for k in "UD*"] == [1, -1, 0, -1, 1, 0]
|
||||
# 13 digits typed in a row skip N/S and stay on the last digit; the
|
||||
# navigation keys reach N/S, E/W, SSID and path; '*' acts on the field under
|
||||
# the cursor. Run for both SET_NAV settings: the raw key moving the cursor
|
||||
# forward (nav_dir +1) is UP with SET_NAV, DOWN without.
|
||||
typed = editor_keys("3352131511256", [0] * 13)
|
||||
good &= typed == (pos, 0, 7, 1, 5, 13)
|
||||
good &= editor_keys("2*", pos)[:2] == ([2] + pos[1:], 1) # '*' on a LAT digit
|
||||
for sn in (True, False):
|
||||
N, P = ("U", "D") if sn else ("D", "U") # next / previous field
|
||||
k = lambda keys, **kw: editor_keys(keys, pos, set_nav=sn, **kw)
|
||||
good &= k(N)[5] == 1 and k(P)[5] == 0 and k(N + P)[5] == 0 # the raw keys' direction
|
||||
good &= k(P * 13 + N * 6 + "*")[1:] == (0 ^ 1, 7, 1, 5, CUR_NS)
|
||||
good &= k(N * 14 + "*" + "5")[:2] == (pos, 2) # E/W: '*', a digit ignored
|
||||
good &= k(N * 15 + "**")[2:] == (9, 1, 5, CUR_SSID)
|
||||
good &= k(N * 16 + "**" + N)[3:] == (0, 5, CUR_SYM)
|
||||
good &= k(N * 15 + "F*F*")[2] == 5 # F then '*': SSID back
|
||||
good &= k(N * 15 + "F*" * 8, ssid=3)[2] == 11 # ... wrapping 0 -> 15
|
||||
good &= k(N * 16 + "F*F*F*")[3] == 1 # path back, 1 -> 0 -> 2 -> 1
|
||||
good &= k(N * 15 + "FF*")[2] == 8 # F twice: disarmed
|
||||
good &= k(N * 15 + "F" + N + "*")[3:] == (2, 5, CUR_PATH) # F used by a move, '*' forward
|
||||
good &= k(N * 17 + "**")[4:] == (7, CUR_SYM) # symbol forward
|
||||
good &= k(N * 17 + "F*")[4:] == (4, CUR_SYM) # symbol backward
|
||||
ok &= good
|
||||
print("editor keys (SET_NAV on and off):", good)
|
||||
checks = [([4,8,5,0,9,0,0,0,2,1,6,2,5], True), ([9,0,0,0,0,0,1,8,0,0,0,0,0], True),
|
||||
([9,0,0,1,0,0,0,0,0,0,0,0,0], False), ([4,8,6,0,0,0,0,0,0,0,0,0,0], False),
|
||||
([4,8,0,0,0,0,1,8,0,0,0,0,1], False), ([4,8,0,0,0,0,1,8,1,0,0,0,0], False),
|
||||
([4,8,0,0,0,0,0,0,0,6,0,0,0], False)]
|
||||
good = all(pos_ok(d) == w for d, w in checks)
|
||||
ok &= good
|
||||
print("posOk limits:", good)
|
||||
|
||||
print("\n%-6s %-5s %4s %6s %5s | frames (3 seeds)" % ("phase", "rx", "tw", "path", "noise"))
|
||||
for reset in (False, True):
|
||||
for mode in ("raw", "std"):
|
||||
for tw, path_db in ((0, 0.0), (4, 0.0), (0, 5.0)):
|
||||
for noise in (0, 1500):
|
||||
w = afsk(bc, tw, reset, path_db)
|
||||
n = 0
|
||||
for seed in (1, 2, 3):
|
||||
adc = M.channel(w, mode, noise, 0, 0, seed)
|
||||
d = M.Demod()
|
||||
for s in adc:
|
||||
d.sample(s)
|
||||
n += d.frames == [f]
|
||||
print("%-6s %-5s %4d %+5.0fdB %5d | %d/3" % (
|
||||
"reset" if reset else "cont", mode, tw, path_db, noise, n))
|
||||
if not reset and noise == 0:
|
||||
ok &= n == 3
|
||||
print("\nALL OK" if ok else "\nFAILURES")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,43 @@
|
||||
/* Overlay-app link script (POC).
|
||||
*
|
||||
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
|
||||
* the multiboot RAM stub — never both active at once). Code + rodata + data +
|
||||
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
|
||||
* data only; the loader zeroes the overlay before copying, so bss starts clean.
|
||||
*
|
||||
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
|
||||
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
|
||||
* mismatch. */
|
||||
|
||||
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
|
||||
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
|
||||
* The default is only a fallback for a standalone size check. */
|
||||
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
|
||||
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
|
||||
|
||||
ENTRY(app_main)
|
||||
|
||||
MEMORY {
|
||||
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
|
||||
}
|
||||
|
||||
SECTIONS {
|
||||
.app APP_VMA : {
|
||||
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
|
||||
*(.text .text.*)
|
||||
*(.rodata .rodata.*)
|
||||
. = ALIGN(4);
|
||||
*(.data .data.*)
|
||||
. = ALIGN(4);
|
||||
__app_bss_start = .;
|
||||
*(.bss .bss.* COMMON)
|
||||
. = ALIGN(4);
|
||||
__app_bss_end = .;
|
||||
} > APP
|
||||
|
||||
__app_end = .;
|
||||
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
|
||||
"Beacon overlay app overflows the 4 KiB overlay")
|
||||
|
||||
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/* 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).
|
||||
*
|
||||
* No division is linked: numbers are printed by subtracting powers of ten.
|
||||
*
|
||||
* 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"
|
||||
#include "beacon_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#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
|
||||
|
||||
_Static_assert(IDLE_STEP == 5 && TX_STEP == 5, "loadConfig() checks multiples of 5");
|
||||
|
||||
/* Texts, fox identifiers, the Morse table, the powers of ten and icons are
|
||||
* read-only assets (gen_assets.py); only the displayed item is fetched from
|
||||
* external flash. */
|
||||
|
||||
/* All the state in one struct: Thumb-1 code then reaches every field from a
|
||||
* single base address (one literal per function instead of one per global),
|
||||
* byte fields first so their offsets fit ldrb's 0..31 immediate. The overlay
|
||||
* is zeroed by the loader at each launch. */
|
||||
struct globals {
|
||||
bool foxLocked, fArm, fLongDone, running, phaseTx, carrier;
|
||||
uint8_t beaconIdle, idleLeft, idleTick, beaconTx, secShown, charsSent, foxFox;
|
||||
uint8_t prevKey;
|
||||
uint16_t fHoldMs, txMsLeft;
|
||||
const app_api_t *api;
|
||||
char foxCall[CALL_MAX+1];
|
||||
char msg[17];
|
||||
};
|
||||
static struct globals g;
|
||||
#define A (g.api)
|
||||
|
||||
_Static_assert(offsetof(struct globals, prevKey) < 32u, "byte fields out of ldrb range");
|
||||
|
||||
/* Text from the assets in the caller's buffer (TEXT_MAX bytes). */
|
||||
static char *T(char *buf,uint16_t off){ A->asset_read(off,buf,TEXT_MAX); return buf; }
|
||||
|
||||
/* ---- 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; }
|
||||
/* Decimal digits of v, no leading zeros, by subtracting the powers of ten. */
|
||||
static char *putu(char *o,uint32_t v){
|
||||
uint32_t place[PLACE_LEN/4u];
|
||||
A->asset_read(PLACE,place,sizeof(place));
|
||||
bool lead=true;
|
||||
for(uint8_t i=0;i<PLACE_LEN/4u;i++){
|
||||
uint8_t d=0;
|
||||
while(v>=place[i]){ v-=place[i]; d++; }
|
||||
if(d||i==PLACE_LEN/4u-1u) lead=false;
|
||||
if(!lead) *o++=(char)('0'+d);
|
||||
}
|
||||
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; }
|
||||
/* Whole seconds left in the TX window, rounded up, by subtraction. */
|
||||
static uint8_t secsLeft(void){ uint8_t s=0; for(uint32_t n=g.txMsLeft+999u;n>=1000u;n-=1000u) s++; return s; }
|
||||
static bool multipleOf5(uint8_t v){ while(v>=5u) v-=5u; return v==0u; }
|
||||
|
||||
/* ---- 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: g.beaconTx =rangeStep(g.beaconTx, TX_MIN, TX_MAX, TX_STEP, dir); return true;
|
||||
case APP_KEY_2: g.beaconIdle=rangeStep(g.beaconIdle,IDLE_MIN,IDLE_MAX,IDLE_STEP,dir); return true;
|
||||
case APP_KEY_3:
|
||||
if(dir>0){ if(++g.foxFox>=FOX_COUNT) g.foxFox=0; }
|
||||
else g.foxFox=g.foxFox?(uint8_t)(g.foxFox-1u):(uint8_t)(FOX_COUNT-1u);
|
||||
return true;
|
||||
case APP_KEY_4: g.carrier=!g.carrier; return true; /* TONE <-> CARR (dir moot) */
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- message ---- */
|
||||
static void buildMsg(void){
|
||||
char t[TEXT_MAX];
|
||||
char *o=g.msg;
|
||||
uint8_t id=g.foxFox;
|
||||
if(g.foxFox==FOX_CALL){ /* "<call> MOE", or "MOE" alone */
|
||||
id=0;
|
||||
if(g.foxCall[0]){ o=put(o,g.foxCall); *o++=' '; }
|
||||
}
|
||||
o=put(o,T(t,FOX_ID+id*FOX_ID_STRIDE));
|
||||
*o='\0';
|
||||
}
|
||||
static void foxLabel(char *out){
|
||||
char t[TEXT_MAX];
|
||||
char *o=put(out,T(t,T_FOX));
|
||||
o=put(o,T(t,g.foxFox==FOX_CALL?T_CALL:FOX_ID+g.foxFox*FOX_ID_STRIDE));
|
||||
*o='\0';
|
||||
}
|
||||
static uint8_t morseByte(char c){
|
||||
uint8_t i, code;
|
||||
if(c>='a'&&c<='z') c-=32;
|
||||
if(c>='A'&&c<='Z') i=(uint8_t)(c-'A');
|
||||
else if(c>='0'&&c<='9') i=(uint8_t)(26+c-'0');
|
||||
else if(c=='/') i=36;
|
||||
else return 0;
|
||||
A->asset_read(MORSE+i,&code,1);
|
||||
return code;
|
||||
}
|
||||
|
||||
/* ---- 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){
|
||||
char s[16], t[TEXT_MAX];
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
A->print_inverse(T(t,T_BEACON),2,0,true,true,26);
|
||||
A->draw_battery();
|
||||
if(g.foxLocked||g.fArm) A->asset_read(g.foxLocked?BMP_LOCK:BMP_F,A->status_line+70,BMP_F_LEN);
|
||||
/* the TX frequency (10 Hz units) as MHz with 5 decimals: every digit, then
|
||||
the point before the last five (f >= 18 MHz: at least 7 digits) */
|
||||
{ char *e=putu(s,A->tx_freq());
|
||||
e[1]='\0';
|
||||
for(uint8_t k=0;k<5u;k++,e--) e[0]=e[-1];
|
||||
e[0]='.'; }
|
||||
A->print_normal(s,(uint8_t)(126-slen(s)*7),0,6);
|
||||
}
|
||||
static void drawTxSeconds(void){
|
||||
char s[16], t[TEXT_MAX];
|
||||
uint8_t sec=secsLeft();
|
||||
for(uint8_t x=0;x<LCD_WIDTH;x++) A->fb[0][x]=0;
|
||||
A->print_normal(T(t,T_TX),1,0,0);
|
||||
char *o=s; put2(&o,sec); *o++='s'; *o='\0';
|
||||
A->print_normal(s,(uint8_t)(127-slen(s)*7),0,0);
|
||||
g.secShown=sec;
|
||||
}
|
||||
static void drawMessage(uint8_t vis){
|
||||
const char *msg=g.msg;
|
||||
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)>>1),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)>>1),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)>>1),0,MSG_ID,8);
|
||||
}
|
||||
}
|
||||
static void updateProgress(uint8_t vis){
|
||||
const char *sp=findsp(g.msg); uint8_t top;
|
||||
g.charsSent=vis;
|
||||
if(!sp) top=MSG_ONE;
|
||||
else { uint8_t callLen=(uint8_t)(sp-g.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(g.charsSent);
|
||||
A->blit_line(top); A->blit_line(top+1);
|
||||
}
|
||||
static void beaconDraw(bool txNow,uint8_t il){
|
||||
char s[16], t[TEXT_MAX];
|
||||
chrome();
|
||||
if(txNow){
|
||||
A->asset_read(BMP_TX,A->status_line+48,BMP_TX_LEN);
|
||||
drawTxSeconds();
|
||||
drawMessage(g.charsSent);
|
||||
} else {
|
||||
A->print_string(T(t,T_IDLE),0,127,1,10);
|
||||
char *o=s; put2(&o,il); *o++='s'; *o='\0';
|
||||
A->print_string(s,0,127,3,10);
|
||||
}
|
||||
char *o=put(s,T(t,T_TX)); *o++=' '; o=putu(o,g.beaconTx); *o++='s'; *o='\0'; tag(s,4,5);
|
||||
o=put(s,T(t,T_IDLE)); *o++=' '; o=putu(o,g.beaconIdle); *o++='s'; *o='\0'; tag(s,4,6);
|
||||
foxLabel(s); tag(s,66,5);
|
||||
{ const char *m=T(t,g.carrier?T_CARR:T_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){ g.fHoldMs=0; g.fLongDone=false; return false; }
|
||||
if(g.fLongDone) return false;
|
||||
g.fHoldMs+=ms;
|
||||
if(g.fHoldMs>=LOCK_HOLD_MS){ g.fLongDone=true; g.foxLocked=!g.foxLocked; g.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;
|
||||
g.txMsLeft=(g.txMsLeft>slice)?(uint16_t)(g.txMsLeft-slice):0;
|
||||
A->backlight_update();
|
||||
if(secsLeft()!=g.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==g.prevKey){ g.prevKey=key; continue; }
|
||||
g.prevKey=key;
|
||||
A->backlight_on();
|
||||
if(g.foxLocked) continue;
|
||||
if(key==APP_KEY_EXIT){ g.running=false; return true; }
|
||||
if(key==APP_KEY_MENU) return true; /* cut short, restart idle */
|
||||
if(key==APP_KEY_F){ g.fArm=!g.fArm; beaconDraw(true,0); blit(); continue; }
|
||||
if(settingKey(key,g.fArm?-1:1)){ g.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(g.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(g.carrier) A->tx_carrier(false); /* CARR: carrier off until first element */
|
||||
g.txMsLeft=(uint16_t)g.beaconTx*1000u;
|
||||
bool stop=false;
|
||||
while(!stop && g.txMsLeft>0){
|
||||
buildMsg();
|
||||
g.charsSent=0;
|
||||
beaconDraw(true,0); blit();
|
||||
for(uint8_t i=0;!stop && g.msg[i];i++) stop=morseChar(g.msg[i],(uint8_t)(i+1));
|
||||
if(!stop && g.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==g.prevKey){ g.prevKey=key; return; }
|
||||
g.prevKey=key;
|
||||
A->backlight_on();
|
||||
if(g.foxLocked) return;
|
||||
if(key==APP_KEY_F){ g.fArm=!g.fArm; return; }
|
||||
switch(key){
|
||||
case APP_KEY_EXIT: g.running=false; break;
|
||||
case APP_KEY_MENU: g.idleLeft=g.beaconIdle; g.idleTick=0; break;
|
||||
default: settingKey(key,g.fArm?-1:1); break;
|
||||
}
|
||||
g.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&&multipleOf5(c[1])) g.beaconIdle=c[1];
|
||||
if(c[2]<FOX_COUNT) g.foxFox=c[2];
|
||||
if(c[3]>=TX_MIN&&c[3]<=TX_MAX&&multipleOf5(c[3])) g.beaconTx=c[3];
|
||||
if(c[4]<=1) g.carrier=c[4]; /* erased (0xFF) legacy config -> keep default */
|
||||
}
|
||||
}
|
||||
static void saveConfig(void){ uint8_t c[5]={CFG_MAGIC,g.beaconIdle,g.foxFox,g.beaconTx,(uint8_t)g.carrier}; A->cfg_save(c,5); }
|
||||
|
||||
static void txDenied(void){
|
||||
char t[TEXT_MAX];
|
||||
chrome();
|
||||
A->print_string(T(t,T_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;
|
||||
/* locks, holds and the TONE keying mode start zeroed (.bss) */
|
||||
g.beaconIdle=IDLE_DEF; g.beaconTx=TX_DEF; g.foxFox=FOX_CALL;
|
||||
g.prevKey=APP_KEY_INVALID;
|
||||
A->boot_callsign(g.foxCall,sizeof(g.foxCall));
|
||||
loadConfig();
|
||||
A->backlight_on();
|
||||
|
||||
g.phaseTx=true; g.idleLeft=g.beaconIdle; g.idleTick=0;
|
||||
g.running=true;
|
||||
while(g.running){
|
||||
if(g.phaseTx){
|
||||
if(A->tx_state()!=0){ txDenied(); g.phaseTx=false; g.idleLeft=g.beaconIdle; g.idleTick=0; continue; }
|
||||
transmit();
|
||||
g.phaseTx=false; g.idleLeft=g.beaconIdle; g.idleTick=0;
|
||||
continue;
|
||||
}
|
||||
idleKeys();
|
||||
if(!g.running) break;
|
||||
beaconDraw(false,g.idleLeft); blit();
|
||||
if(++g.idleTick>=(1000/TICK_MS)){ g.idleTick=0; if(g.idleLeft>0)g.idleLeft--; if(g.idleLeft==0)g.phaseTx=true; }
|
||||
A->battery_sample();
|
||||
tickDelay();
|
||||
}
|
||||
|
||||
saveConfig();
|
||||
A->tx_end(); /* safety: PA off + RX restored */
|
||||
A->audio_path(false);
|
||||
}
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/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.1"
|
||||
APP_API_MIN=2
|
||||
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/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 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 --assets "${APP}_assets.bin" >/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"
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
# Beacon read-only assets: texts, fox identifiers, Morse table and icons.
|
||||
#
|
||||
# ./gen_assets.py beacon_assets.bin beacon_assets.h
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
|
||||
a = Assets("BEACON")
|
||||
a.text("T_BEACON", "BEACON")
|
||||
a.text("T_TX", "TX")
|
||||
a.text("T_IDLE", "IDLE")
|
||||
a.text("T_FOX", "FOX ")
|
||||
a.text("T_CALL", "CALL")
|
||||
a.text("T_CARR", "CARR")
|
||||
a.text("T_TONE", "TONE")
|
||||
a.text("T_TX_OFF", "TX OFF")
|
||||
# Fox identifiers by foxFox (0..FOX_MO); FOX_CALL sends "<call> " + entry 0.
|
||||
a.table("FOX_ID", ["MOE", "MOI", "MOS", "MOH", "MO5", "MO"])
|
||||
# Sentinel-prefixed Morse (leading 1, then elements MSB-first: 0 dit, 1 dah):
|
||||
# A..Z, then 0..9, then '/'.
|
||||
a.u8("MORSE", [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,
|
||||
0x3F,0x2F,0x27,0x23,0x21,0x20,0x30,0x38,0x3C,0x3E,
|
||||
0x32])
|
||||
a.u8("BMP_TX", [0x1c,0x22,0x41,0x1c,0x22,0x00,0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c])
|
||||
a.u8("BMP_LOCK", [0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c])
|
||||
a.u8("BMP_F", [0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e])
|
||||
# Powers of ten, 10^8 down to 1: numbers (the frequency too) are printed by
|
||||
# subtraction, so the app links no division.
|
||||
a.u32("PLACE", [10 ** k for k in range(8, -1, -1)])
|
||||
a.main()
|
||||
@@ -0,0 +1,30 @@
|
||||
/* Overlay-app link script. */
|
||||
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
|
||||
APP_LENGTH = 0x1000;
|
||||
|
||||
ENTRY(app_main)
|
||||
|
||||
MEMORY {
|
||||
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
|
||||
}
|
||||
|
||||
SECTIONS {
|
||||
.app APP_VMA : {
|
||||
KEEP(*(.text.entry))
|
||||
*(.text .text.*)
|
||||
*(.rodata .rodata.*)
|
||||
. = ALIGN(4);
|
||||
*(.data .data.*)
|
||||
. = ALIGN(4);
|
||||
__app_bss_start = .;
|
||||
*(.bss .bss.* COMMON)
|
||||
. = ALIGN(4);
|
||||
__app_bss_end = .;
|
||||
} > APP
|
||||
|
||||
__app_end = .;
|
||||
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
|
||||
"BEAM overlay app overflows the 4 KiB overlay")
|
||||
|
||||
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
/* 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.
|
||||
*
|
||||
* Requires APP_CAP_BEAM2: the resident bridge only tunes the channel and
|
||||
* converts it to/from app_beam_channel_t; this app drives the BK4819 FSK modem
|
||||
* itself, from register sequences stored in its assets.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "beam_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define PACKET_MAGIC 0xBEA5u
|
||||
#define PACKET_VERSION 2u
|
||||
#define PACKET_START 0xABCDu
|
||||
#define PACKET_END 0xDCBAu
|
||||
#define PACKET_WORDS 36u
|
||||
#define STORE_WORDS 18u /* 72 bytes, 4-byte aligned for the payload */
|
||||
|
||||
/* BK4819 registers and REG_02 interrupt flags (mirror driver/bk4819-regs.h). */
|
||||
enum {
|
||||
REG_02 = 0x02, /* interrupt status */
|
||||
REG_0B = 0x0B, /* FSK status: bit 4 = CRC error */
|
||||
REG_0C = 0x0C, /* bit 0 = interrupt pending */
|
||||
REG_5F = 0x5F, /* FSK data FIFO */
|
||||
};
|
||||
#define IRQ_FSK_FIFO_ALMOST_FULL (1u << 12)
|
||||
#define IRQ_FSK_RX_FINISHED (1u << 13)
|
||||
|
||||
/* Largest register sequence (asset_read into a stack buffer). */
|
||||
#define SEQ_MAX_LEN SEQ_RX_LEN
|
||||
_Static_assert(SEQ_RESET_LEN <= SEQ_MAX_LEN && SEQ_SETUP_LEN <= SEQ_MAX_LEN &&
|
||||
SEQ_TX_LOAD_LEN <= SEQ_MAX_LEN && SEQ_TX_GO_LEN <= SEQ_MAX_LEN &&
|
||||
SEQ_TX_END_LEN <= SEQ_MAX_LEN, "SEQ_MAX_LEN too small");
|
||||
|
||||
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");
|
||||
|
||||
/* All the state in one struct: Thumb-1 code then reaches every field from a
|
||||
* single base address (one literal per function instead of one per global).
|
||||
* The packet buffer lives on app_main's stack, outside the 4 KiB overlay; the
|
||||
* overlay itself is zeroed by the loader at each launch. */
|
||||
struct globals {
|
||||
uint8_t mode, status;
|
||||
uint8_t fsk_index; /* next RX FIFO word of the packet */
|
||||
bool receiving, running, dirty;
|
||||
uint16_t copiedChannel;
|
||||
const app_api_t *api;
|
||||
uint32_t *packet_store; /* STORE_WORDS words, on app_main's stack */
|
||||
};
|
||||
static struct globals g;
|
||||
#define A (g.api)
|
||||
|
||||
static uint16_t *packet(void) { return (uint16_t *)g.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 key[8]; /* obfuscation key, from the assets */
|
||||
uint16_t *p = packet();
|
||||
A->asset_read(OBFUSCATION, key, sizeof(key));
|
||||
for (uint8_t i = 0; i < 32u; i++)
|
||||
p[i + 1u] ^= key[i & 7u];
|
||||
}
|
||||
|
||||
/* Run one register sequence: (register, value) word pairs, a SEQ_DELAY
|
||||
* register meaning "wait value ms". */
|
||||
static void bk_seq(uint16_t offset, uint16_t len)
|
||||
{
|
||||
uint16_t seq[SEQ_MAX_LEN / 2u];
|
||||
A->asset_read(offset, seq, len);
|
||||
for (const uint16_t *w = seq; w < seq + len / 2u; w += 2) {
|
||||
if (w[0] == SEQ_DELAY)
|
||||
A->delay_ms(w[1]);
|
||||
else
|
||||
A->bk_write((uint8_t)w[0], w[1]);
|
||||
}
|
||||
}
|
||||
#define SEQ(name) bk_seq(name, name##_LEN)
|
||||
|
||||
/* Key up, send one packet (BK4819_SendFSKData), then back to RX. */
|
||||
static void fsk_send(const uint16_t *p)
|
||||
{
|
||||
A->tx_set_params();
|
||||
SEQ(SEQ_TX_LOAD);
|
||||
for (uint8_t i = 0; i < PACKET_WORDS; i++)
|
||||
A->bk_write(REG_5F, p[i]);
|
||||
SEQ(SEQ_TX_GO);
|
||||
/* TX-finished poll ceiling in 5 ms ticks: ~3 per word plus margin. */
|
||||
for (uint16_t t = PACKET_WORDS * 3u + 100u; t && !(A->bk_read(REG_0C) & 1u); t--)
|
||||
A->delay_ms(5);
|
||||
SEQ(SEQ_TX_END);
|
||||
A->tx_end();
|
||||
}
|
||||
|
||||
static void fsk_rx_start(void)
|
||||
{
|
||||
g.fsk_index = 0;
|
||||
SEQ(SEQ_RX);
|
||||
}
|
||||
|
||||
/* Drain the RX FIFO into the packet; APP_BEAM_RX_* once complete. */
|
||||
static uint8_t fsk_rx_poll(uint16_t *p)
|
||||
{
|
||||
while (A->bk_read(REG_0C) & 1u) {
|
||||
A->bk_write(REG_02, 0);
|
||||
const uint16_t irq = A->bk_read(REG_02);
|
||||
if (irq & (IRQ_FSK_FIFO_ALMOST_FULL | IRQ_FSK_RX_FINISHED)) {
|
||||
uint8_t words = (irq & IRQ_FSK_RX_FINISHED)
|
||||
? (uint8_t)(PACKET_WORDS - g.fsk_index) : 4u;
|
||||
while (words--) {
|
||||
const uint16_t word = A->bk_read(REG_5F);
|
||||
if (g.fsk_index < PACKET_WORDS)
|
||||
p[g.fsk_index++] = word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (g.fsk_index < PACKET_WORDS)
|
||||
return APP_BEAM_RX_WAIT;
|
||||
|
||||
const uint16_t status = A->bk_read(REG_0B);
|
||||
fsk_rx_start();
|
||||
return (status & 0x0010u) ? APP_BEAM_RX_ERROR : APP_BEAM_RX_READY;
|
||||
}
|
||||
|
||||
static void draw(void)
|
||||
{
|
||||
/* T_STATE: READY in TX / RX mode, then one entry per later status. */
|
||||
char state[T_STATE_STRIDE];
|
||||
const uint8_t index = g.status ? (uint8_t)(g.status + 1u) : g.mode;
|
||||
A->asset_read(T_STATE + index * T_STATE_STRIDE, state, sizeof(state));
|
||||
A->beam_draw(state);
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
}
|
||||
|
||||
static void stop_rx(void)
|
||||
{
|
||||
if (g.receiving) {
|
||||
SEQ(SEQ_RESET);
|
||||
g.receiving = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void send_packet(void)
|
||||
{
|
||||
uint16_t *p = packet();
|
||||
clear_bytes(g.packet_store, STORE_WORDS * 4u);
|
||||
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();
|
||||
|
||||
g.status = STATUS_TX_WAIT;
|
||||
draw();
|
||||
fsk_send(p);
|
||||
g.status = STATUS_TX_DONE;
|
||||
for (uint8_t i = 0; i < 3u; i++) {
|
||||
A->play_tone(880, 60);
|
||||
if (i < 2u) A->delay_ms(20);
|
||||
}
|
||||
g.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 (g.mode && g.copiedChannel != 0xFFFFu)
|
||||
return;
|
||||
A->beam_prepare();
|
||||
SEQ(SEQ_SETUP);
|
||||
if (!g.mode) {
|
||||
send_packet();
|
||||
return;
|
||||
}
|
||||
fsk_rx_start();
|
||||
g.receiving = true;
|
||||
g.status = STATUS_RX_WAIT;
|
||||
g.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 (!g.receiving)
|
||||
return;
|
||||
const uint8_t result = fsk_rx_poll(packet());
|
||||
if (result == APP_BEAM_RX_WAIT)
|
||||
return;
|
||||
if (result == APP_BEAM_RX_ERROR || !valid_packet()) {
|
||||
g.status = STATUS_ERROR;
|
||||
A->backlight_on();
|
||||
g.dirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
g.copiedChannel = A->beam_save(&payload()->channel);
|
||||
stop_rx();
|
||||
g.status = g.copiedChannel == 0xFFFFu ? STATUS_RX_FULL : STATUS_RX_SAVED;
|
||||
A->backlight_on();
|
||||
g.dirty = true;
|
||||
}
|
||||
|
||||
static void key_press(uint8_t key)
|
||||
{
|
||||
switch (key) {
|
||||
case APP_KEY_UP:
|
||||
case APP_KEY_DOWN:
|
||||
stop_rx();
|
||||
g.mode ^= 1u;
|
||||
g.status = STATUS_READY;
|
||||
g.dirty = true;
|
||||
break;
|
||||
case APP_KEY_MENU:
|
||||
start();
|
||||
break;
|
||||
case APP_KEY_EXIT:
|
||||
g.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)
|
||||
{
|
||||
uint32_t store[STORE_WORDS]; /* the packet: 4-byte aligned, not in the overlay */
|
||||
A = api;
|
||||
g.packet_store = store;
|
||||
/* TX mode, READY and not receiving: zeroed (.bss) */
|
||||
g.copiedChannel = 0xFFFFu;
|
||||
g.running = true;
|
||||
g.dirty = true;
|
||||
A->backlight_on();
|
||||
|
||||
uint8_t previous = APP_KEY_INVALID;
|
||||
uint8_t batteryTicks = 0;
|
||||
while (g.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;
|
||||
g.dirty = true;
|
||||
} else {
|
||||
if (key != previous && key != APP_KEY_INVALID) {
|
||||
A->backlight_on();
|
||||
key_press(key);
|
||||
}
|
||||
previous = key;
|
||||
}
|
||||
|
||||
poll_rx();
|
||||
if (g.dirty) { draw(); g.dirty = false; }
|
||||
A->delay_ms(10);
|
||||
A->backlight_update();
|
||||
if (++batteryTicks >= 50u) {
|
||||
batteryTicks = 0;
|
||||
A->battery_sample();
|
||||
g.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
SEQ(SEQ_RESET); /* stop the FSK modem (RX or idle) before returning */
|
||||
}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")"
|
||||
APP_NAME="Beam"
|
||||
APP_VER="1.3"
|
||||
APP_API_MIN=2
|
||||
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/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 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 --exit-main --require beam2 --assets "${APP}_assets.bin" >/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"
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
# Beam read-only assets: status texts, the packet obfuscation key and the
|
||||
# BK4819 FSK register sequences run by the app (APP_CAP_BEAM2).
|
||||
#
|
||||
# ./gen_assets.py beam_assets.bin beam_assets.h
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
|
||||
a = Assets("BEAM")
|
||||
# Indexed by beam_app.c: 0/1 = READY in TX/RX mode, then status + 1.
|
||||
a.table("T_STATE", ["BEAM TX", "BEAM RX", "SENDING", "SENT", "WAITING",
|
||||
"RECEIVED", "MEM FULL", "ERROR"])
|
||||
a.u16("OBFUSCATION", [0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9])
|
||||
|
||||
# Register sequences: (register, value) word pairs; register SEQ_DELAY means
|
||||
# "wait value ms". Each one mirrors the resident BK4819 driver routine noted.
|
||||
SEQ_DELAY = 0xFF
|
||||
a.const("SEQ_DELAY", SEQ_DELAY)
|
||||
|
||||
def seq(name, pairs):
|
||||
a.u16(name, [w for pair in pairs for w in pair])
|
||||
|
||||
RESET = [ # BK4819_ResetFSK
|
||||
(0x3F, 0x0000), # disable interrupts
|
||||
(0x59, 0x0068), # sync length 4 bytes, 7 byte preamble
|
||||
(SEQ_DELAY, 30),
|
||||
(0x30, 0x0000), # BK4819_Idle
|
||||
]
|
||||
seq("SEQ_RESET", RESET)
|
||||
seq("SEQ_SETUP", [ # BK4819_SetupAircopy + BK4819_ResetFSK
|
||||
(0x70, 0x00C3), # enable Tone2, tuning gain 48
|
||||
(0x72, 0x3065), # Tone2 baudrate 1200
|
||||
(0x58, 0x00C1), # FSK enable, 1.2K RX bandwidth, TX/RX FSK 1.2K
|
||||
(0x5C, 0x5665), # enable CRC
|
||||
(0x5D, 0x4700), # FSK data length 72 bytes
|
||||
(0x5E, 0x3204),
|
||||
] + RESET)
|
||||
seq("SEQ_RX", RESET + [ # BK4819_PrepareFSKReceive
|
||||
(0x02, 0x0000),
|
||||
(0x3F, 0x0000),
|
||||
(0x37, 0x9F1F), # BK4819_RX_TurnOn
|
||||
(0x30, 0x0000),
|
||||
(0x30, 0xBFF1),
|
||||
(0x3F, 0x3000), # FSK RX finished | FIFO almost full interrupts
|
||||
(0x59, 0x4068), # clear RX FIFO
|
||||
(0x59, 0x3068), # scramble + FSK RX enable
|
||||
])
|
||||
seq("SEQ_TX_LOAD", [ # BK4819_SendFSKData, before the FIFO fill
|
||||
(SEQ_DELAY, 30),
|
||||
(0x3F, 0x8000), # FSK TX finished interrupt
|
||||
(0x59, 0x8068), # clear TX FIFO
|
||||
(0x59, 0x0068),
|
||||
])
|
||||
seq("SEQ_TX_GO", [ # BK4819_SendFSKData, after the FIFO fill
|
||||
(SEQ_DELAY, 20),
|
||||
(0x59, 0x2868), # FSK TX enable
|
||||
])
|
||||
seq("SEQ_TX_END", [ # BK4819_SendFSKData tail + PA bias off
|
||||
(0x02, 0x0000),
|
||||
(SEQ_DELAY, 30),
|
||||
] + RESET + [
|
||||
(0x36, 0x0088), # BK4819_SetupPowerAmplifier(0, 0)
|
||||
])
|
||||
a.main()
|
||||
@@ -0,0 +1,43 @@
|
||||
/* Overlay-app link script (POC).
|
||||
*
|
||||
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
|
||||
* the multiboot RAM stub — never both active at once). Code + rodata + data +
|
||||
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
|
||||
* data only; the loader zeroes the overlay before copying, so bss starts clean.
|
||||
*
|
||||
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
|
||||
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
|
||||
* mismatch. */
|
||||
|
||||
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
|
||||
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
|
||||
* The default is only a fallback for a standalone size check. */
|
||||
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
|
||||
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
|
||||
|
||||
ENTRY(app_main)
|
||||
|
||||
MEMORY {
|
||||
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
|
||||
}
|
||||
|
||||
SECTIONS {
|
||||
.app APP_VMA : {
|
||||
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
|
||||
*(.text .text.*)
|
||||
*(.rodata .rodata.*)
|
||||
. = ALIGN(4);
|
||||
*(.data .data.*)
|
||||
. = ALIGN(4);
|
||||
__app_bss_start = .;
|
||||
*(.bss .bss.* COMMON)
|
||||
. = ALIGN(4);
|
||||
__app_bss_end = .;
|
||||
} > APP
|
||||
|
||||
__app_end = .;
|
||||
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
|
||||
"Breakout overlay app overflows the 4 KiB overlay")
|
||||
|
||||
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
/* 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).
|
||||
*
|
||||
* No division is linked (libgcc's two would cost ~740 B): numbers are printed
|
||||
* by subtracting decimal places, bounce directions come from tables computed
|
||||
* by gen_assets.py, and the wall is cleared by counting the bricks left.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "breakout_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#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
|
||||
#define TICK_MS 20u
|
||||
#define STR_LEN 12u /* "Score " + 5 digits + NUL */
|
||||
|
||||
typedef struct { uint8_t x; uint8_t y; bool destroy; } Brick;
|
||||
typedef struct { uint8_t x; uint8_t p; } Racket; /* x: 0..102, even */
|
||||
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; }
|
||||
|
||||
/* ---- all the state in one struct: Thumb-1 code then reaches every field
|
||||
* from a single base address (one literal per function instead of one per
|
||||
* global), byte fields first so their offsets fit ldrb's 0..31 immediate.
|
||||
* It lives in the overlay, which the loader zeroes at each launch. ---- */
|
||||
struct globals {
|
||||
uint8_t blockAnim, titleTick, levelCountBreakout, gameOverLevel;
|
||||
uint8_t kbdPrev, kbdCur, wallLeft;
|
||||
bool isInitialized, isPaused, saverPaused, saverActive;
|
||||
bool gameOver, redrawRequired, title;
|
||||
Racket racket;
|
||||
uint16_t tone, score, gameOverScore;
|
||||
int16_t ballCount;
|
||||
Ball ball;
|
||||
const app_api_t *api;
|
||||
Brick brick[BRICK_NUMBER];
|
||||
};
|
||||
static struct globals g;
|
||||
#define A (g.api)
|
||||
|
||||
_Static_assert(offsetof(struct globals, title) < 32u, "byte fields out of ldrb range");
|
||||
|
||||
/* A random ball dx in -3..3, by multiplication rather than a modulo. */
|
||||
static int rand_dx(void) { return (int)(((A->rand32() >> 24) * 7u) >> 8) - 3; }
|
||||
|
||||
static void reset(void) { g.ballCount = BALL_NUMBER; g.levelCountBreakout = 1; g.score = 0; }
|
||||
|
||||
static void playBeep(uint16_t t) { A->play_tone(t, 100); }
|
||||
|
||||
/* "<label><value>" in tiny type on the status line: the value zero-padded to
|
||||
* w digits, or longer if it needs more (up to 5, no division). */
|
||||
static void stat(uint16_t label, uint16_t v, uint8_t w, uint8_t x) {
|
||||
char s[STR_LEN];
|
||||
uint16_t place[5];
|
||||
A->asset_read(label, s, TEXT_MAX);
|
||||
char *o = s;
|
||||
while (*o) o++;
|
||||
A->asset_read(PLACE, place, sizeof(place));
|
||||
bool lead = true; /* still in leading zeros beyond the width */
|
||||
for (uint8_t i = 0; i < 5u; i++) {
|
||||
uint8_t d = 0;
|
||||
while (v >= place[i]) { v -= place[i]; d++; }
|
||||
if (d || i >= 5u - w) lead = false;
|
||||
if (!lead) *o++ = (char)('0' + d);
|
||||
}
|
||||
*o = '\0';
|
||||
A->print_tiny(s, x, 1, true, true);
|
||||
}
|
||||
|
||||
/* A centred bold text from the assets on LCD line `line`. */
|
||||
static void say(uint16_t off, uint8_t line) {
|
||||
char s[TEXT_MAX];
|
||||
A->asset_read(off, s, TEXT_MAX);
|
||||
A->print_bold(s, 0, LCD_WIDTH - 1, line);
|
||||
}
|
||||
|
||||
static void drawScoreValues(uint8_t level, int16_t balls, uint16_t value) {
|
||||
A->status_clear();
|
||||
stat(T_LEVEL, level, 2, 0);
|
||||
stat(T_BALL, (balls < 0) ? 0 : (uint16_t)balls, 2, 45);
|
||||
stat(T_SCORE, value, 4, 88);
|
||||
}
|
||||
|
||||
static void drawScore(void) { drawScoreValues(g.levelCountBreakout, g.ballCount, g.score); }
|
||||
|
||||
static void renderBall(bool state) {
|
||||
A->draw_rect(A->fb, g.ball.x, g.ball.y, g.ball.x + BALL_WIDTH - 1, g.ball.y + BALL_HEIGHT - 1, state);
|
||||
A->draw_line(A->fb, g.ball.x - 1, g.ball.y + 1, g.ball.x + BALL_WIDTH, g.ball.y + 1, state);
|
||||
}
|
||||
|
||||
static void initBall(void) { g.ball.x = 62; g.ball.y = 30; g.ball.dx = 0; g.ball.dy = 1; renderBall(true); }
|
||||
|
||||
/* Bounce off a racket or brick at x, w wide: the new dx for the hit offset
|
||||
* x + w - ball.x (-1 at most below 0, as the hit tests allow) comes from a
|
||||
* table of the original formula (x + w - ball.x) * -2num / w + num. */
|
||||
static void bounce(uint16_t table, int x, int w) {
|
||||
int8_t dx;
|
||||
A->asset_read((uint16_t)(table + (x + w - g.ball.x) + 1), &dx, 1);
|
||||
g.ball.dx = dx;
|
||||
g.ball.dy = (int8_t)-g.ball.dy;
|
||||
}
|
||||
|
||||
static void initWall(void) {
|
||||
Brick *current = g.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++;
|
||||
}
|
||||
g.wallLeft = BRICK_NUMBER;
|
||||
}
|
||||
|
||||
static void initRacket(void);
|
||||
|
||||
static void drawBall(void) {
|
||||
renderBall(false);
|
||||
g.ball.x += g.ball.dx; g.ball.y += g.ball.dy;
|
||||
|
||||
if (g.ball.y <= 0) { g.ball.dx = (int8_t)rand_dx(); g.ball.dy = 1; }
|
||||
else if (g.ball.x <= 2) { g.ball.dx = (int8_t)iabs(g.ball.dx); }
|
||||
else if (g.ball.x >= 124) { g.ball.dx = (int8_t)-iabs(g.ball.dx); }
|
||||
|
||||
if (g.ball.y == 47) {
|
||||
if (g.ball.x + 1 >= g.racket.x && g.ball.x - 1 <= g.racket.x + RACKET_WIDTH) {
|
||||
bounce(RACKET_DX, g.racket.x, RACKET_WIDTH); g.tone = 400;
|
||||
}
|
||||
} else if (g.ball.y > 49) {
|
||||
g.ballCount--;
|
||||
A->display_clear();
|
||||
drawScore();
|
||||
g.tone = 800;
|
||||
if (g.ballCount < 0) {
|
||||
g.gameOverLevel = g.levelCountBreakout;
|
||||
g.gameOverScore = g.score;
|
||||
g.gameOver = true;
|
||||
reset(); initWall(); /* drawWall drawn by main loop */
|
||||
g.isPaused = true;
|
||||
say(T_GAME_OVER, 4);
|
||||
}
|
||||
initRacket(); initBall();
|
||||
}
|
||||
renderBall(true);
|
||||
}
|
||||
|
||||
static void drawWall(bool checkCollision) {
|
||||
uint8_t anim[BRICK_ANIM_LEN]; /* brick patterns, read once per frame */
|
||||
A->asset_read(BRICK_ANIM, anim, sizeof(anim));
|
||||
for (uint8_t i = 0; i < BRICK_NUMBER; i++) {
|
||||
Brick *b = &g.brick[i];
|
||||
if (b->destroy) continue;
|
||||
uint8_t *fb_ptr = A->fb[b->y >> 3] + b->x;
|
||||
fb_ptr[0] = 0b00011110;
|
||||
fb_ptr[14] = 0b00011110;
|
||||
|
||||
if (checkCollision &&
|
||||
(g.ball.x + 1 >= b->x && g.ball.x - 1 <= b->x + BRICK_WIDTH) &&
|
||||
(g.ball.y + 1 >= b->y && g.ball.y - 1 <= b->y + BRICK_HEIGHT)) {
|
||||
b->destroy = true; g.score++;
|
||||
bounce(BRICK_DX, b->x, BRICK_WIDTH);
|
||||
|
||||
A->led(true);
|
||||
memset(fb_ptr + 1, 0b00111111, 13);
|
||||
A->blit_line(b->y >> 3);
|
||||
playBeep(600);
|
||||
memset(fb_ptr + 0, 0b00000000, 15);
|
||||
A->blit_line(b->y >> 3);
|
||||
A->led(false);
|
||||
|
||||
if (--g.wallLeft == 0u) { /* the wall is cleared */
|
||||
g.levelCountBreakout++; g.ballCount++; initWall(); return;
|
||||
}
|
||||
} else {
|
||||
for (uint8_t k = 0; k < 13; k++)
|
||||
fb_ptr[k + 1] = anim[(g.blockAnim + k) & 3u];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) { g.racket.x = 64 - (RACKET_WIDTH / 2); g.racket.p = g.racket.x; renderRacket(g.racket.x, true); }
|
||||
|
||||
static void drawRacket(void) {
|
||||
if (g.racket.p != g.racket.x) {
|
||||
renderRacket(g.racket.p, false);
|
||||
g.racket.p = g.racket.x;
|
||||
renderRacket(g.racket.x, true);
|
||||
}
|
||||
}
|
||||
|
||||
static void redrawScene(void) {
|
||||
A->display_clear();
|
||||
if (g.gameOver) {
|
||||
drawScoreValues(g.gameOverLevel, -1, g.gameOverScore);
|
||||
} else {
|
||||
drawScore();
|
||||
drawWall(false);
|
||||
}
|
||||
renderRacket(g.racket.x, true);
|
||||
g.racket.p = g.racket.x;
|
||||
renderBall(true);
|
||||
if (g.isPaused)
|
||||
say(g.gameOver ? T_GAME_OVER : T_PAUSE, 4);
|
||||
}
|
||||
|
||||
/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers,
|
||||
* with a blinking prompt on the blank bottom page. */
|
||||
static void drawTitle(void) {
|
||||
g.titleTick++;
|
||||
A->asset_read(ART_TITLE, A->status_line, LCD_WIDTH);
|
||||
A->asset_read(ART_TITLE + LCD_WIDTH, A->fb[0], 7u * LCD_WIDTH);
|
||||
if (g.titleTick & 32u)
|
||||
say(T_PRESS, 6);
|
||||
}
|
||||
|
||||
static void OnKeyDown(uint8_t key) {
|
||||
if (g.title) { /* MENU starts the game (and pauses it later on) */
|
||||
if (key == APP_KEY_MENU) { g.title = false; g.redrawRequired = true; }
|
||||
else if (key == APP_KEY_EXIT) g.isInitialized = false;
|
||||
return;
|
||||
}
|
||||
bool wasPaused = g.isPaused;
|
||||
switch (key) {
|
||||
case APP_KEY_4:
|
||||
case APP_KEY_UP: if (!g.isPaused && g.racket.x > 0) g.racket.x -= 2; g.isPaused = false; break;
|
||||
case APP_KEY_0:
|
||||
case APP_KEY_DOWN: if (!g.isPaused && g.racket.x < 102) g.racket.x += 2; g.isPaused = false; break;
|
||||
case APP_KEY_MENU:
|
||||
g.isPaused = !g.isPaused;
|
||||
if (g.isPaused) say(T_PAUSE, 4);
|
||||
break;
|
||||
case APP_KEY_EXIT: g.isPaused = false; g.isInitialized = false; break;
|
||||
}
|
||||
if (wasPaused && !g.isPaused) {
|
||||
g.gameOver = false;
|
||||
for (uint8_t i = 0; i < 8; i++)
|
||||
A->draw_line(A->fb, 32, 32 + i, 96, 32 + i, false);
|
||||
}
|
||||
}
|
||||
|
||||
static void handleInput(void) {
|
||||
g.kbdPrev = g.kbdCur;
|
||||
g.kbdCur = A->get_key();
|
||||
|
||||
if (g.kbdCur == APP_KEY_SAVER) {
|
||||
if (!g.isPaused) {
|
||||
g.isPaused = true;
|
||||
g.saverPaused = true;
|
||||
}
|
||||
g.saverActive = true;
|
||||
g.kbdPrev = APP_KEY_INVALID;
|
||||
g.kbdCur = APP_KEY_INVALID;
|
||||
return;
|
||||
}
|
||||
|
||||
if (g.kbdCur == APP_KEY_WAKE || g.kbdCur == APP_KEY_PTT) {
|
||||
if (g.saverPaused)
|
||||
g.isPaused = false;
|
||||
g.saverPaused = false;
|
||||
g.saverActive = false;
|
||||
g.redrawRequired = true;
|
||||
g.kbdPrev = APP_KEY_INVALID;
|
||||
g.kbdCur = APP_KEY_INVALID;
|
||||
return;
|
||||
}
|
||||
|
||||
if (g.kbdCur == APP_KEY_INVALID) return;
|
||||
if (g.kbdCur == APP_KEY_UP || g.kbdCur == APP_KEY_DOWN ||
|
||||
g.kbdCur == APP_KEY_4 || g.kbdCur == APP_KEY_0 || g.kbdCur != g.kbdPrev)
|
||||
OnKeyDown(g.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();
|
||||
say(T_HELLO, 3);
|
||||
A->blit_full();
|
||||
A->delay_ms(2000);
|
||||
return;
|
||||
#endif
|
||||
|
||||
/* the loader zeroed the overlay: every flag starts false, every count 0 */
|
||||
g.kbdPrev = APP_KEY_INVALID; g.kbdCur = APP_KEY_INVALID;
|
||||
uint8_t swap = 0;
|
||||
|
||||
A->led(false);
|
||||
A->backlight_on();
|
||||
|
||||
A->display_clear();
|
||||
reset(); initWall(); initRacket(); initBall();
|
||||
A->status_clear();
|
||||
g.isInitialized = true;
|
||||
g.title = true;
|
||||
|
||||
while (g.isInitialized) {
|
||||
handleInput();
|
||||
|
||||
if (g.saverActive) {
|
||||
A->backlight_update();
|
||||
A->delay_ms(TICK_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (g.title) { /* the game scene is redrawn when MENU leaves the title */
|
||||
drawTitle();
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
A->backlight_update();
|
||||
A->delay_ms(TICK_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (g.redrawRequired) {
|
||||
redrawScene();
|
||||
g.redrawRequired = false;
|
||||
}
|
||||
|
||||
if (!g.isPaused) {
|
||||
if (swap == 0) g.blockAnim = (g.blockAnim + 1) & 3u;
|
||||
swap = (swap + 1) & 3u;
|
||||
drawScore(); drawWall(true); drawRacket(); drawBall();
|
||||
if (g.tone != 0) { playBeep(g.tone); g.tone = 0; }
|
||||
else A->delay_ms(40 - imin(g.levelCountBreakout - 1, 20));
|
||||
}
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
A->backlight_update();
|
||||
if (g.isPaused)
|
||||
A->delay_ms(TICK_MS);
|
||||
}
|
||||
}
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
|
||||
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
|
||||
# derived from the directory name; only APP_NAME - the human label baked into
|
||||
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
|
||||
# stripped), so "Broadcast FM" -> BroadcastFM.app.
|
||||
set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||
APP_NAME="Breakout" # <-- the only per-app line
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
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/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/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"
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
# Breakout read-only assets: texts, the brick animation patterns and the
|
||||
# 128x64 title screen.
|
||||
#
|
||||
# ./gen_assets.py breakout_assets.bin breakout_assets.h [preview.pgm]
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
from app_art import Canvas
|
||||
|
||||
BRICK_ANIM = [0b00110001, 0b00101001, 0b00100101, 0b00100011]
|
||||
|
||||
def bounce(w, num, t_max):
|
||||
"""Ball dx after a hit at offset t = x + w - ball.x (t = -1 .. t_max), as
|
||||
the original C expression t * (-2 * num) / w + num (division truncating
|
||||
toward zero): the app reads it from here and links no division."""
|
||||
out = []
|
||||
for t in range(-1, t_max + 1):
|
||||
a = t * (-2 * num)
|
||||
q = abs(a) // w
|
||||
out.append((q if a >= 0 else -q) + num)
|
||||
return out
|
||||
|
||||
def brick(cv, x, y):
|
||||
"""A 15 px brick as drawn by the game: end caps and the animated fill."""
|
||||
for k in range(15):
|
||||
col = 0b00011110 if k in (0, 14) else BRICK_ANIM[(k - 1) % 4]
|
||||
for bit in range(8):
|
||||
if col >> bit & 1:
|
||||
cv.set(x + k, y + bit)
|
||||
|
||||
def title_screen():
|
||||
"""BREAK / OUT, a few bricks, the racket and the ball with its trail."""
|
||||
cv = Canvas()
|
||||
cv.two_words("BREAK", "OUT")
|
||||
for x in (2, 22, 86, 106):
|
||||
brick(cv, x, 42)
|
||||
for k in range(24): # racket, as renderRacket()
|
||||
cv.set(52 + k, 54)
|
||||
if 0 < k < 23:
|
||||
cv.set(52 + k, 53); cv.set(52 + k, 55)
|
||||
cv.rect(62, 47, 64, 49) # ball, as renderBall()
|
||||
cv.set(61, 48); cv.set(65, 48)
|
||||
for i in range(0, 8, 2): # dotted trail
|
||||
cv.set(58 - i * 2, 47 - i // 2)
|
||||
return cv
|
||||
|
||||
preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None
|
||||
|
||||
a = Assets("BREAKOUT")
|
||||
a.text("T_LEVEL", "Level ")
|
||||
a.text("T_BALL", "Ball ")
|
||||
a.text("T_SCORE", "Score ")
|
||||
a.text("T_GAME_OVER", "GAME OVER")
|
||||
a.text("T_PAUSE", "PAUSE")
|
||||
a.text("T_HELLO", "OVL HELLO") # APP_POC_HELLO bisection build only
|
||||
a.text("T_PRESS", "PRESS MENU")
|
||||
a.u8("BRICK_ANIM", BRICK_ANIM)
|
||||
a.u16("PLACE", [10000, 1000, 100, 10, 1]) # decimal places, no division
|
||||
# Hit offsets: ball.x within one pixel of the racket (24 px) or brick (14 px).
|
||||
a.i8("RACKET_DX", bounce(24, 3, 25))
|
||||
a.i8("BRICK_DX", bounce(14, 2, 15))
|
||||
title = title_screen()
|
||||
a.raw("ART_TITLE", title.pages())
|
||||
a.main()
|
||||
if preview_path:
|
||||
title.preview(preview_path)
|
||||
@@ -0,0 +1,25 @@
|
||||
# Cube3D
|
||||
|
||||
Cube3D renders rotating 3D shapes on the 128x64 monochrome display. Geometry
|
||||
and lookup tables live in the app assets; the overlay uses fixed
|
||||
point arithmetic and performs no hardware floating-point or division.
|
||||
|
||||
The `GLOBE` scene has a dedicated renderer with latitude/longitude lines and
|
||||
a stable circular limb. In wire mode its far side is
|
||||
dotted; in solid mode it is hidden. Shape 10, `DODECA`, is reached with `*`
|
||||
after the globe because the numeric keypad directly selects shapes 1 through 9.
|
||||
|
||||
## Keys
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| 1-9 | Select a shape directly (`9` selects the globe) |
|
||||
| * | Next shape |
|
||||
| UP / DOWN | Increase / decrease rotation speed |
|
||||
| F then UP / DOWN | Zoom in / out (eight levels) |
|
||||
| F then * | Toggle wire / solid hidden-line rendering (`W` / `S`) |
|
||||
| 0 | Reset the orientation and zoom |
|
||||
| MENU | Pause / resume |
|
||||
| EXIT | Quit |
|
||||
|
||||
The status bar shows the shape name, the armed `F`, and `PAUSE` when stopped.
|
||||
@@ -0,0 +1,43 @@
|
||||
/* Overlay-app link script (POC).
|
||||
*
|
||||
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
|
||||
* the multiboot RAM stub — never both active at once). Code + rodata + data +
|
||||
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
|
||||
* data only; the loader zeroes the overlay before copying, so bss starts clean.
|
||||
*
|
||||
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
|
||||
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
|
||||
* mismatch. */
|
||||
|
||||
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
|
||||
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
|
||||
* The default is only a fallback for a standalone size check. */
|
||||
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
|
||||
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
|
||||
|
||||
ENTRY(app_main)
|
||||
|
||||
MEMORY {
|
||||
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
|
||||
}
|
||||
|
||||
SECTIONS {
|
||||
.app APP_VMA : {
|
||||
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
|
||||
*(.text .text.*)
|
||||
*(.rodata .rodata.*)
|
||||
. = ALIGN(4);
|
||||
*(.data .data.*)
|
||||
. = ALIGN(4);
|
||||
__app_bss_start = .;
|
||||
*(.bss .bss.* COMMON)
|
||||
. = ALIGN(4);
|
||||
__app_bss_end = .;
|
||||
} > APP
|
||||
|
||||
__app_end = .;
|
||||
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
|
||||
"Cube3D overlay app overflows the 4 KiB overlay")
|
||||
|
||||
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||
}
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
|
||||
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
|
||||
# derived from the directory name; only APP_NAME - the human label baked into
|
||||
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
|
||||
# stripped), so "Broadcast FM" -> BroadcastFM.app.
|
||||
set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||
APP_NAME="Cube3D" # <-- the only per-app line
|
||||
APP_VER="1.2"
|
||||
APP_API_MIN=2
|
||||
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/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
if [ "$BYTES" -gt 4096 ]; then
|
||||
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
|
||||
fi
|
||||
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
|
||||
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
|
||||
@@ -0,0 +1,374 @@
|
||||
/* 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 multiply by
|
||||
* a reciprocal from the assets, exactly equal to the divide it replaces, so no
|
||||
* libgcc division is linked. Two looks, toggled with F then STAR:
|
||||
* - 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 solids, the sine table and the
|
||||
* speeds are read-only assets (gen_assets.py; faces with a uniform outward
|
||||
* winding from an offline convex-hull extractor). They are copied onto the stack
|
||||
* (the sine once, the displayed solid every frame), so none of them occupies the
|
||||
* 4 KiB overlay. The globe is rendered procedurally as a latitude/longitude grid.
|
||||
* Pure compute, no radio.
|
||||
*
|
||||
* Keys: UP/DOWN speed · F+UP/DOWN zoom · 1-9 shape · STAR next shape ·
|
||||
* F+STAR solid/wire · 0 reset view · MENU pause · EXIT quit.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "../app_api.h"
|
||||
#include "cube3d_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define W 128
|
||||
#define H 64
|
||||
#define CX 64 /* projection centre x */
|
||||
#define CY 32 /* projection centre y */
|
||||
/* DIST (camera distance along +z, keeps zc > 0) and FOCAL (field-of-view
|
||||
* scale) come from the assets: the reciprocal table is built for them. */
|
||||
#define MAXV SHAPE_MAXV /* largest vertex count across the solids */
|
||||
|
||||
static const app_api_t *A;
|
||||
static const int16_t *sinq; /* Q14 sine quadrant, on app_main's stack */
|
||||
static const uint16_t *recipq;
|
||||
static int rcx, rsx, rcy, rsy, rcz, rsz;
|
||||
static uint8_t zoomq; /* Q4 screen scale: 16 = 100% */
|
||||
|
||||
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 = sinq[i];
|
||||
return quadrant >= 2u ? -value : value;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/* n / zc, truncated toward zero like C, from zc's reciprocal m: exact for
|
||||
* the projection's range (see gen_assets.py). */
|
||||
static int proj(int n, uint32_t m)
|
||||
{
|
||||
const int q = (int)(((uint32_t)(n < 0 ? -n : n) * m) >> RECIP_SHIFT);
|
||||
return n < 0 ? -q : q;
|
||||
}
|
||||
|
||||
/* Rotate, project and retain rotated depth for hidden-line decisions. */
|
||||
static void project_point(int x, int y, int z, int16_t *px, int16_t *py, int16_t *pz)
|
||||
{
|
||||
int ny = (y * rcx - z * rsx) >> 14;
|
||||
int nz = (y * rsx + z * rcx) >> 14;
|
||||
y = ny; z = nz;
|
||||
int nx = (x * rcy + z * rsy) >> 14;
|
||||
nz = (z * rcy - x * rsy) >> 14;
|
||||
x = nx; z = nz;
|
||||
nx = (x * rcz - y * rsz) >> 14;
|
||||
ny = (x * rsz + y * rcz) >> 14;
|
||||
x = nx; y = ny;
|
||||
|
||||
int zc = z + (int)DIST;
|
||||
if (zc < (int)RECIP_ZMIN) zc = RECIP_ZMIN;
|
||||
if (zc > (int)RECIP_ZMAX) zc = RECIP_ZMAX;
|
||||
const uint32_t m = recipq[zc - (int)RECIP_ZMIN];
|
||||
int qx = proj(x * (int)FOCAL, m), qy = proj(y * (int)FOCAL, m);
|
||||
qx = qx < 0 ? -(((-qx) * zoomq) >> 4) : (qx * zoomq) >> 4;
|
||||
qy = qy < 0 ? -(((-qy) * zoomq) >> 4) : (qy * zoomq) >> 4;
|
||||
*px = (int16_t)(CX + qx);
|
||||
*py = (int16_t)(CY + qy);
|
||||
*pz = (int16_t)z;
|
||||
}
|
||||
|
||||
#define GLOBE_R 36
|
||||
#define GLOBE_SCREEN_R 19
|
||||
|
||||
/* Latitude/longitude use the same 256-step turn as sin8(). Longitude zero is
|
||||
* the front of the globe, and positive latitude is drawn upward. */
|
||||
static void globe_point(int8_t lat, int8_t lon, int16_t *x, int16_t *y, int16_t *z)
|
||||
{
|
||||
const uint8_t la = (uint8_t)lat, lo = (uint8_t)lon;
|
||||
const int r = (GLOBE_R * sin8((uint8_t)(la + 64u))) >> 14;
|
||||
const int gx = (r * sin8(lo)) >> 14;
|
||||
const int gy = -((GLOBE_R * sin8(la)) >> 14);
|
||||
const int gz = -((r * sin8((uint8_t)(lo + 64u))) >> 14);
|
||||
project_point(gx, gy, gz, x, y, z);
|
||||
}
|
||||
|
||||
static void globe_edge(int16_t x0, int16_t y0, int16_t z0,
|
||||
int16_t x1, int16_t y1, int16_t z1, bool wire)
|
||||
{
|
||||
if (z0 <= 0 && z1 <= 0)
|
||||
draw_edge(x0, y0, x1, y1, false);
|
||||
else if (wire && z0 >= 0 && z1 >= 0)
|
||||
draw_edge(x0, y0, x1, y1, true);
|
||||
}
|
||||
|
||||
static void draw_globe(bool wire)
|
||||
{
|
||||
int16_t x0, y0, z0, x1, y1, z1;
|
||||
|
||||
/* Three parallels, each split into 16 short segments. */
|
||||
for (int8_t lat = -32; lat <= 32; lat += 32) {
|
||||
globe_point(lat, 0, &x0, &y0, &z0);
|
||||
for (uint16_t lon = 16; lon <= 256u; lon += 16u) {
|
||||
globe_point(lat, (int8_t)lon, &x1, &y1, &z1);
|
||||
globe_edge(x0, y0, z0, x1, y1, z1, wire);
|
||||
x0 = x1; y0 = y1; z0 = z1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Eight pole-to-pole half-meridians make four complete great circles. */
|
||||
for (uint16_t lon = 0; lon < 256u; lon += 32u) {
|
||||
globe_point(-64, (int8_t)lon, &x0, &y0, &z0);
|
||||
for (int8_t lat = -48; lat <= 64; lat += 16) {
|
||||
globe_point(lat, (int8_t)lon, &x1, &y1, &z1);
|
||||
globe_edge(x0, y0, z0, x1, y1, z1, wire);
|
||||
x0 = x1; y0 = y1; z0 = z1;
|
||||
}
|
||||
}
|
||||
|
||||
/* A stable limb keeps the globe legible when most grid lines are edge-on. */
|
||||
const int limb = (GLOBE_SCREEN_R * zoomq) >> 4;
|
||||
x0 = (int16_t)(CX + limb); y0 = CY;
|
||||
for (uint16_t a = 8; a <= 256u; a += 8u) {
|
||||
x1 = (int16_t)(CX + ((limb * sin8((uint8_t)(a + 64u))) >> 14));
|
||||
y1 = (int16_t)(CY + ((limb * sin8((uint8_t)a)) >> 14));
|
||||
draw_edge(x0, y0, x1, y1, false);
|
||||
x0 = x1; y0 = y1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Signed area of a packed face's projected polygon (<0 == facing us). */
|
||||
static int face_area(const uint8_t *f, const int16_t *px, const int16_t *py)
|
||||
{
|
||||
const uint8_t n = f[0];
|
||||
const uint8_t *v = f + 1;
|
||||
int sa = 0;
|
||||
for (uint8_t k = 0; k < n; k++) {
|
||||
const uint8_t a = v[k];
|
||||
const uint8_t b = v[(k + 1u == 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();
|
||||
|
||||
/* Assets copied to the stack: shared tables once, the displayed solid every
|
||||
* frame (nv, nf, name + NUL, vertices, packed faces; see gen_assets.py). */
|
||||
int16_t sin_q[SIN_Q_LEN / 2u];
|
||||
uint16_t recip[RECIP_LEN / 2u]; /* by zc - RECIP_ZMIN */
|
||||
char pause[T_PAUSE_LEN];
|
||||
uint8_t rec[SHAPE_REC_MAX];
|
||||
uint8_t nshape;
|
||||
int16_t px[MAXV], py[MAXV], pz[MAXV]; /* projected x, y and rotated depth */
|
||||
A->asset_read(SIN_Q, sin_q, sizeof(sin_q));
|
||||
A->asset_read(RECIP, recip, sizeof(recip));
|
||||
A->asset_read(T_PAUSE, pause, sizeof(pause));
|
||||
sinq = sin_q;
|
||||
recipq = recip;
|
||||
if (A->asset_read(SHAPES, &nshape, 1) != 1u || nshape == 0u)
|
||||
return;
|
||||
|
||||
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 */
|
||||
uint8_t zoom = 4; /* 1..8: 62.5%..150%, level 4 = 100% */
|
||||
bool paused = false;
|
||||
bool wire = true; /* false = solid (hidden-line) */
|
||||
bool fArm = false; /* F: the next UP/DOWN or STAR is modified */
|
||||
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();
|
||||
if (key == APP_KEY_F) {
|
||||
fArm = !fArm;
|
||||
prevKey = key;
|
||||
continue;
|
||||
}
|
||||
const bool fn = fArm;
|
||||
fArm = false;
|
||||
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 (fn) {
|
||||
if (dir > 0 && zoom < 8u) zoom++;
|
||||
if (dir < 0 && zoom > 1u) zoom--;
|
||||
} else {
|
||||
if (dir > 0 && speed < 16u) speed++;
|
||||
if (dir < 0 && speed > 1u) speed--;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case APP_KEY_MENU:
|
||||
paused = !paused;
|
||||
break;
|
||||
case APP_KEY_STAR: /* next shape, wrapping without a modulo */
|
||||
if (fn)
|
||||
wire = !wire;
|
||||
else if (++shape >= nshape)
|
||||
shape = 0;
|
||||
break;
|
||||
case APP_KEY_0:
|
||||
ax = ay = az = 0;
|
||||
zoom = 4;
|
||||
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:
|
||||
case APP_KEY_9:
|
||||
if ((uint8_t)(key - APP_KEY_1) < nshape)
|
||||
shape = (uint8_t)(key - APP_KEY_1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
prevKey = key;
|
||||
if (!running)
|
||||
break;
|
||||
|
||||
uint8_t entry[3]; /* directory: u16 record offset, u8 record length */
|
||||
A->asset_read(SHAPES + 1u + 3u * shape, entry, sizeof(entry));
|
||||
const uint8_t reclen = entry[2] < sizeof(rec) ? entry[2] : (uint8_t)sizeof(rec);
|
||||
A->asset_read(SHAPES + (uint16_t)(entry[0] | (entry[1] << 8)), rec, reclen);
|
||||
const uint8_t nv = rec[0] < MAXV ? rec[0] : MAXV;
|
||||
const uint8_t nf = rec[1];
|
||||
uint8_t namelen = 0;
|
||||
while (2u + namelen < reclen && rec[2u + namelen])
|
||||
namelen++;
|
||||
const int8_t (*verts)[3] = (const int8_t (*)[3])&rec[3u + namelen];
|
||||
const uint8_t ia = (uint8_t)(ax >> 3), ib = (uint8_t)(ay >> 3), ic = (uint8_t)(az >> 3);
|
||||
zoomq = (uint8_t)(8u + 2u * zoom);
|
||||
rcx = sin8((uint8_t)(ia + 64u)); rsx = sin8(ia);
|
||||
rcy = sin8((uint8_t)(ib + 64u)); rsy = sin8(ib);
|
||||
rcz = sin8((uint8_t)(ic + 64u)); rsz = sin8(ic);
|
||||
|
||||
for (uint8_t i = 0; i < nv; i++) {
|
||||
project_point(verts[i][0], verts[i][1], verts[i][2],
|
||||
&px[i], &py[i], &pz[i]);
|
||||
}
|
||||
|
||||
clear_screen();
|
||||
if (nv == 0u && nf == 0u) {
|
||||
draw_globe(wire);
|
||||
} else {
|
||||
const uint8_t *f = &rec[3u + namelen + 3u * rec[0]]; /* packed faces */
|
||||
for (uint8_t i = 0; i < nf; i++) {
|
||||
const uint8_t n = f[0];
|
||||
if (wire || face_area(f, px, py) < 0) { /* else hidden face culled */
|
||||
for (uint8_t k = 0; k < n; k++) {
|
||||
const uint8_t a = f[1u + k];
|
||||
const uint8_t b = f[(k + 1u == n) ? 1u : 2u + k];
|
||||
const bool dotted = wire && (pz[a] + pz[b] > 0); /* far half */
|
||||
draw_edge(px[a], py[a], px[b], py[b], dotted);
|
||||
}
|
||||
}
|
||||
f += 1u + n;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shape name: inverse label, top-left of the status bar (scan-list look). */
|
||||
const uint8_t end = (uint8_t)(2u + 4u * namelen);
|
||||
for (uint8_t i = 0; i <= end; i++)
|
||||
A->status_line[i] = 0;
|
||||
A->print_inverse((const char *)&rec[2], 2, 0, true, true, end);
|
||||
if (fArm)
|
||||
A->asset_read(BMP_F, A->status_line + 70, BMP_F_LEN);
|
||||
if (paused)
|
||||
A->print_inverse(pause, 82, 0, true, true, 104);
|
||||
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
|
||||
if (!paused) {
|
||||
uint8_t rate;
|
||||
A->asset_read(ROT_RATE + speed - 1u, &rate, 1);
|
||||
ax += rate;
|
||||
ay += (uint16_t)(rate + (rate >> 1));
|
||||
az += (uint16_t)((rate + 1u) >> 1);
|
||||
}
|
||||
|
||||
A->backlight_update();
|
||||
A->delay_ms((uint32_t)(32u - speed * 2u)); /* slow low end, no added delay at level 16 */
|
||||
}
|
||||
}
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
# Cube3D read-only assets: the solids, Q14 sine quadrant and speeds.
|
||||
#
|
||||
# SHAPES layout (offsets relative to SHAPES):
|
||||
# 0 count N
|
||||
# 1 + 3*k solid k directory entry: u16 record offset, u8 record length
|
||||
# record nv, nf, name + NUL, nv x (x, y, z) int8, then nf packed faces
|
||||
# (n, v0 .. v{n-1}), each wound CCW as seen from outside
|
||||
# The faces were generated offline by a convex-hull extractor, so every solid
|
||||
# shares the same outward winding and the signed-area cull sign. SHAPE_MAXV and
|
||||
# SHAPE_REC_MAX size the app's buffers, so adding a solid needs no C change.
|
||||
#
|
||||
# ./gen_assets.py cube3d_assets.bin cube3d_assets.h
|
||||
import math, os, struct, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
|
||||
MAXN = 6 # largest face polygon
|
||||
|
||||
SHAPES = [
|
||||
("CUBE",
|
||||
[(-26,-26,-26),(26,-26,-26),(26,26,-26),(-26,26,-26),(-26,-26,26),(26,-26,26),(26,26,26),(-26,26,26)],
|
||||
[(2,1,0,3),(4,0,1,5),(7,3,0,4),(5,1,2,6),(6,2,3,7),(7,4,5,6)]),
|
||||
("OCTAHEDRON",
|
||||
[(38,0,0),(-38,0,0),(0,38,0),(0,-38,0),(0,0,38),(0,0,-38)],
|
||||
[(4,0,2),(2,0,5),(3,0,4),(5,0,3),(2,1,4),(5,1,2),(4,1,3),(3,1,5)]),
|
||||
("TETRAHEDRON",
|
||||
[(28,28,28),(28,-28,-28),(-28,28,-28),(-28,-28,28)],
|
||||
[(2,0,1),(1,0,3),(3,0,2),(2,1,3)]),
|
||||
("DIAMOND",
|
||||
[(30,0,0),(14,26,0),(-14,26,0),(-30,0,0),(-14,-26,0),(14,-26,0),(0,0,40),(0,0,-40)],
|
||||
[(6,0,1),(1,0,7),(5,0,6),(7,0,5),(6,1,2),(2,1,7),(6,2,3),(3,2,7),(6,3,4),(4,3,7),(6,4,5),(5,4,7)]),
|
||||
("ICOSAHEDRON",
|
||||
[(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)],
|
||||
[(8,0,2),(2,0,10),(6,0,4),(4,0,8),(10,0,6),(3,1,9),(11,1,3),(4,1,6),(9,1,4),(6,1,11),
|
||||
(5,2,7),(8,2,5),(7,2,10),(7,3,5),(5,3,9),(11,3,7),(9,4,8),(8,5,9),(10,6,11),(11,7,10)]),
|
||||
("CUBOCTA",
|
||||
[(-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)],
|
||||
[(4,0,5,1),(2,9,0,8),(8,0,4),(5,0,9),(10,1,11,3),(4,1,10),(11,1,5),(3,7,2,6),(6,2,8),(9,2,7),(10,3,6),(7,3,11),(8,4,10,6),(7,11,5,9)]),
|
||||
("HEXPRISM",
|
||||
[(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)],
|
||||
[(4,5,0,1,2,3),(7,1,0,6),(6,0,5,11),(8,2,1,7),(9,3,2,8),(10,4,3,9),(11,5,4,10),(9,8,7,6,11,10)]),
|
||||
("PENTAGEM",
|
||||
[(28,0,0),(8,26,0),(-22,16,0),(-22,-16,0),(8,-26,0),(0,0,42),(0,0,-42)],
|
||||
[(5,0,1),(1,0,6),(4,0,5),(6,0,4),(5,1,2),(2,1,6),(5,2,3),(3,2,6),(5,3,4),(4,3,6)]),
|
||||
# Empty geometry selects the procedural globe renderer in cube3d_app.c.
|
||||
("GLOBE", [], []),
|
||||
("DODECA",
|
||||
[(-20,-20,-20),(-20,-20,20),(-20,20,-20),(-20,20,20),
|
||||
(20,-20,-20),(20,-20,20),(20,20,-20),(20,20,20),
|
||||
(0,-12,-32),(0,-12,32),(0,12,-32),(0,12,32),
|
||||
(-12,-32,0),(-12,32,0),(12,-32,0),(12,32,0),
|
||||
(-32,0,-12),(-32,0,12),(32,0,-12),(32,0,12)],
|
||||
[(17,16,0,12,1),(10,8,0,16,2),(14,12,0,8,4),(3,17,1,9,11),
|
||||
(5,9,1,12,14),(3,13,2,16,17),(6,10,2,13,15),(15,13,3,11,7),
|
||||
(5,14,4,18,19),(6,18,4,8,10),(11,9,5,19,7),(19,18,6,15,7)]),
|
||||
]
|
||||
|
||||
# One Q14 sine quadrant: symmetry recovers the full 256-step wave.
|
||||
SIN_Q = [
|
||||
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,
|
||||
]
|
||||
|
||||
# Quarter-half-units per frame for speed levels 1..16. The low end has
|
||||
# fractional angular steps; level 16 reaches 16 half-units/frame once divided
|
||||
# by four.
|
||||
ROT_RATE = [1, 2, 3, 4, 6, 8, 10, 12, 16, 20, 24, 30, 36, 44, 52, 64]
|
||||
|
||||
# Perspective projection without a division: for n = x * FOCAL and
|
||||
# zc = z + DIST, the C expression n / zc (truncating toward zero) equals
|
||||
# sign(n) * ((|n| * RECIP[zc - RECIP_ZMIN]) >> RECIP_SHIFT) exactly, with
|
||||
# RECIP[d] = 2^RECIP_SHIFT // d + 1, as long as |n| < 2^RECIP_SHIFT / zc (the
|
||||
# error term then stays below 1 / zc). The zc range covers every solid's
|
||||
# largest radius, plus a margin for the Q14 rotation rounding.
|
||||
DIST, FOCAL = 150, 80
|
||||
RECIP_SHIFT = 20
|
||||
R = int(max(math.sqrt(x * x + y * y + z * z) for _, v, _ in SHAPES for x, y, z in v)) + 4
|
||||
RECIP_ZMIN, RECIP_ZMAX = DIST - R, DIST + R
|
||||
RECIP = [(1 << RECIP_SHIFT) // d + 1 for d in range(RECIP_ZMIN, RECIP_ZMAX + 1)]
|
||||
if FOCAL * R >= (1 << RECIP_SHIFT) // RECIP_ZMAX or FOCAL * R * RECIP[0] >= 1 << 31 or RECIP[0] > 0xFFFF:
|
||||
sys.exit("RECIP: a solid is too large for an exact division-free projection")
|
||||
|
||||
def record(name, verts, faces):
|
||||
out = bytes([len(verts), len(faces)]) + name.encode("ascii") + b"\x00"
|
||||
for v in verts:
|
||||
out += struct.pack("<3b", *v)
|
||||
for f in faces:
|
||||
if not 3 <= len(f) <= MAXN or any(i >= len(verts) for i in f):
|
||||
sys.exit(f"{name}: bad face {f}")
|
||||
out += bytes([len(f), *f])
|
||||
if len(out) > 255:
|
||||
sys.exit(f"{name}: record {len(out)} B exceeds 255 B")
|
||||
return out
|
||||
|
||||
records = [record(*s) for s in SHAPES]
|
||||
base = 1 + 3 * len(records)
|
||||
shapes = bytes([len(records)])
|
||||
body = b""
|
||||
for r in records:
|
||||
shapes += struct.pack("<HB", base + len(body), len(r))
|
||||
body += r
|
||||
|
||||
a = Assets("CUBE3D")
|
||||
a.text("T_PAUSE", "PAUSE")
|
||||
a.raw("SHAPES", shapes + body)
|
||||
a.i16("SIN_Q", SIN_Q)
|
||||
a.u8("ROT_RATE", ROT_RATE)
|
||||
a.u16("RECIP", RECIP)
|
||||
a.u8("BMP_F", [0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e])
|
||||
a.const("RECIP_ZMIN", RECIP_ZMIN)
|
||||
a.const("RECIP_ZMAX", RECIP_ZMAX)
|
||||
a.const("RECIP_SHIFT", RECIP_SHIFT)
|
||||
a.const("DIST", DIST)
|
||||
a.const("FOCAL", FOCAL)
|
||||
a.const("SHAPE_MAXV", max(len(v) for _, v, _ in SHAPES))
|
||||
a.const("SHAPE_REC_MAX", max(len(r) for r in records))
|
||||
a.main()
|
||||
@@ -0,0 +1,318 @@
|
||||
# EPIRB 406: on-radio 406 MHz beacon decoder (work in progress)
|
||||
|
||||
Goal: an overlay app that decodes first-generation Cospas-Sarsat 406 MHz beacon
|
||||
messages directly on the UV-K1 / UV-K5 v3 and shows the 15-hex ID, country,
|
||||
protocol, position and BCH status.
|
||||
|
||||
Status:
|
||||
|
||||
| Step | State |
|
||||
|---|---|
|
||||
| Feasibility: can an app read the demodulated signal? | **Done**: yes, on PA4 (see `../lab406/README.md`) |
|
||||
| Decoder core (`dec406.c`), host-tested on synthetic audio | **Done**: 17/17 tests pass |
|
||||
| Radio app (`epirb406_app.c`: trigger, sampling, display) | **v1.4 decodes the full reference frame on the K1** (position, `END F58521EDA3`); v1.6 (assets, 3,320 bytes) decodes the Flipper Zero test frames on the radio, and the rpitx bench frame on a UV-K1 (F4WAT, 2026-10-01); v1.7: 3,284 bytes |
|
||||
| Bench test with the beacon generator on 433.650 MHz | **Done** (2026-09-27) |
|
||||
| Bench test with a Flipper Zero on 433.650 MHz (`test/flipper406.py`) | **Done** (2026-09-30, v1.6) |
|
||||
|
||||
## Using the app
|
||||
|
||||
1. Set the VFO to the beacon frequency, **FM, wide bandwidth**: 406.031 MHz for
|
||||
real beacons, or the generator frequency (433.650 MHz on the bench).
|
||||
2. Launch **EPIRB 406** with no burst in progress: the noise floor is measured at
|
||||
launch.
|
||||
3. Lower the volume: the RX audio must stay on (it is what reaches PA4), so the
|
||||
speaker plays each burst.
|
||||
|
||||
Each burst is detected on RSSI (10 dB above the floor), sampled for up to 900 ms
|
||||
and decoded. The last decoded message stays on screen (the 2-message history was
|
||||
dropped in v1.5 to make room for tuning).
|
||||
|
||||
| Screen | Content |
|
||||
|---|---|
|
||||
| Line 0 | 15-hex ID |
|
||||
| Line 1 | Country code and protocol |
|
||||
| Line 2 | Position (5 decimals, truncated) or "no position" |
|
||||
| Line 3 | Tuned frequency and offset from the VFO, e.g. `433.645 -5 kHz` (v1.5; up to v1.4: `END` + raw bits 105-144) |
|
||||
| Small rows | SELF-TEST, LONG/SHORT, BCH-1/BCH-2; `#` decode number, RSSI of the burst, internal/external position source, 121.5 homing, `coarse` (no PDF-2 offsets), `rawID` (not a standard location protocol, ID = raw bits 26-85) |
|
||||
| Bottom row | RSSI / floor, decodes ok, errors, last error: `nosync` (no frame sync found) or `cut` (sync found, message incomplete) |
|
||||
|
||||
Keys (UV-K5 and UV-K1):
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| UP / DOWN | Tune ±5 kHz from the VFO, up to ±50 kHz (direction follows the firmware's navigation setting, so K1 LEFT/RIGHT work as on the main screen) |
|
||||
| 5 | Back to the VFO frequency |
|
||||
| MENU | Clear the last message and the counters |
|
||||
| EXIT | Quit (the firmware retunes to the VFO frequency) |
|
||||
|
||||
Tuning writes the BK4829 frequency registers (0x38/0x39, 10 Hz units) and
|
||||
restarts the synthesizer (REG_30 to 0, then back) so it relocks; it only lasts
|
||||
while the app runs. Useful when the carrier sits near the edge of the channel: on
|
||||
the bench, the full frame was only received with the K1 5 kHz below the
|
||||
generator. The backlight stays on while the app runs (since v1.4). The
|
||||
decoder integrates the discriminator pulses (INT); the INT/DIR toggle of v1.1-v1.2
|
||||
(key 1) was removed in v1.3 once INT was proven on the radio.
|
||||
|
||||
Space: v1.5 used 4,052 of the 4,096 bytes. To make it fit, the sync search
|
||||
compares 32-bit words (the inverted-polarity distance is 44 minus the normal
|
||||
one), number formatting uses subtraction instead of division (no `__udivsi3`),
|
||||
only the last message is kept, and the app is built with `-fno-jump-tables`
|
||||
(48 bytes saved on the key `switch`).
|
||||
|
||||
v1.6 moves the screen texts and the 24 protocol names to read-only assets
|
||||
(`gen_assets.py`, API level 2, 432 bytes outside the overlay). Measured: the app
|
||||
went from 4,052 to **3,320 bytes, leaving 776 bytes free** (44 before). `draw()` reads the UI texts in one
|
||||
`asset_read` into a word-aligned stack block, so each text costs a 2-byte
|
||||
sp-relative add instead of a literal-pool load and its pool word; each text is
|
||||
padded to 4 bytes for that. A protocol name is read straight into the line
|
||||
buffer. The names are parsed from `NAMES` in `dec406.c`, so the host test and the
|
||||
app share one table (`dec406_proto_name` is dropped from the app by
|
||||
`--gc-sections`). The hex digits of the ID are computed instead of taken from a
|
||||
table. The app state is grouped in one struct (one base address per function
|
||||
instead of a literal-pool word per global), the dead `seq`/`inv` copies of the
|
||||
last message are gone, and the zeroing already done by the loader, the ADC
|
||||
restore already done after each capture and the backlight call already made
|
||||
every loop are no longer repeated.
|
||||
|
||||
v1.7 counts the sync-pattern bits with a loop (one pass per set bit) instead of
|
||||
the branch-free version: 36 bytes less, **3,284 bytes, 812 free**. The cost is
|
||||
at most 4 x 32 passes per half-bit (every 12 samples), and only while searching
|
||||
for the sync, against 5,000 cycles per sample; host tests 29/29.
|
||||
|
||||
## Signal path on the radio
|
||||
|
||||
```
|
||||
beacon RF --> BK4829 (RAW: no HPF300 / LPF3K / de-emphasis, AFC off)
|
||||
--> AF output (FM) --> audio circuit --> PA4 --> ADC channel 4 @ 9.6 kHz
|
||||
```
|
||||
|
||||
- **RAW receive** is set by the app itself (REG_2B bits 10/9/8, REG_73 bit 4), as
|
||||
`BK4819_EnterRaw` does. The firmware restores all radio registers on app exit.
|
||||
- **PA4** is the voice-prompt DAC pin. Voice is disabled in every preset, so the
|
||||
app switches the DAC off and reads PA4 as an analog input. Measured on the K1:
|
||||
idle 518, message range 165-894 (about 0.6 V p-p), no clipping.
|
||||
- The receive audio must be on (AF output enabled, audio path on): the speaker
|
||||
plays the burst.
|
||||
- **PA4 bias**: the pin has no DC reference; the app holds it at mid-scale with
|
||||
the MCU DAC (output buffer off, code 2048) for the whole run. The volume knob is analog; whether it changes the PA4 level
|
||||
still has to be checked.
|
||||
- Tuning: first-generation channels span 406.025-406.040 MHz. One VFO at
|
||||
406.031 MHz with the wide filter covers them all; a carrier offset only shifts
|
||||
the audio DC level, which the decoder removes.
|
||||
|
||||
## First-generation message (C/S T.001)
|
||||
|
||||
400 bps biphase-L, ±1.1 rad phase modulation. About 160 ms of unmodulated carrier,
|
||||
then 112 (short) or 144 (long) bits, one burst about every 50 s.
|
||||
|
||||
| Bits | Content |
|
||||
|---|---|
|
||||
| 1-15 | Bit sync, all ones |
|
||||
| 16-24 | Frame sync: `000101111` normal, `011010000` self-test |
|
||||
| 25 | Format flag: 1 = long |
|
||||
| 26 | Protocol flag: 0 = location protocols, 1 = user protocols |
|
||||
| 27-36 | Country code (France 227/228) |
|
||||
| 37-40 | Protocol code (37-39 for user protocols) |
|
||||
| 41-85 | Identification and coarse position (PDF-1) |
|
||||
| 86-106 | BCH-1, BCH(82,61) over bits 25-106 |
|
||||
| 107-132 | Fine position offsets and flags (PDF-2, long messages) |
|
||||
| 133-144 | BCH-2, BCH(26,14) over bits 107-144 |
|
||||
|
||||
Standard location protocols (codes 0010-0111, 1100, 1110): bits 41-64
|
||||
identification, 65-85 coarse position (N/S, degrees, 15' steps), 107-110 `1101`,
|
||||
111 position source, 112 121.5 MHz homing, 113-132 offsets (±minutes, 4 s steps).
|
||||
The 15-hex ID is bits 26-85 with the position replaced by the default pattern.
|
||||
|
||||
## Decoder (`dec406.c`)
|
||||
|
||||
Streaming, one call per ADC sample, integer only, no division per sample,
|
||||
freestanding (no libc). About 150 bytes of state.
|
||||
|
||||
1. **DC removal**: exponential tracker, fast while searching (~13 ms, so the
|
||||
noise before the burst does not bias it), slow once locked (~107 ms).
|
||||
2. **Phase rebuild**: the discriminator outputs dphi/dt, a pulse per phase flip.
|
||||
A leaky integrator (~1.7 ms) rebuilds the ±1.1 rad biphase waveform.
|
||||
3. **Clock recovery**: a DPLL (gain 1/8) aligns 12-sample half-bit slots on the
|
||||
zero crossings of the rebuilt waveform.
|
||||
4. **Sync search**: the signs of the last 44 half-bits are compared with 13
|
||||
preamble ones + frame sync, normal and self-test, in both polarities (the
|
||||
receive chain may invert). Up to 2 mismatches allowed.
|
||||
5. **Bits**: soft decision per bit, first half minus second half.
|
||||
6. **Parsing**: BCH-1 and BCH-2 syndromes, country, protocol, 15-hex ID, position
|
||||
for standard location protocols (coarse + fine offsets).
|
||||
|
||||
Size for Cortex-M0+ (firmware toolchain, `-Os`): 1,468 bytes of code, after the
|
||||
size work described above.
|
||||
|
||||
## Tests (`test/`)
|
||||
|
||||
```
|
||||
test/run_tests.sh
|
||||
```
|
||||
|
||||
- `frame406.py` builds frames: the bench generator's reference frame (long,
|
||||
standard location test, country 227, ID `1C7C2468ACFFBFF`), a self-test
|
||||
variant, a short message, or a frame with one bit flipped.
|
||||
- `gen406.py` synthesizes what PA4 sees: biphase phase modulation with
|
||||
raised-cosine transitions, carrier offset, noise in a 25 kHz channel, FM
|
||||
discriminator, audio low-pass, AC coupling, 9.6 kHz ADC with clock error,
|
||||
12-bit around the measured 518 bias, full-scale noise when no carrier.
|
||||
- `host_dec406.c` runs the decoder over the samples and prints each message.
|
||||
|
||||
Results (2026-09-27): 29/29 pass. The first 17 cases, covering CNR 12-15 dB, ±5 kHz offset,
|
||||
inverted chain, ±3000 ppm clock error, 50-250 µs rise time, 3 kHz audio
|
||||
low-pass, 150 Hz AC coupling, a combined worst case, self-test, short message,
|
||||
a corrupted bit (BCH-1 reported as failed) and 3 bursts in a row.
|
||||
|
||||
Sensitivity (5 seeds per point, CNR in 25 kHz):
|
||||
|
||||
| CNR | 12 dB | 10 dB | 9 dB | 8 dB | 7 dB | 6 dB |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Decoded, BCH ok | 5/5 | 4/5 | 4/5 | 3/5 | 1/5 | 0/5 |
|
||||
|
||||
Possible gains later: BCH error correction (BCH-1 corrects up to 3 bit errors,
|
||||
BCH-2 up to 2), and a 12.5 kHz filter when the beacon channel is known (+3 dB).
|
||||
|
||||
## Flipper Zero test transmitter (`test/flipper406.py`)
|
||||
|
||||
Without the rpitx generator, a Flipper Zero is enough to test the app on the bench.
|
||||
It cannot phase-modulate, so each biphase-L half-bit is sent as one 2-FSK tone
|
||||
(preset `2FSKDev238Async`, +/-2.38 kHz, Sub-GHz RAW durations of 1,250 us, 160 ms
|
||||
of lead tone, 20 ms of tail). The receiver's discriminator then outputs the
|
||||
biphase waveform itself instead of the pulses of a real beacon, and the app's
|
||||
leaky integrator still tracks its sign: checked first with a pure-Python model of
|
||||
the receive chain and dec406 (exact frame down to ~2.5 kHz rms of discriminator
|
||||
noise, +/-4 kHz offset, 2000 ppm clock error), then on the radio.
|
||||
|
||||
```
|
||||
test/flipper406.py test/flipper
|
||||
```
|
||||
|
||||
writes `epirb406_long.sub` (reference frame), `_selftest`, `_short` and
|
||||
`_bch_err` (bit 50 flipped, BCH-1 fails), on 433.650 MHz (433 MHz SRD band). Copy
|
||||
them to `subghz/` on the Flipper, set the K1 to 433.650 MHz FM wide, launch the
|
||||
app, then Send one file per burst, at least 2 s apart. If the Flipper's crystal
|
||||
puts the carrier off the channel, tune with UP/DOWN. Never transmit these files
|
||||
on 406.0-406.1 MHz.
|
||||
|
||||
## Bench results
|
||||
|
||||
**2026-09-27, UV-K1, generator on 433.650 MHz, DIR mode, first on-air decode.**
|
||||
Sync found, but the message is wrong in a systematic way:
|
||||
|
||||
| | Sent | Received |
|
||||
|---|---|---|
|
||||
| 15-hex ID | `1C7C2468ACFFBFF` | `0C3C002004FFBFF` |
|
||||
| Country | 227 | 97 |
|
||||
| Position | 49.27111 N 0.78222 E | 16.00000 N 0.25000 E (coarse) |
|
||||
| BCH | ok / ok | ERR / ERR |
|
||||
|
||||
Over bits 26-64: every `1` preceded by a `0` is read as `0` (9/9), every `1`
|
||||
preceded by a `1` is correct (8/8), no `0` is ever wrong. Timing and sync are
|
||||
therefore right; the decision fails where biphase-L has no transition at the bit
|
||||
boundary (`0` then `1`), which depends on the real signal shape at PA4.
|
||||
|
||||
None of the simulated chains reproduce this exact pattern (pulse or de-emphasized
|
||||
signal, low-pass, AC coupling, inverted: INT always decodes). Simulated ADC
|
||||
clipping at 0 (PA4 bias is only 518/4095) does break INT decoding entirely, which
|
||||
may explain why DIR was in use.
|
||||
|
||||
**Diagnosis with 406 Lab scope mode (v1.1-v1.5).** Sampled at 9.6 kHz, PA4 slid
|
||||
towards 0 V as soon as fast sampling started, and about half of the message
|
||||
samples were clipped at 0 whatever the AF gain or RAW setting (details in
|
||||
`../lab406/README.md`). PA4 has no DC reference of its own: the audio reaches it
|
||||
through a coupling capacitor, and fast ADC sampling drags the node down.
|
||||
|
||||
**Fix.** The MCU DAC on PA4, output buffer off, set to mid-scale (2048), holds
|
||||
the pin at about VDD/2 as a weak bias. Measured with it (AF gain 14, RAW):
|
||||
carrier 2048-2049, message 1896-2194, no clipping. EPIRB 406 v1.2 turns this bias
|
||||
on at launch and restores the DAC, its clock and the pin on exit. The host test
|
||||
generator now uses the measured bias (2048) and level (~150 LSB peak); 17/17
|
||||
still pass.
|
||||
|
||||
**v1.2 on the bench (INT mode):** ID `1C7C2468ACFFBFF`, country 227, "Std test",
|
||||
BCH-1 OK: the first block (bits 25-106) is received intact and BCH-1 is confirmed
|
||||
on a real frame. BCH-2 fails, so the fine position offsets and the homing flag
|
||||
are not applied (`49.25000N 0.75000E coarse`, no `121.5`). Either BCH-2 is
|
||||
computed differently from the generator, or the last bits of the burst are
|
||||
received with errors. v1.3 shows the raw end of the frame (`END` + bits 105-144
|
||||
as hex) to tell which: an error-free reception of the reference frame reads
|
||||
`END F58521EDA3`.
|
||||
|
||||
**v1.3 on the bench.** The generator sends exactly the reference frame
|
||||
(`FFFE2F8E3E12345631401FB07DF58521EDA3`), so BCH-2 is computed correctly. The
|
||||
received end changed from burst to burst (`F68521E704`, `D5786733DD`): errors
|
||||
growing towards the end of the burst, not a fixed pattern.
|
||||
|
||||
**Root cause: DC tracker rounding.** The mean was updated with `e >> 10`; an
|
||||
arithmetic right shift floors, so every small negative deviation moved the mean
|
||||
down by one step and small positive ones never moved it up. The mean crept
|
||||
downwards during the message, the leaky integrator accumulated the offset, and
|
||||
the last bits went wrong. Negligible with the large swing of the first model,
|
||||
fatal at the ~150 LSB swing measured on PA4. Reproduced on the host at the
|
||||
measured level (BCH-1 ok / BCH-2 fail, 3 of 4 seeds at 30 dB CNR), fixed with
|
||||
round-to-nearest: 4/4 from 30 down to 12 dB, and from -3% to +2% bit-rate error.
|
||||
A second-order (bit-rate tracking) loop was tried and dropped: no gain once the
|
||||
rounding was fixed.
|
||||
|
||||
The test suite now has 12 more cases at the measured level (CNR 12 dB, ±2%
|
||||
clock, 4 noise seeds each): the old decoder fails 6 of them with the radio's
|
||||
symptom, the fixed one passes all 29.
|
||||
|
||||
**Recordings from the radio (406 Rec, 2026-09-27).** Three bursts recorded
|
||||
on the K1 and decoded on the host give exactly the radio's result (sync found,
|
||||
polarity inverted, ID correct, BCH FAIL/FAIL). Activity per 10 ms shows carrier
|
||||
until 150 ms after the trigger, modulation until **410 ms**, then noise: the
|
||||
carrier is gone. 260 ms of modulation instead of 360 ms, so only ~104 of the 144
|
||||
bits are on air, at the same point on every burst; the bit rate itself is exact
|
||||
(the preamble pattern repeats every 24 samples at 9.6 kHz). A synthetic burst cut
|
||||
after 106 bits decodes as BCH ok/FAIL (the v1.2 result) and after 104 bits as
|
||||
FAIL/FAIL with the correct ID (the v1.4 result). **The bench generator (rpitx
|
||||
playing the IQ file) drops the last ~100 ms of the burst**; the decoder is not at
|
||||
fault for the end of the frame. Fix on the generator side: pad the IQ file with
|
||||
at least 150 ms of carrier after the last bit. The recordings are kept outside
|
||||
the repo in `~/dev/uv-k1-406-captures`.
|
||||
|
||||
**Correction.** The generator is not at fault: an RTL-SDR on the Mac decodes the
|
||||
full burst on air. 406 Rec v1.1 (RSSI every 10 ms) showed RSSI falling to the
|
||||
noise floor (about -93 dBm) 410-420 ms after the trigger on every burst: the
|
||||
signal left the K1's channel, most likely a frequency move of the rpitx carrier
|
||||
near the end of the burst that a wideband RTL-SDR decoder follows and a 25 kHz
|
||||
channel does not. (The RSSI read during fast sampling showed a constant -21 dBm,
|
||||
not the -64 to -75 dBm of the trigger; only its change is meaningful. v1.1 is a
|
||||
diagnostic build: the RSSI reads disturb the sample timing.)
|
||||
|
||||
**Full decode on the radio (2026-09-27).** With the K1 VFO **5 kHz below** the
|
||||
generator frequency (433.645 MHz for 433.650), EPIRB 406 v1.4 decoded the whole
|
||||
reference frame: ID `1C7C2468ACFFBFF`, country 227, "Std test", position
|
||||
49.27111 N 0.78222 E, `END F58521EDA3` (all 144 bits intact, BCH-2 ok). This confirms the explanation above: at
|
||||
433.650 the rpitx carrier moves down near the end of the burst and leaves the
|
||||
channel; 5 kHz lower, it stays inside. A steady carrier offset only shifts the
|
||||
discriminator's DC level, which the decoder removes. Real first-generation
|
||||
beacons have far more stable oscillators, but the offset matters there too: one
|
||||
VFO at 406.031 MHz with the wide filter must cover 406.025-406.040.
|
||||
|
||||
The earlier fixes stand on their own: the PA4 bias (v1.2) removed the clipping
|
||||
that caused the "every 1 after a 0" errors, and the DC tracker rounding (v1.4) is
|
||||
a real bias reproduced on the host at the measured level.
|
||||
|
||||
## Open points
|
||||
|
||||
- **Default position pattern** in the 15-hex ID (`0 1111111 11 0 11111111 11`)
|
||||
follows the reference decoder; to confirm against T.001.
|
||||
- **BCH generators** are the T.001 ones. They are only checked against frames
|
||||
built with the same code; the bench generator's real frame
|
||||
(`FFFE2F8E3E12345631401FB07DF58521EDA3` expected) will confirm them.
|
||||
- **Protocol name table** to check against T.001; national location, RLS and
|
||||
ELT-DT positions are not decoded, and their ID is shown as raw bits 26-85.
|
||||
- The synthetic chain is a model: the first real captures from PA4 may need the
|
||||
integrator or DC constants retuned (`dec406_init(..., integrate)` also allows a
|
||||
phase-like input if the hardware turns out to integrate already).
|
||||
- Second-generation beacons (spread-spectrum OQPSK) are out of scope.
|
||||
|
||||
## Version
|
||||
|
||||
`APP_VER` in `build.sh` is bumped for every build that goes on a radio. It is
|
||||
written into the title text of the assets by `gen_assets.py` and shown in the
|
||||
status-bar title (e.g. `v1.1`), since the apps menu does not display versions.
|
||||
Current: **v1.7**.
|
||||
@@ -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,
|
||||
"EPIRB 406 overlay app overflows the 4 KiB overlay")
|
||||
|
||||
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||
}
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/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="EPIRB 406" # <-- the only per-app line
|
||||
APP_VER="1.7"
|
||||
APP_API_MIN=2
|
||||
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 -fno-jump-tables -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/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 assets ; APP_VER="$APP_VER" python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
|
||||
--shortcut none --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
if [ "$BYTES" -gt 4096 ]; then
|
||||
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
|
||||
fi
|
||||
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
|
||||
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
|
||||
@@ -0,0 +1,242 @@
|
||||
/* Copyright 2026 Johan Denoyer F4WAT
|
||||
* https://github.com/jdenoy
|
||||
*
|
||||
* 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 "dec406.h"
|
||||
|
||||
#define SLOT ((int32_t)DEC406_HALF * 256) /* half-bit slot, Q8 samples */
|
||||
#define MEAN_SRCH 7 /* DC tracker while searching: ~13 ms at 9.6 kHz */
|
||||
#define MEAN_DATA 10 /* DC tracker once locked: ~107 ms */
|
||||
#define LEAK_SHR 4 /* phase integrator leak: ~1.7 ms, bounds DC error gain */
|
||||
#define PLL_SHR 3 /* zero-crossing correction gain 1/8 */
|
||||
#define SYNC_TOL 2 /* half-bit mismatches allowed in preamble + sync */
|
||||
|
||||
/* 13 preamble ones + frame sync, as 44 half-bit signs (1 = "10", 0 = "01"),
|
||||
* split in a 12-bit high part and a 32-bit low part */
|
||||
#define PAT_BITS 44u
|
||||
#define PAT_HI_MASK 0xFFFu
|
||||
#define PAT_HI 0xAAAu /* same for both syncs */
|
||||
#define PAT_NORMAL 0xAAA959AAu /* sync 000101111 */
|
||||
#define PAT_SELFTEST 0xAAA9A655u /* sync 011010000 */
|
||||
|
||||
/* BCH generators (C/S T.001): PDF-1 BCH(82,61), PDF-2 BCH(26,14) */
|
||||
#define BCH1_GEN 0x26D9E3u /* x^21+x^18+x^17+x^15+x^14+x^12+x^11+x^8+x^7+x^6+x^5+x+1 */
|
||||
#define BCH2_GEN 0x1539u /* x^12+x^10+x^8+x^5+x^4+x^3+1 */
|
||||
|
||||
/* Standard location default position, bits 65-85, as used for the 15-hex ID.
|
||||
* Matches the reference decoder output for ID 1C7C2468ACFFBFF; to be confirmed
|
||||
* against T.001. */
|
||||
#define STD_DEFAULT_POS 0x0FFBFFu /* 0 1111111 11 0 11111111 11 */
|
||||
|
||||
/* Bit count, one pass per set bit: at most 4 x 32 passes per half-bit (every
|
||||
* 12 samples, and only while searching), far inside the 5000 cycles of a
|
||||
* sample period; 36 bytes smaller than the branch-free version. */
|
||||
static uint8_t popc32(uint32_t x)
|
||||
{
|
||||
uint8_t n = 0;
|
||||
for (; x; x &= x - 1u) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Match the last 44 half-bits against one sync pattern in both polarities:
|
||||
* returns 1 (normal), 2 (inverted) or 0. Inverted distance = 44 - distance. */
|
||||
static uint8_t match(const dec406_t *d, uint32_t lo)
|
||||
{
|
||||
uint8_t n = (uint8_t)(popc32((d->hsrHi ^ PAT_HI) & PAT_HI_MASK) + popc32(d->hsrLo ^ lo));
|
||||
if (n <= SYNC_TOL) return 1;
|
||||
if (n >= PAT_BITS - SYNC_TOL) return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void dec406_rearm(dec406_t *d)
|
||||
{
|
||||
d->lvl = 0; d->acc = 0; d->h1 = 0; d->ph = 0; d->hsrHi = d->hsrLo = 0;
|
||||
d->state = DEC406_SEARCH; d->half = 0; d->inv = 0; d->selftest = 0;
|
||||
d->sign = 0; d->nbits = 0; d->total = 0;
|
||||
for (uint8_t i = 0; i < sizeof d->bits; i++) d->bits[i] = 0;
|
||||
}
|
||||
|
||||
void dec406_init(dec406_t *d, bool integrate)
|
||||
{
|
||||
d->meanQ8 = -1; /* seeded by the first sample */
|
||||
d->integrate = integrate ? 1u : 0u;
|
||||
dec406_rearm(d);
|
||||
}
|
||||
|
||||
static void putBit(dec406_t *d, uint8_t b)
|
||||
{
|
||||
if (b) d->bits[d->nbits >> 3] |= (uint8_t)(0x80u >> (d->nbits & 7u));
|
||||
d->nbits++;
|
||||
}
|
||||
|
||||
static void emitHalf(dec406_t *d, int32_t h)
|
||||
{
|
||||
d->hsrHi = (d->hsrHi << 1) | (d->hsrLo >> 31);
|
||||
d->hsrLo = (d->hsrLo << 1) | (h > 0 ? 1u : 0u);
|
||||
|
||||
if (d->state == DEC406_SEARCH) {
|
||||
uint8_t m = match(d, PAT_NORMAL), st = 0;
|
||||
if (!m) { m = match(d, PAT_SELFTEST); st = 1; }
|
||||
if (m) {
|
||||
d->inv = (uint8_t)(m - 1u); d->selftest = st;
|
||||
d->state = DEC406_DATA; d->half = 0; d->nbits = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (d->state != DEC406_DATA) return;
|
||||
|
||||
if (!d->half) { d->h1 = h; d->half = 1; return; }
|
||||
d->half = 0;
|
||||
/* biphase-L: "1" = high then low, "0" = low then high (soft decision) */
|
||||
uint8_t b = (uint8_t)(((d->h1 - h) > 0) ^ d->inv);
|
||||
putBit(d, b);
|
||||
if (d->nbits == 1) d->total = b ? 120u : 88u; /* bit 25: long / short */
|
||||
if (d->nbits >= d->total) d->state = DEC406_DONE;
|
||||
}
|
||||
|
||||
bool dec406_push(dec406_t *d, uint16_t sample)
|
||||
{
|
||||
if (d->state == DEC406_DONE) return true;
|
||||
|
||||
int32_t xq = (int32_t)sample << 8;
|
||||
if (d->meanQ8 < 0) d->meanQ8 = xq;
|
||||
int32_t e = xq - d->meanQ8;
|
||||
/* round to nearest: a plain >> floors, so small negative errors always moved
|
||||
* the mean down and small positive ones never moved it up; at the ~150 LSB
|
||||
* swing measured on PA4 that downward creep corrupted the end of the frame */
|
||||
uint8_t msh = d->state == DEC406_SEARCH ? MEAN_SRCH : MEAN_DATA;
|
||||
d->meanQ8 += (e + (1 << (msh - 1))) >> msh;
|
||||
int32_t v = e >> 4;
|
||||
|
||||
if (d->integrate) d->lvl += v - (d->lvl >> LEAK_SHR);
|
||||
else d->lvl = v;
|
||||
|
||||
/* DPLL: phase-waveform zero crossings sit on half-bit slot boundaries */
|
||||
uint8_t s = d->lvl > 0 ? 1u : 0u;
|
||||
if (s != d->sign) {
|
||||
int32_t err = d->ph;
|
||||
if (err > SLOT / 2) err -= SLOT;
|
||||
d->ph -= err >> PLL_SHR;
|
||||
if (d->ph < 0) d->ph += SLOT;
|
||||
d->sign = s;
|
||||
}
|
||||
|
||||
d->acc += d->lvl;
|
||||
d->ph += 256;
|
||||
if (d->ph >= SLOT) {
|
||||
d->ph -= SLOT;
|
||||
emitHalf(d, d->acc);
|
||||
d->acc = 0;
|
||||
}
|
||||
return d->state == DEC406_DONE;
|
||||
}
|
||||
|
||||
/* ---- parsing ---- */
|
||||
|
||||
static uint8_t bit(const dec406_t *d, uint8_t n) /* n = 25..144 */
|
||||
{
|
||||
uint8_t i = (uint8_t)(n - 25u);
|
||||
return (uint8_t)((d->bits[i >> 3] >> (7u - (i & 7u))) & 1u);
|
||||
}
|
||||
|
||||
static uint32_t field(const dec406_t *d, uint8_t first, uint8_t len)
|
||||
{
|
||||
uint32_t v = 0;
|
||||
for (uint8_t i = 0; i < len; i++) v = (v << 1) | bit(d, (uint8_t)(first + i));
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Remainder of the codeword spanning bits first..last: 0 when valid. */
|
||||
static uint32_t syndrome(const dec406_t *d, uint8_t first, uint8_t last, uint32_t gen, uint8_t r)
|
||||
{
|
||||
uint32_t reg = 0;
|
||||
for (uint8_t n = first; n <= last; n++) {
|
||||
reg = (reg << 1) | bit(d, n);
|
||||
if (reg >> r) reg ^= gen;
|
||||
}
|
||||
return reg;
|
||||
}
|
||||
|
||||
static bool isStdLoc(uint8_t code)
|
||||
{
|
||||
/* 0010 EPIRB MMSI, 0011 ELT 24-bit, 0100 ELT serial, 0101 ELT op. designator,
|
||||
* 0110 EPIRB serial, 0111 PLB serial, 1100 ship security, 1110 test */
|
||||
return (code >= 2u && code <= 7u) || code == 12u || code == 14u;
|
||||
}
|
||||
|
||||
void dec406_parse(const dec406_t *d, dec406_info_t *o)
|
||||
{
|
||||
|
||||
o->longMsg = bit(d, 25);
|
||||
o->selftest = d->selftest;
|
||||
o->bch1 = syndrome(d, 25, 106, BCH1_GEN, 21) == 0;
|
||||
o->bch2 = o->longMsg ? syndrome(d, 107, 144, BCH2_GEN, 12) == 0 : 1u;
|
||||
o->userProto = bit(d, 26);
|
||||
o->country = (uint16_t)field(d, 27, 10);
|
||||
o->proto = (uint8_t)(o->userProto ? field(d, 37, 3) : field(d, 37, 4));
|
||||
o->stdLoc = !o->userProto && isStdLoc(o->proto);
|
||||
o->idRaw = !o->stdLoc;
|
||||
o->idData = field(d, 41, 24);
|
||||
o->hasPos = o->hasFine = o->internalPos = o->homing = 0;
|
||||
o->latS = o->lonS = 0;
|
||||
|
||||
for (uint8_t k = 0; k < 15; k++) {
|
||||
uint8_t nib = 0;
|
||||
for (uint8_t j = 0; j < 4; j++) {
|
||||
uint8_t n = (uint8_t)(26u + 4u * k + j);
|
||||
uint8_t b = (o->stdLoc && n >= 65u) ? (uint8_t)((STD_DEFAULT_POS >> (85u - n)) & 1u) : bit(d, n);
|
||||
nib = (uint8_t)((nib << 1) | b);
|
||||
}
|
||||
o->id[k] = (char)(nib < 10u ? '0' + nib : 'A' - 10 + nib); /* no hex table */
|
||||
}
|
||||
o->id[15] = '\0';
|
||||
|
||||
if (!o->stdLoc) return;
|
||||
|
||||
if (field(d, 65, 21) != STD_DEFAULT_POS) {
|
||||
int32_t lat = (int32_t)field(d, 66, 7) * 3600 + (int32_t)field(d, 73, 2) * 900;
|
||||
int32_t lon = (int32_t)field(d, 76, 8) * 3600 + (int32_t)field(d, 84, 2) * 900;
|
||||
if (o->longMsg && o->bch2 && field(d, 107, 4) == 0xDu) {
|
||||
uint32_t am = field(d, 114, 5), as = field(d, 119, 4);
|
||||
uint32_t om = field(d, 124, 5), os = field(d, 129, 4);
|
||||
if (am <= 30u && om <= 30u) {
|
||||
int32_t dlat = (int32_t)(am * 60u + as * 4u), dlon = (int32_t)(om * 60u + os * 4u);
|
||||
lat += bit(d, 113) ? dlat : -dlat;
|
||||
lon += bit(d, 123) ? dlon : -dlon;
|
||||
o->hasFine = 1;
|
||||
}
|
||||
}
|
||||
o->latS = bit(d, 65) ? -lat : lat;
|
||||
o->lonS = bit(d, 75) ? -lon : lon;
|
||||
o->hasPos = 1;
|
||||
}
|
||||
if (o->longMsg && o->bch2) { o->internalPos = bit(d, 111); o->homing = bit(d, 112); }
|
||||
}
|
||||
|
||||
const char *dec406_proto_name(const dec406_info_t *in)
|
||||
{
|
||||
/* 16 location protocol names (bits 37-40), then 8 user protocol names
|
||||
* (bits 37-39), packed to avoid a pointer table */
|
||||
static const char NAMES[] =
|
||||
"Loc 0000\0Loc 0001\0EPIRB MMSI\0ELT 24bit\0ELT serial\0ELT opdes\0"
|
||||
"EPIRB ser\0PLB serial\0Nat ELT\0ELT-DT\0Nat EPIRB\0Nat PLB\0"
|
||||
"Ship sec\0RLS\0Std test\0Nat test\0"
|
||||
"Orbito\0ELT avia\0Maritime\0Serial\0National\0Spare\0Callsign\0User test";
|
||||
uint8_t n = in->userProto ? (uint8_t)(16u + (in->proto & 7u)) : (uint8_t)(in->proto & 15u);
|
||||
const char *p = NAMES;
|
||||
while (n--) { while (*p) p++; p++; }
|
||||
return p;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/* Copyright 2026 Johan Denoyer F4WAT
|
||||
* https://github.com/jdenoy
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* dec406: streaming decoder for first-generation Cospas-Sarsat 406 MHz beacon
|
||||
* messages, fed with FM-discriminator audio samples (the BK4829 RAW RX audio as
|
||||
* read on PA4). Freestanding C (no libc), no division in the per-sample path, so
|
||||
* the same code runs in the overlay app and in the host test harness.
|
||||
*
|
||||
* Signal: 400 bps biphase-L, +/-1.1 rad phase modulation. The discriminator gives
|
||||
* dphi/dt (a pulse per phase flip); a leaky integrator rebuilds the phase
|
||||
* waveform, a DPLL on its zero crossings tracks the half-bit clock, and the
|
||||
* half-bit signs are matched against 13 preamble ones + frame sync, in both
|
||||
* polarities (the receive chain may invert). Bit n of the message (1-based, as in
|
||||
* C/S T.001) for n = 25..144 is then stored MSB first in bits[].
|
||||
*/
|
||||
|
||||
#ifndef DEC406_H
|
||||
#define DEC406_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define DEC406_FS 9600u /* sample rate expected by the decoder (Hz) */
|
||||
#define DEC406_HALF 12u /* samples per half-bit at 400 bps */
|
||||
|
||||
enum { DEC406_SEARCH = 0, DEC406_DATA = 1, DEC406_DONE = 2 };
|
||||
|
||||
typedef struct {
|
||||
int32_t meanQ8; /* input DC estimate, x256 */
|
||||
int32_t lvl; /* rebuilt phase waveform */
|
||||
int32_t acc; /* sum of lvl over the current half-bit slot */
|
||||
int32_t h1; /* first half of the bit being received */
|
||||
int32_t ph; /* position in the half-bit slot, Q8 */
|
||||
uint32_t hsrHi, hsrLo; /* last 64 half-bit signs, newest in hsrLo bit 0 */
|
||||
uint8_t state, half, inv, selftest, integrate, sign, nbits, total;
|
||||
uint8_t bits[15]; /* message bits 25..144, MSB first */
|
||||
} dec406_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t longMsg; /* bit 25 */
|
||||
uint8_t selftest; /* self-test frame sync */
|
||||
uint8_t bch1, bch2; /* 1 = BCH ok (bch2 is 1 on short messages) */
|
||||
uint8_t userProto; /* bit 26: 1 = user / user-location protocols */
|
||||
uint8_t proto; /* protocol code: bits 37-40, or 37-39 if user */
|
||||
uint8_t stdLoc; /* standard location protocol (position decoded) */
|
||||
uint8_t hasPos; /* position present (not the default pattern) */
|
||||
uint8_t hasFine; /* PDF-2 offsets applied */
|
||||
uint8_t internalPos; /* bit 111: 1 = internal navigation device */
|
||||
uint8_t homing; /* bit 112: 121.5 MHz homing */
|
||||
uint8_t idRaw; /* 1 = ID is bits 26-85 as sent (not std location) */
|
||||
uint16_t country; /* bits 27-36 */
|
||||
uint32_t idData; /* std location: bits 41-64 */
|
||||
int32_t latS, lonS; /* position in arc seconds, N and E positive */
|
||||
char id[16]; /* 15-hex beacon ID, NUL terminated */
|
||||
} dec406_info_t;
|
||||
|
||||
/* integrate: 1 = input is discriminator output (dphi/dt), rebuild the phase;
|
||||
* 0 = input is already phase-like (use it directly). */
|
||||
void dec406_init(dec406_t *d, bool integrate);
|
||||
|
||||
/* Re-arm for the next burst (keeps the DC estimate). */
|
||||
void dec406_rearm(dec406_t *d);
|
||||
|
||||
/* Feed one sample (any unsigned ADC scale). Returns true when a full message
|
||||
* has been received (state DEC406_DONE); call dec406_parse, then dec406_rearm. */
|
||||
bool dec406_push(dec406_t *d, uint16_t sample);
|
||||
|
||||
/* BCH checks and field extraction. */
|
||||
void dec406_parse(const dec406_t *d, dec406_info_t *out);
|
||||
|
||||
/* Short protocol name for display. */
|
||||
const char *dec406_proto_name(const dec406_info_t *in);
|
||||
|
||||
#endif /* DEC406_H */
|
||||
@@ -0,0 +1,326 @@
|
||||
/* Copyright 2026 Johan Denoyer F4WAT
|
||||
* https://github.com/jdenoy
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* EPIRB 406 (RX only) - decodes first-generation Cospas-Sarsat 406 MHz beacon
|
||||
* messages on the radio. Tune the VFO (FM, wide; 406.031 MHz covers the
|
||||
* 406.025-406.040 channels, or a test generator's frequency), then launch.
|
||||
*
|
||||
* The receiver is switched to RAW (RX HPF300 / LPF3K / de-emphasis and AFC off)
|
||||
* and the RX audio is left on: it reaches PA4 (the voice DAC pin, unused while
|
||||
* voice is disabled), held at mid-scale by the MCU DAC (unbuffered) and sampled
|
||||
* on ADC channel 4 at 9.6 kHz, timed from
|
||||
* SysTick, from the moment RSSI shows a burst. Samples go straight into dec406
|
||||
* (see README.md); the last decoded message stays on screen.
|
||||
*
|
||||
* Keys (UV-K5 and UV-K1): UP/DOWN tune +/-5 kHz from the VFO (up to +/-50 kHz,
|
||||
* while the app runs; direction as set by nav_dir) · 5 back to the VFO frequency
|
||||
* MENU clear · EXIT quit. The speaker plays the bursts: lower the volume.
|
||||
* The loader re-runs RADIO_SetupRegisters on exit; the app restores the ADC,
|
||||
* PA4, the DAC and its clock itself.
|
||||
* Screen texts and protocol names are read-only assets (gen_assets.py, API
|
||||
* level 2), kept out of the 4 KiB overlay.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "epirb406_assets.h" /* generated by gen_assets.py (title carries APP_VER) */
|
||||
#include "dec406.c"
|
||||
|
||||
/* ---- MCU registers (PY32F071, core and SysTick at 48 MHz, 10 ms period) ---- */
|
||||
#define SYST_LOAD (*(volatile uint32_t *)0xE000E014u)
|
||||
#define SYST_VAL (*(volatile uint32_t *)0xE000E018u)
|
||||
#define ADC_SR (*(volatile uint32_t *)0x40012400u)
|
||||
#define ADC_CR2 (*(volatile uint32_t *)0x40012408u)
|
||||
#define ADC_SMPR3 (*(volatile uint32_t *)0x40012414u)
|
||||
#define ADC_SQR3 (*(volatile uint32_t *)0x40012438u)
|
||||
#define ADC_DR (*(volatile uint32_t *)0x40012450u)
|
||||
#define GPIOA_MODER (*(volatile uint32_t *)0x50000000u)
|
||||
#define DAC_CR (*(volatile uint32_t *)0x40007400u)
|
||||
#define DAC_SWTRIGR (*(volatile uint32_t *)0x40007404u)
|
||||
#define DAC_DHR12R1 (*(volatile uint32_t *)0x40007408u)
|
||||
#define RCC_APBENR1 (*(volatile uint32_t *)0x4002103Cu)
|
||||
#define RCC_DACEN (1u << 29)
|
||||
/* DAC channel 1 on, output buffer off, software trigger (TSEL1 = 111) */
|
||||
#define DAC_CR_BIAS ((1u << 0) | (1u << 1) | (1u << 2) | (7u << 3))
|
||||
#define BIAS_CODE 2048u
|
||||
#define ADC_SR_EOC (1u << 1)
|
||||
#define ADC_CR2_START ((1u << 22) | (1u << 20)) /* SWSTART | EXTTRIG */
|
||||
#define SMP8_POS 24u
|
||||
#define SMP4_POS 12u
|
||||
#define ADC_CH_PA4 4u
|
||||
#define CYC_PER_SAMPLE (48000000u / DEC406_FS) /* 5000 */
|
||||
#define CAP_MAX_CYC (48000000u / 1000u * 900u) /* 900 ms after trigger */
|
||||
|
||||
/* ---- BK4829 RAW receive ---- */
|
||||
#define REG_2B 0x2B
|
||||
#define REG_73 0x73
|
||||
#define REG_30 0x30 /* 0 then back: VCO recalibration, relock on the new frequency */
|
||||
#define REG_38 0x38 /* RX frequency, 10 Hz units, low 16 bits */
|
||||
#define REG_39 0x39 /* RX frequency, high 16 bits */
|
||||
#define STEP_10HZ 500 /* 5 kHz */
|
||||
#define OFF_MAX 10 /* +/- 10 steps = +/- 50 kHz */
|
||||
|
||||
#define TRIG_DB 10
|
||||
#define REARM_DB 5
|
||||
#define COOL_MS 1500
|
||||
#define TICK_MS 50
|
||||
|
||||
/* The app state in one struct: a function loads one base address instead of one
|
||||
* literal-pool word per global. Words, then bytes, then halfwords, so every field
|
||||
* stays within the Thumb load/store immediate ranges (ldrb <= 31, ldrh <= 62,
|
||||
* ldr <= 124). The loader zeroes the overlay (.bss) at every launch. */
|
||||
static struct {
|
||||
const app_api_t *A;
|
||||
uint32_t vfoFreq; /* VFO RX frequency at launch, 10 Hz units */
|
||||
int32_t floorQ; /* noise floor, dBm x64 */
|
||||
uint32_t tPrev, tCyc; /* SysTick cycle counter */
|
||||
uint8_t have, seq, prevKey, lastErr;
|
||||
int8_t offSteps; /* tuning offset from the VFO, 5 kHz steps */
|
||||
bool running;
|
||||
int16_t rssi, burstRssi; /* current RSSI, RSSI of the last burst */
|
||||
uint16_t nSync, nFail;
|
||||
uint32_t savedSqr3, savedSmpr3, savedModer, savedDac, savedRcc, savedDhr;
|
||||
dec406_info_t last; /* last decoded message */
|
||||
} g;
|
||||
static dec406_t d;
|
||||
static char str[34];
|
||||
|
||||
/* ---- formatting ---- */
|
||||
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
|
||||
/* Formatting by repeated subtraction: no division, so no __udivsi3 in the
|
||||
* 4 KiB overlay. Values printed stay below 100000. */
|
||||
static uint8_t sub(uint32_t *v,uint32_t d){ uint8_t q=0; while(*v>=d){ *v-=d; q++; } return q; }
|
||||
static char *puti(char *o,int32_t v){
|
||||
static const uint16_t P10[]={10000u,1000u,100u,10u,1u};
|
||||
uint32_t u;
|
||||
if(v<0){*o++='-';u=(uint32_t)(-v);} else u=(uint32_t)v;
|
||||
bool lead=false;
|
||||
for(uint8_t i=0;i<5;i++){
|
||||
uint8_t c=sub(&u,P10[i]);
|
||||
if(c||lead||i==4u){ *o++=(char)('0'+c); lead=true; }
|
||||
}
|
||||
return o;
|
||||
}
|
||||
/* arc seconds -> "49.27111" (5 decimals, truncated) followed by the hemisphere */
|
||||
static char *putPos(char *o,int32_t s,char pos,char neg){
|
||||
uint32_t a=(uint32_t)(s<0?-s:s);
|
||||
o=puti(o,sub(&a,3600u)); *o++='.';
|
||||
for(uint8_t k=0;k<5;k++){ a*=10u; *o++=(char)('0'+sub(&a,3600u)); }
|
||||
*o++= s<0?neg:pos;
|
||||
return o;
|
||||
}
|
||||
static void tiny(uint8_t x,uint8_t y,char *end){ *end='\0'; g.A->print_tiny(str,x,y,false,true); }
|
||||
static void line(uint8_t l,char *end){ *end='\0'; g.A->print_normal(str,0,0,l); }
|
||||
|
||||
/* ---- tuning: offset the RX frequency from the VFO (restored by the loader on exit) ---- */
|
||||
static void tune(void){
|
||||
uint32_t f=(uint32_t)((int32_t)g.vfoFreq+(int32_t)g.offSteps*STEP_10HZ);
|
||||
g.A->bk_write(REG_38,(uint16_t)(f&0xFFFFu));
|
||||
g.A->bk_write(REG_39,(uint16_t)(f>>16));
|
||||
uint16_t r30=g.A->bk_read(REG_30);
|
||||
g.A->bk_write(REG_30,0);
|
||||
g.A->bk_write(REG_30,r30);
|
||||
}
|
||||
/* "433.645" from 10 Hz units, rounded to the kHz, without division */
|
||||
static char *putFreq(char *o,uint32_t f){
|
||||
uint16_t mhz=0;
|
||||
f+=50u;
|
||||
while(f>=100000u){ f-=100000u; mhz++; }
|
||||
o=puti(o,mhz); *o++='.';
|
||||
*o++=(char)('0'+sub(&f,10000u)); *o++=(char)('0'+sub(&f,1000u)); *o++=(char)('0'+sub(&f,100u));
|
||||
return o;
|
||||
}
|
||||
|
||||
/* ---- SysTick cycle counter (call at least every 10 ms) ---- */
|
||||
static void clkStart(void){ g.tPrev=SYST_VAL; g.tCyc=0; }
|
||||
static uint32_t clkCyc(void){
|
||||
uint32_t v=SYST_VAL;
|
||||
g.tCyc += (v<=g.tPrev) ? g.tPrev-v : g.tPrev+(SYST_LOAD+1u-v);
|
||||
g.tPrev=v;
|
||||
return g.tCyc;
|
||||
}
|
||||
|
||||
/* ---- PA4 bias: the pin has no DC reference of its own (AC-coupled audio), so
|
||||
* 9.6 kHz sampling dragged it to 0 V and clipped half the message (406 Lab scope).
|
||||
* The MCU DAC on PA4, output buffer off, at mid-scale, holds it at ~VDD/2.
|
||||
* Set for the whole run, restored on exit. ---- */
|
||||
static void biasOn(void){
|
||||
GPIOA_MODER=g.savedModer|(3u<<8); /* PA4 analog */
|
||||
RCC_APBENR1=g.savedRcc|RCC_DACEN;
|
||||
DAC_CR=DAC_CR_BIAS;
|
||||
DAC_DHR12R1=BIAS_CODE;
|
||||
DAC_SWTRIGR=1u; /* DHR -> DOR */
|
||||
}
|
||||
static void biasOff(void){ DAC_CR=g.savedDac; DAC_DHR12R1=g.savedDhr; RCC_APBENR1=g.savedRcc; GPIOA_MODER=g.savedModer; }
|
||||
|
||||
/* ---- ADC on PA4 (channel 4); channel 8 (battery) restored afterwards ---- */
|
||||
static void adcSelPA4(void){
|
||||
ADC_SMPR3=(g.savedSmpr3&~(7u<<SMP4_POS))|(((g.savedSmpr3>>SMP8_POS)&7u)<<SMP4_POS);
|
||||
ADC_SQR3=(g.savedSqr3&~0x1Fu)|ADC_CH_PA4;
|
||||
}
|
||||
static void adcRestore(void){ ADC_SQR3=g.savedSqr3; ADC_SMPR3=g.savedSmpr3; }
|
||||
static uint16_t adcRead(void){
|
||||
ADC_CR2|=ADC_CR2_START;
|
||||
for(uint16_t n=0; !(ADC_SR&ADC_SR_EOC) && n<2000u; n++){}
|
||||
return (uint16_t)(ADC_DR&0x0FFFu);
|
||||
}
|
||||
|
||||
/* ---- one burst: sample PA4 at 9.6 kHz into the decoder ---- */
|
||||
static void capture(void){
|
||||
dec406_init(&d,true); /* INT: integrate the discriminator pulses */
|
||||
adcSelPA4();
|
||||
clkStart();
|
||||
uint32_t next=0;
|
||||
bool done=false;
|
||||
while(next<CAP_MAX_CYC){
|
||||
while(clkCyc()<next){}
|
||||
if(dec406_push(&d,adcRead())){ done=true; break; }
|
||||
next+=CYC_PER_SAMPLE;
|
||||
}
|
||||
adcRestore();
|
||||
|
||||
if(!done){ g.nFail++; g.lastErr = d.state==DEC406_DATA ? 2u : 1u; return; } /* 1 no sync, 2 cut */
|
||||
g.nSync++; g.lastErr=0;
|
||||
dec406_parse(&d,&g.last);
|
||||
g.burstRssi=g.rssi; /* RSSI at the trigger (not updated while sampling) */
|
||||
g.seq++;
|
||||
g.have=1;
|
||||
}
|
||||
|
||||
/* ---- display ---- */
|
||||
static void draw(void){
|
||||
/* The UI texts live in the assets: one read per frame into this word-aligned
|
||||
* stack block, so each text costs a 2-byte sp-relative add instead of a
|
||||
* literal and its pool word (gen_assets.py keeps every offset aligned). */
|
||||
char s[UI_SIZE] __attribute__((aligned(4)));
|
||||
char *o;
|
||||
g.A->asset_read(0,s,UI_SIZE);
|
||||
g.A->display_clear();
|
||||
g.A->status_clear();
|
||||
g.A->print_inverse(s+T_TITLE,2,0,true,true,(uint8_t)(2u+T_TITLE_CHARS*4u));
|
||||
g.A->draw_battery();
|
||||
|
||||
if(!g.have){
|
||||
o=put(str,s+T_WAIT); line(1,o);
|
||||
} else {
|
||||
const dec406_info_t *in=&g.last;
|
||||
o=put(str,in->id); line(0,o); /* 15-hex ID */
|
||||
o=puti(str,in->country); *o++=' ';
|
||||
/* protocol name straight from the assets: 16 location names (bits 37-40),
|
||||
* then 8 user names (bits 37-39); the entry's NUL padding ends the line */
|
||||
uint8_t n=in->userProto ? (uint8_t)(16u+(in->proto&7u)) : (uint8_t)(in->proto&15u);
|
||||
o+=g.A->asset_read((uint16_t)(T_PROTO+n*T_PROTO_STRIDE),o,T_PROTO_STRIDE);
|
||||
line(1,o);
|
||||
if(in->hasPos){ o=putPos(str,in->latS,'N','S'); *o++=' '; o=putPos(o,in->lonS,'E','W'); }
|
||||
else o=put(str,s+T_NOPOS);
|
||||
line(2,o);
|
||||
|
||||
o=str;
|
||||
if(in->selftest) o=put(o,s+T_SELFTEST);
|
||||
o=put(o,in->longMsg?s+T_LONG:s+T_SHORT);
|
||||
o=put(o,s+T_BCH); o=put(o,in->bch1?s+T_OK:s+T_ERR); *o++='/'; o=put(o,in->bch2?s+T_OK:s+T_ERR);
|
||||
tiny(0,33,o);
|
||||
o=str; *o++='#'; o=puti(o,g.seq); *o++=' '; o=puti(o,g.burstRssi); o=put(o,s+T_DBM);
|
||||
if(in->longMsg){ o=put(o,in->internalPos?s+T_INT:s+T_EXT); if(in->homing) o=put(o,s+T_HOMING); }
|
||||
if(in->hasPos && !in->hasFine) o=put(o,s+T_COARSE);
|
||||
if(in->idRaw) o=put(o,s+T_RAWID);
|
||||
tiny(0,40,o);
|
||||
}
|
||||
|
||||
o=putFreq(str,g.vfoFreq+(uint32_t)((int32_t)g.offSteps*STEP_10HZ));
|
||||
if(g.offSteps){ *o++=' '; if(g.offSteps>0) *o++='+'; o=puti(o,(int32_t)g.offSteps*5); o=put(o,s+T_KHZ); }
|
||||
tiny(0,26,o);
|
||||
|
||||
o=puti(str,g.rssi); *o++='/'; o=puti(o,g.floorQ/64); o=put(o,s+T_DBMOK); o=puti(o,g.nSync);
|
||||
o=put(o,s+T_ERRS); o=puti(o,g.nFail);
|
||||
if(g.lastErr) o=put(o,g.lastErr==1u?s+T_NOSYNC:s+T_CUT);
|
||||
tiny(0,48,o);
|
||||
}
|
||||
|
||||
/* ---- input ---- */
|
||||
static void handleKeys(void){
|
||||
uint8_t key=g.A->get_key();
|
||||
if(key==APP_KEY_INVALID||key==g.prevKey){ g.prevKey=key; return; }
|
||||
g.prevKey=key;
|
||||
switch(key){
|
||||
case APP_KEY_EXIT: g.running=false; break;
|
||||
case APP_KEY_5: g.offSteps=0; tune(); break;
|
||||
case APP_KEY_MENU: g.have=0; g.nSync=g.nFail=0; g.lastErr=0; break;
|
||||
case APP_KEY_UP:
|
||||
case APP_KEY_DOWN: {
|
||||
int8_t n=(int8_t)(g.offSteps+g.A->nav_dir(key));
|
||||
if(n>=-OFF_MAX && n<=OFF_MAX){ g.offSteps=n; tune(); }
|
||||
break; }
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
static void cooldown(void){
|
||||
for(uint16_t ms=0; ms<COOL_MS; ms+=10){
|
||||
if(g.A->rssi_dbm() < g.floorQ/64+REARM_DB) break;
|
||||
g.A->delay_ms(10);
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((section(".text.entry"),used))
|
||||
void app_main(const app_api_t *api){
|
||||
g.A=api;
|
||||
g.prevKey=APP_KEY_INVALID; /* the rest of g starts at 0 (overlay zeroed by the loader) */
|
||||
|
||||
g.savedSqr3=ADC_SQR3; g.savedSmpr3=ADC_SMPR3; g.savedModer=GPIOA_MODER; g.savedDac=DAC_CR;
|
||||
g.savedRcc=RCC_APBENR1; g.savedDhr=DAC_DHR12R1;
|
||||
biasOn();
|
||||
g.A->backlight_on();
|
||||
g.vfoFreq=g.A->rx_freq();
|
||||
g.A->bk_write(REG_2B,(uint16_t)((g.A->bk_read(REG_2B)|0x0700u)&~0x0007u));
|
||||
g.A->bk_write(REG_73,(uint16_t)(g.A->bk_read(REG_73)|0x0010u));
|
||||
g.A->audio_path(true);
|
||||
g.A->set_af(APP_AF_FM);
|
||||
g.A->delay_ms(50);
|
||||
|
||||
int32_t f=0;
|
||||
for(uint8_t i=0;i<16;i++){ f+=g.A->rssi_dbm(); g.A->delay_ms(5); }
|
||||
g.floorQ=f*4;
|
||||
|
||||
g.running=true;
|
||||
while(g.running){
|
||||
for(uint8_t i=0;i<TICK_MS;i++){
|
||||
g.rssi=g.A->rssi_dbm();
|
||||
if(g.rssi>=g.floorQ/64+TRIG_DB){
|
||||
capture();
|
||||
draw(); g.A->blit_status(); g.A->blit_full();
|
||||
cooldown();
|
||||
break;
|
||||
}
|
||||
g.floorQ+=((int32_t)g.rssi*64-g.floorQ)/64;
|
||||
g.A->delay_ms(1);
|
||||
}
|
||||
handleKeys();
|
||||
if(!g.running) break;
|
||||
draw();
|
||||
g.A->blit_status();
|
||||
g.A->blit_full();
|
||||
g.A->battery_sample();
|
||||
g.A->backlight_on(); /* keep the screen lit while waiting for bursts (and after a key) */
|
||||
}
|
||||
|
||||
biasOff(); /* the ADC is already restored after each capture */
|
||||
g.A->set_af(APP_AF_MUTE);
|
||||
g.A->audio_path(false);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
# EPIRB 406 read-only assets: the screen texts and the protocol names.
|
||||
#
|
||||
# The UI texts come first, each padded to a multiple of 4 bytes: draw() reads
|
||||
# them in one asset_read into a word-aligned stack block, so every text costs a
|
||||
# 2-byte sp-relative add instead of a literal-pool load and a pool word.
|
||||
# The protocol names are parsed from dec406.c (NAMES in dec406_proto_name), so
|
||||
# the host test and the app share one table.
|
||||
#
|
||||
# APP_VER=1.6 ./gen_assets.py epirb406_assets.bin epirb406_assets.h
|
||||
import os, re, sys
|
||||
sys.dont_write_bytecode = True
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.join(HERE, ".."))
|
||||
from app_assets import Assets
|
||||
|
||||
def proto_names():
|
||||
src = open(os.path.join(HERE, "dec406.c")).read()
|
||||
m = re.search(r"static const char NAMES\[\]\s*=(.*?);", src, re.S)
|
||||
if not m:
|
||||
sys.exit("dec406.c: NAMES table not found")
|
||||
packed = "".join(re.findall(r'"((?:[^"\\]|\\.)*)"', m.group(1)))
|
||||
names = packed.split("\\0")
|
||||
if len(names) != 24:
|
||||
sys.exit(f"dec406.c: {len(names)} protocol names, 24 expected")
|
||||
return names
|
||||
|
||||
TITLE = "EPIRB 406 v" + os.environ.get("APP_VER", "dev")
|
||||
|
||||
UI = [
|
||||
("T_TITLE", TITLE),
|
||||
("T_WAIT", "Waiting..."),
|
||||
("T_NOPOS", "no position"),
|
||||
("T_SELFTEST", "SELF-TEST "),
|
||||
("T_LONG", "LONG"),
|
||||
("T_SHORT", "SHORT"),
|
||||
("T_BCH", " BCH "),
|
||||
("T_OK", "OK"),
|
||||
("T_ERR", "ERR"),
|
||||
("T_DBM", "dBm"),
|
||||
("T_INT", " int"),
|
||||
("T_EXT", " ext"),
|
||||
("T_HOMING", " 121.5"),
|
||||
("T_COARSE", " coarse"),
|
||||
("T_RAWID", " rawID"),
|
||||
("T_KHZ", " kHz"),
|
||||
("T_DBMOK", "dBm ok"),
|
||||
("T_ERRS", " err"),
|
||||
("T_NOSYNC", " nosync"),
|
||||
("T_CUT", " cut"),
|
||||
]
|
||||
|
||||
a = Assets("EPIRB406")
|
||||
ui_size = 0
|
||||
for name, s in UI:
|
||||
pad = -(len(s) + 1) % 4 # keep every offset word-aligned
|
||||
a.text(name, s + "\0" * pad)
|
||||
ui_size += len(s) + 1 + pad
|
||||
a.const("UI_SIZE", ui_size) # the UI block read by draw()
|
||||
a.const("T_TITLE_CHARS", len(TITLE))
|
||||
a.table("T_PROTO", proto_names()) # 16 location, then 8 user protocols
|
||||
a.main()
|
||||
@@ -0,0 +1,6 @@
|
||||
Filetype: Flipper SubGhz RAW File
|
||||
Version: 1
|
||||
Frequency: 433650000
|
||||
Preset: FuriHalSubGhzPreset2FSKDev238Async
|
||||
Protocol: RAW
|
||||
RAW_Data: 161250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -2500 1250 -1250 1250 -1250 2500 -2500 2500 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -2500 1250 -1250 1250 -1250 2500 -1250 1250 -1250 1250 -2500 1250 -1250 1250 -1250 2500 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -2500 1250 -1250 1250 -1250 1250 -1250 2500 -2500 1250 -1250 2500 -2500 1250 -1250 2500 -1250 1250 -1250 1250 -2500 2500 -2500 1250 -1250 1250 -1250 2500 -2500 2500 -2500 2500 -1250 1250 -2500 1250 -1250 1250 -1250 2500 -1250 1250 -2500 1250 -1250 1250 -1250 2500 -2500 2500 -2500 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 2500 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -2500 2500 -1250 1250 -2500 1250 -1250 1250 -1250 1250 -1250 1250 -1250 2500 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -2500 2500 -1250 1250 -1250 1250 -1250 1250 -1250 1250 -2500 2500 -2500 2500 -1250 1250 -2500 1250 -1250 1250 -1250 1250 -1250 2500 -2500 2500 -2500 1250 -1250 2500 -2500 1250 -1250 1250 -1250 1250 -1250 2500 -1250 1250 -1250 1250 -1250 1250 -2500 2500 -1250 1250 -2500 2500 -1250 1250 -2500 2500 -2500 1250 -1250 1250 -1250 2500 -1250 1250 -21250
|
||||
Loaded 100 of 333 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user