Compare commits
1014 Commits
main
..
cc64c51fd0
| Author | SHA1 | Date | |
|---|---|---|---|
| cc64c51fd0 | |||
| 723da3f7ed | |||
| 7cf829ef52 | |||
| 2517ed0b29 | |||
| 9019acee0e | |||
| cf4c262456 | |||
| 7527770c0c | |||
| b533d704a1 | |||
| a823e66816 | |||
| 7044747ade | |||
| da799a1d1f | |||
| bcd3255ad7 | |||
| 46415fa307 | |||
| 8aa1884d2d | |||
| 7844cb65c8 | |||
| c71fc58e3e | |||
| 4464fa3735 | |||
| e1a9a8717f | |||
| 49659a5ce7 | |||
| d6d7c7d1f0 | |||
| 8da4c49d1d | |||
| fc423d9958 | |||
| 659f18143d | |||
| bff3be38be | |||
| 088f05050c | |||
| 4c69273584 | |||
| 7a144375f2 | |||
| e337870d53 | |||
| 016d4ddfd7 | |||
| 6eb2d5341f | |||
| bdc581637c | |||
| c818139175 | |||
| 223726368d | |||
| 3564a48ced | |||
| 6be8644d45 | |||
| 52452b609d | |||
| 844b0e60df | |||
| 9fba303bd8 | |||
| 84dc28cf77 | |||
| 0f57fb8920 | |||
| 4a12d79f92 | |||
| d131efae36 | |||
| 9058e5d101 | |||
| 1b6ae1b18c | |||
| 1f4c3e0384 | |||
| efa51443b6 | |||
| 338c75c13d | |||
| e15e822a1f | |||
| 6700e79155 | |||
| c2d6530b83 | |||
| 37a5600d99 | |||
| 1a8bfb3c4e | |||
| 0dc6761fa0 | |||
| 5aa3d61ce0 | |||
| b7a4b8a1df | |||
| 2ebd085aec | |||
| 2a298add7d | |||
| fb715860fb | |||
| db71ca0950 | |||
| 2702696032 | |||
|
22311eb01c
|
|||
| 8cb713b3b1 | |||
|
3d99cac03b
|
|||
|
262e78e72b
|
|||
|
f78232562d
|
|||
|
b1f52bbfa5
|
|||
| ab3bf9120e | |||
|
342adf476c
|
|||
|
eac2efdb35
|
|||
|
3b28281684
|
|||
|
0d2eb7adcd
|
|||
|
188fa38f1b
|
|||
| 71bfc5fca1 | |||
| 5dd8f4112a | |||
| d2257fd8a3 | |||
| 7c08e12870 | |||
| cededd9c3f | |||
| dc683792de | |||
| 4efb0dc5ef | |||
| 86d1775484 | |||
| 2ad555efd1 | |||
| f43f3a27fd | |||
| e6a78d0ec0 | |||
| 0b97329f63 | |||
| 5b168fd6d5 | |||
| 3c538d5712 | |||
| d3abce9ab0 | |||
| 2ab05f5001 | |||
| 6d430a4300 | |||
| 27de75cf79 | |||
| 2570fe739e | |||
| b032473801 | |||
| ad6aa6aab4 | |||
| d547c45a9c | |||
| 708c00a84c | |||
| 9b8ea37a6e | |||
| 780e68089f | |||
| 5156296444 | |||
| 8eae376c56 | |||
| a1b9b7f762 | |||
|
73b1d5618d
|
|||
|
cba3751f0e
|
|||
|
e9cb2852be
|
|||
|
abae110ecd
|
|||
|
4114e0b9fa
|
|||
|
a924902074
|
|||
|
86768c8e7f
|
|||
|
d5c3283b37
|
|||
|
e2517ec184
|
|||
|
3b52fdf232
|
|||
|
daedd91829
|
|||
|
3f7afd961b
|
|||
|
c1e8ce42d2
|
|||
|
5590ac5c7d
|
|||
|
8603cee7be
|
|||
|
9e8003afb6
|
|||
|
c50f716390
|
|||
|
dca01ead90
|
|||
|
badf7c0d0f
|
|||
|
c4d1313625
|
|||
|
28d5494c3b
|
|||
|
07c5129668
|
|||
|
9292bf3830
|
|||
|
b9744ef8fd
|
|||
|
a3c098b68f
|
|||
|
3c31e16833
|
|||
|
40e99d3ea9
|
|||
|
872e086763
|
|||
|
fed8948a61
|
|||
|
ecb058a9d2
|
|||
|
1dec2c2663
|
|||
|
f7d3a935ee
|
|||
|
587ece9903
|
|||
|
945912adc1
|
|||
|
5ed3e29d90
|
|||
|
0b0e86f496
|
|||
|
fb83e3cade
|
|||
|
cb824d20ec
|
|||
|
968dc84997
|
|||
|
e4487d9037
|
|||
|
a82a7ab668
|
|||
|
60697bb26a
|
|||
|
7fde0f7730
|
|||
|
47f57aa10d
|
|||
|
bc153b84ee
|
|||
|
12f1d81af8
|
|||
|
1be02ec8ad
|
|||
|
e5bf7346c8
|
|||
|
fc954fd27c
|
|||
|
8be8e798c6
|
|||
|
8043a5001c
|
|||
|
73173d29ff
|
|||
|
c014f5cdc2
|
|||
|
66987a4070
|
|||
|
c178c56f2e
|
|||
|
69fd24577c
|
|||
|
0179c13000
|
|||
|
4cac91e36a
|
|||
|
be4844c710
|
|||
|
4f4cd4647d
|
|||
|
b679ff0282
|
|||
|
4cca188d9f
|
|||
|
f7cbc0cb02
|
|||
|
5c28ed1269
|
|||
|
f8fd4572c7
|
|||
|
963d527dfe
|
|||
|
bdd3d29374
|
|||
|
95fccd3da6
|
|||
|
34d0ec8ca8
|
|||
|
a91a1868d8
|
|||
|
2462f1dd47
|
|||
|
21a534bdb6
|
|||
|
763d4c00b0
|
|||
|
6c9ff33d68
|
|||
|
b83558b1a2
|
|||
|
2d014ac45b
|
|||
|
fc24dc37ed
|
|||
|
6d18f5e1d4
|
|||
|
e8326b4822
|
|||
|
add0a93424
|
|||
| 1fe7dc88c6 | |||
| 93ff35a824 | |||
|
21972c27d2
|
|||
|
daa0631b35
|
|||
|
717228a635
|
|||
|
502e97049a
|
|||
|
894739bbab
|
|||
|
19ab3b3931
|
|||
|
60267d450b
|
|||
|
4e93dcc82a
|
|||
|
6131d7a1d6
|
|||
|
28036ab589
|
|||
|
3d284abab6
|
|||
|
4bb7248257
|
|||
|
af45c32222
|
|||
|
cef1741e40
|
|||
|
832e42ca3b
|
|||
|
e5aa74a1b6
|
|||
|
dda5ec17bb
|
|||
|
05169912b1
|
|||
|
2f115fbec3
|
|||
|
d53d60629e
|
|||
|
e4cfd35282
|
|||
|
998c6ad0e6
|
|||
|
f1d412a566
|
|||
|
877573c905
|
|||
|
4f9f93c9c1
|
|||
|
c9d204ad1c
|
|||
|
6874055b1c
|
|||
|
46c0f8d0bb
|
|||
|
e1fe1980ea
|
|||
|
d862d953e1
|
|||
|
81515d921e
|
|||
|
6a755ef2b4
|
|||
|
a62997d6ce
|
|||
|
4740b38ad4
|
|||
|
ce1ca48384
|
|||
|
00fc1dbdfb
|
|||
|
9a398f3754
|
|||
|
2e8f025b19
|
|||
|
51df676e46
|
|||
|
35983eb971
|
|||
|
5a3c013db5
|
|||
|
03a5bff4cc
|
|||
|
e29b7ed3d3
|
|||
|
003075c5e4
|
|||
|
541e27bb7a
|
|||
|
aa079598bd
|
|||
|
9a96ed8236
|
|||
|
de11c7ff51
|
|||
| 2030d0b050 | |||
|
8cb6292c1d
|
|||
|
824434e27e
|
|||
|
75355c75a5
|
|||
|
1d8b77ae44
|
|||
|
409b173f62
|
|||
| 26fbd37b6d | |||
| ac586418fc | |||
| 19d6d2e50b | |||
|
ee821a71b1
|
|||
|
250300395f
|
|||
|
6c4853c88c
|
|||
|
f8d7472510
|
|||
|
b4fb42dba4
|
|||
|
cbf3301664
|
|||
|
53199642ea
|
|||
|
a1bcf6f8bf
|
|||
|
0fc92bb1de
|
|||
|
2f1b0609fb
|
|||
|
b65b85e13c
|
|||
|
1001786a08
|
|||
|
bfc510e1eb
|
|||
|
e2c568a98a
|
|||
|
b23f05966f
|
|||
|
90d446c8f4
|
|||
|
4272094882
|
|||
|
175abe35f9
|
|||
|
91562b2a4b
|
|||
|
4d22fa964d
|
|||
|
edaef98522
|
|||
|
7de847b504
|
|||
|
faaaad6ab9
|
|||
|
69b2aee753
|
|||
|
e3378d5f49
|
|||
|
f9c0fa6981
|
|||
|
ab27f111d5
|
|||
|
8c16f0da41
|
|||
|
18d3a00c07
|
|||
|
e5b9fb7aca
|
|||
|
3d0cdbd181
|
|||
|
890abcdc24
|
|||
|
8bd779a239
|
|||
|
3eb5a615b9
|
|||
|
3188c9b0ad
|
|||
|
5597a8c206
|
|||
|
3d9003feb2
|
|||
|
3f2892e245
|
|||
|
5dcc117e61
|
|||
|
9ed93ec0fa
|
|||
|
8be3b273bc
|
|||
|
4e6a800391
|
|||
|
c4f0844137
|
|||
|
27b90a62c5
|
|||
|
633ad92dd6
|
|||
|
a041dd4505
|
|||
|
ccef359034
|
|||
|
bc0d9a6273
|
|||
|
bf74044c05
|
|||
|
8e8d21b527
|
|||
|
1370234b35
|
|||
|
49f44e84a0
|
|||
|
05e72e21e8
|
|||
|
2e6b9f87fb
|
|||
|
083d009aa5
|
|||
|
4c69e2bcde
|
|||
|
a905fa9678
|
|||
|
bfff095cf0
|
|||
|
14d58ce989
|
|||
|
75e177a5f4
|
|||
|
9786030f7d
|
|||
|
c1f37a320f
|
|||
|
f3f0b1dfd3
|
|||
|
d8f7ebafa0
|
|||
|
878b50cad7
|
|||
|
239857d536
|
|||
|
4f01db0801
|
|||
|
1ebe65e0f2
|
|||
|
2a9cd5ed0e
|
|||
|
eac5e142db
|
|||
|
d6ad873bfe
|
|||
|
0175b1c916
|
|||
|
a45979fe9b
|
|||
|
e91d36775b
|
|||
|
4b9e84e194
|
|||
|
013667f284
|
|||
|
0836091bbc
|
|||
|
8b174db04d
|
|||
|
125237f53d
|
|||
|
fbfff4154f
|
|||
|
c34ecc493d
|
|||
|
9e1bdf2435
|
|||
|
75a74d4063
|
|||
|
283125989b
|
|||
|
d1968bd2ba
|
|||
|
046d0a33af
|
|||
|
bd5c909695
|
|||
|
a194043caf
|
|||
|
52bf573e4e
|
|||
|
efe9dc346b
|
|||
|
df173a3202
|
|||
|
4bdb4937c9
|
|||
|
b9b420fa54
|
|||
|
80de405a03
|
|||
|
68b6bfa0b7
|
|||
|
2eb3c4f66f
|
|||
|
295a395999
|
|||
|
e3a5fee666
|
|||
|
087ebb33bb
|
|||
|
8042624c39
|
|||
|
417a86284c
|
|||
|
da9d762ad2
|
|||
|
d72acf9e90
|
|||
|
d40aa7e614
|
|||
|
5cdcdd3498
|
|||
|
1825a0a003
|
|||
|
004ac19ea6
|
|||
|
6f57813d36
|
|||
|
9bf579aaa0
|
|||
|
fa91cf011b
|
|||
|
e8002a10ab
|
|||
|
acecaff810
|
|||
|
564f25fc6d
|
|||
|
953edb336a
|
|||
|
7188d38610
|
|||
|
605531184b
|
|||
|
c1ed936c2e
|
|||
|
e2e4d34f30
|
|||
|
298b7247f8
|
|||
|
081d2d062d
|
|||
|
8d06d73e60
|
|||
|
9f43cfd5e8
|
|||
|
e9188ce400
|
|||
|
1755608d58
|
|||
|
67c7b5d1d3
|
|||
|
c454f2218d
|
|||
|
906db0aee2
|
|||
|
696ba09049
|
|||
|
10f5a147dc
|
|||
|
5174e1ab73
|
|||
|
c6af684d6c
|
|||
|
9fe293e056
|
|||
|
c8d81ed353
|
|||
|
40b235e030
|
|||
|
5e84fe2a82
|
|||
|
6e558303a7
|
|||
|
92423f1e02
|
|||
|
051d07eaab
|
|||
|
e7b38c52f7
|
|||
|
8bb0497066
|
|||
|
ff915953b9
|
|||
|
dfc4430413
|
|||
|
bc50429559
|
|||
|
3099ae7d68
|
|||
|
27f45feedd
|
|||
|
c778d4b9a8
|
|||
|
b6692b759e
|
|||
|
91a55554ac
|
|||
|
9ec0ba6545
|
|||
|
a48c11c1fe
|
|||
|
eb6165e969
|
|||
|
67352032e9
|
|||
|
518e10c36a
|
|||
|
89e44f3fa7
|
|||
|
49f0de75f0
|
|||
|
50f8c12487
|
|||
|
5ed55c6103
|
|||
|
1aae38aa4c
|
|||
|
e57cf95320
|
|||
|
88203fb043
|
|||
|
d5bf8f5c43
|
|||
|
1deac71f82
|
|||
|
fda883533d
|
|||
|
1b6acb0fca
|
|||
|
17be874ee3
|
|||
|
3d7ac6f6ed
|
|||
|
55e0a62e4b
|
|||
|
0af9b73bb9
|
|||
|
5054fdd25f
|
|||
|
2ffb341dac
|
|||
|
55e506ce2e
|
|||
|
a4a21181db
|
|||
|
37ffeac5f0
|
|||
|
8b94c5e0bd
|
|||
|
18ebf24115
|
|||
|
5dac587233
|
|||
|
5e3f8791b5
|
|||
|
53da1570ec
|
|||
|
473bbb280a
|
|||
|
587b06c6d8
|
|||
|
e80f71ced6
|
|||
|
832fac1d64
|
|||
|
616ff1b79e
|
|||
|
dc3c99b0ee
|
|||
|
32a7629e6a
|
|||
|
34f6b42330
|
|||
|
31238098ca
|
|||
|
623aad10d1
|
|||
|
fb52558652
|
|||
|
d569db6643
|
|||
|
2b1764b90e
|
|||
|
a1a8aaeb9d
|
|||
|
129c27548e
|
|||
|
ded6e2a1f8
|
|||
|
fbd881af7b
|
|||
|
0244839a4f
|
|||
|
353ce517ec
|
|||
|
00f5108a58
|
|||
|
31b40fc1ef
|
|||
|
e337b6d2de
|
|||
|
5899592b6c
|
|||
|
63fd35802e
|
|||
|
08a91ebe43
|
|||
|
e2a9e4d610
|
|||
|
2188d62069
|
|||
|
c2add05a7c
|
|||
|
0e6d2c0c47
|
|||
|
1ef1da7c0d
|
|||
|
6e9d0040bb
|
|||
|
42e73f5eeb
|
|||
|
db425156a4
|
|||
|
dd051beee3
|
|||
|
f537e1a11b
|
|||
|
8c6c370563
|
|||
|
1ae44a714f
|
|||
|
21a019a0fa
|
|||
|
81dca146cf
|
|||
|
5b3d0c0865
|
|||
|
aa73a4bc00
|
|||
|
eec147c26f
|
|||
|
579ce31bbd
|
|||
|
69253959c8
|
|||
|
17a643d070
|
|||
|
26e51447d7
|
|||
|
b24d6663a2
|
|||
|
436e1d8e23
|
|||
|
b6848c0c8e
|
|||
|
39c4f1d800
|
|||
|
c60b764acb
|
|||
|
1b34d75a31
|
|||
|
8359beae5c
|
|||
|
3e5df90c8f
|
|||
|
8e353620de
|
|||
|
a7ba1eec5f
|
|||
|
7c810729ed
|
|||
|
5987a0b4a8
|
|||
|
37966ea29a
|
|||
|
8ffe73c539
|
|||
|
df612aae00
|
|||
|
b552829c95
|
|||
|
350087855f
|
|||
|
cbefdc15c4
|
|||
|
aa367239de
|
|||
|
62c5889619
|
|||
|
fce024b090
|
|||
|
99ff619b9a
|
|||
|
56d6d12d9e
|
|||
|
08b8c80cc3
|
|||
|
441cdd3adb
|
|||
|
64bb9a0d42
|
|||
|
46bc96ddf2
|
|||
|
523f3992f5
|
|||
|
cb5467ba34
|
|||
|
d133b9082b
|
|||
|
ca6b83c967
|
|||
|
313c9b39a6
|
|||
|
f7c10a0185
|
|||
|
c2b287e000
|
|||
|
8ec442da0a
|
|||
|
a61f901024
|
|||
|
f9492633d9
|
|||
|
f8507698f4
|
|||
|
99c6ff0e87
|
|||
|
00a453f18c
|
|||
|
e3c7231650
|
|||
|
6e8b23052d
|
|||
|
bfbf18ae21
|
|||
|
da2cc91925
|
|||
|
93108c1248
|
|||
|
816780aa6b
|
|||
|
3ea516d27c
|
|||
|
ec1ea49092
|
|||
|
a2872dece5
|
|||
|
740d678357
|
|||
|
626d5e2ec5
|
|||
|
52aeb7b694
|
|||
|
a9aee190d3
|
|||
|
0c35051630
|
|||
|
4f99c23e7a
|
|||
|
7eb88b6669
|
|||
|
132cd5b950
|
|||
|
36d0e7e862
|
|||
|
380231a68f
|
|||
|
700e5c9fab
|
|||
|
1ba9c9dd3e
|
|||
|
4994a869f9
|
|||
|
a5e66ed287
|
|||
|
edb456e843
|
|||
|
cb4b81ca94
|
|||
|
8f6a5af4ec
|
|||
|
13de90d664
|
|||
|
8d0dc17317
|
|||
|
815efdb1ae
|
|||
|
13576f73aa
|
|||
|
33b218fce3
|
|||
|
2ed4aaf46d
|
|||
|
e7997f8492
|
|||
|
a6c3a85c8d
|
|||
|
f9691fee1e
|
|||
|
72219956d9
|
|||
|
b2ce62646c
|
|||
|
926a02e043
|
|||
|
b42e557a25
|
|||
|
198efa2092
|
|||
|
7a6467320b
|
|||
|
ca0f236f4a
|
|||
|
bec3fc72d3
|
|||
|
6f37f9c66b
|
|||
|
e84a0599fa
|
|||
|
3e88c3f5d9
|
|||
|
5411607b63
|
|||
|
17c70f89e5
|
|||
|
fdcc732c83
|
|||
|
b0d8ce8e29
|
|||
|
a6847742d9
|
|||
|
a8ed9e8fa4
|
|||
|
a25182ea3f
|
|||
|
3b277da243
|
|||
|
206696acc5
|
|||
|
5060522541
|
|||
|
b6f0a9967d
|
|||
|
32822effda
|
|||
|
635d2b39bb
|
|||
|
420e98de31
|
|||
|
9453c90dac
|
|||
|
0dd91a952b
|
|||
|
6d466f44d2
|
|||
|
9e49bfbcc8
|
|||
|
617255cd32
|
|||
|
93e507606e
|
|||
|
dbeec9fb39
|
|||
|
96da7ca471
|
|||
|
5a6ff1427a
|
|||
|
8440fe164b
|
|||
|
735c9a7f07
|
|||
|
3ccda5fb5a
|
|||
|
3a75b7ac5b
|
|||
|
85ad204974
|
|||
|
f06dbc921a
|
|||
|
b6053729a4
|
|||
|
df1bbf8f5b
|
|||
|
d6c89bcc6b
|
|||
|
b722787ada
|
|||
|
862f0200bb
|
|||
|
73fe9d52ff
|
|||
|
a456864957
|
|||
|
1cc60c8a10
|
|||
|
eb740dbb43
|
|||
|
bbc7e857e5
|
|||
|
3d8fd32488
|
|||
|
244f91d97b
|
|||
|
b7c1da138b
|
|||
|
95716a0fc3
|
|||
|
ffdc193671
|
|||
|
ec1518facc
|
|||
|
e26b8dd6a1
|
|||
|
332ad4448b
|
|||
|
1f3bdb988a
|
|||
|
ce25751c5d
|
|||
|
a18ef33ee2
|
|||
|
1f9c09668d
|
|||
|
eed5673f95
|
|||
|
ba9881ff62
|
|||
|
5096e01a60
|
|||
|
ea6830e343
|
|||
|
6dc26e48a3
|
|||
|
42f61de502
|
|||
|
cf8d0743ce
|
|||
|
8827131264
|
|||
|
b1fab91e0e
|
|||
|
27c8018d89
|
|||
|
b172ace0ee
|
|||
|
e9fa27be3a
|
|||
|
5a27cb634d
|
|||
|
2874c63dd1
|
|||
|
a94fcd75af
|
|||
|
017b7be8a8
|
|||
|
778e695941
|
|||
|
2ed68e4210
|
|||
|
0329be6124
|
|||
|
8f7afed132
|
|||
|
976f66d383
|
|||
|
f48e2a6a81
|
|||
|
adadcb4a77
|
|||
|
abab89b89c
|
|||
|
2cd33386ef
|
|||
|
f509500877
|
|||
|
4b5dd36778
|
|||
|
b0bf9e3ee0
|
|||
|
e886f97eb9
|
|||
|
ee9add1b53
|
|||
|
b131b1d313
|
|||
|
ab8a3f4889
|
|||
|
5428d063a7
|
|||
|
6f09022563
|
|||
|
855d21fd8a
|
|||
|
88e0be0cdd
|
|||
|
ff8625f04e
|
|||
|
f7367e6270
|
|||
|
8b3951d99d
|
|||
|
d5b1b6f020
|
|||
|
e1ab2ae9dc
|
|||
|
454cf33d6b
|
|||
|
bfb09213e0
|
|||
|
3ab8ca8c1e
|
|||
|
3f29ba3db8
|
|||
|
c7b99f6fa9
|
|||
|
b27c2f8d73
|
|||
|
4b93e8ff97
|
|||
|
2f2a74b810
|
|||
|
1ee0389de8
|
|||
|
b5a1b4af33
|
|||
|
2c2a0951a5
|
|||
|
7a4c2d52b1
|
|||
|
5d43578e03
|
|||
|
d8f030667f
|
|||
|
06312abe42
|
|||
|
6a47fb00ad
|
|||
|
7fa4f5d133
|
|||
|
4f8658b773
|
|||
|
eb3b45da64
|
|||
|
f65135cb5e
|
|||
|
ff885dd5fd
|
|||
|
51c1d6b912
|
|||
|
4930e5cc45
|
|||
|
4f650f5ffb
|
|||
|
7520796869
|
|||
| 094d11835c | |||
|
4e9516871f
|
|||
|
449663ddd7
|
|||
|
b67fdeef8c
|
|||
|
01485f3cd4
|
|||
|
7258ea57b2
|
|||
|
7d9a00cc4e
|
|||
|
4e79820e14
|
|||
|
4c4d14b705
|
|||
|
8d4a729d7c
|
|||
|
cd045d353c
|
|||
|
eb920ce138
|
|||
|
c703a5bafd
|
|||
|
adc17507ce
|
|||
|
b5cac4be41
|
|||
|
b976465e37
|
|||
|
cf7b2cb3a5
|
|||
|
54c6320f73
|
|||
|
e422dc0b89
|
|||
|
258258920d
|
|||
|
d9b73479c7
|
|||
|
2154279d78
|
|||
|
c010073e0c
|
|||
|
fffc4c6b90
|
|||
|
f77d0b0bb1
|
|||
|
e212283d3c
|
|||
|
e392e7ffa5
|
|||
|
8827b2fa21
|
|||
|
b8c154af60
|
|||
|
14cefd1198
|
|||
|
c5f1c5308b
|
|||
|
dee165008d
|
|||
| 7f222eaf10 | |||
| 071157e5bf | |||
| bc206e8f6b | |||
| 5730ce2991 | |||
| c23f1a4b4d | |||
| 54fb107d3b | |||
|
07ad26b54a
|
|||
|
600830ba3d
|
|||
|
a29264093a
|
|||
|
146516c52e
|
|||
|
af9f2e092e
|
|||
|
547f253837
|
|||
|
952961b9fd
|
|||
|
76969b5499
|
|||
|
df79f06ff0
|
|||
|
1a3cc0def7
|
|||
|
ca097efb33
|
|||
|
d4cb390721
|
|||
|
0a7195c93c
|
|||
|
223c01392a
|
|||
|
600257a7c4
|
|||
|
0cfbf22c18
|
|||
|
6c213369da
|
|||
|
bc270834e0
|
|||
|
2bdc23400a
|
|||
|
efd2c53754
|
|||
|
a370742d2b
|
|||
|
12d967154c
|
|||
|
6ed3f96155
|
|||
|
c3ea605924
|
|||
|
1de15cba7e
|
|||
|
5a6cac470a
|
|||
|
a337c0ccea
|
|||
|
2fe3ee6fc1
|
|||
|
f716e7ec47
|
|||
|
283c9b049e
|
|||
|
8297af8302
|
|||
|
68944f7d78
|
|||
|
f8a683b312
|
|||
|
30ac7aab1e
|
|||
|
68ca798b47
|
|||
|
e3626eafd4
|
|||
|
3cd614e6a2
|
|||
|
b7073e9104
|
|||
|
47cff3f927
|
|||
|
b1c232f388
|
|||
|
8ffeba47df
|
|||
|
a052c8a33d
|
|||
|
bd80ffa870
|
|||
|
6f836a93e9
|
|||
|
30ad6d1bb4
|
|||
|
49118b65a1
|
|||
| 93536ed45e | |||
| 08acbeecad | |||
| 66163c7e7d | |||
| 3b98c8b7b5 | |||
| dc33d1e841 | |||
| 3335942374 | |||
| 9177206dd9 | |||
| 80afb928ae | |||
| eec5aaf811 | |||
| d7d2b3f790 | |||
| 758dde97a2 | |||
| 0b8c408c17 | |||
| bb3464e32f | |||
| 319e935d97 | |||
| dc3fd63a83 | |||
| 1be5b3d4c2 | |||
| 1353e2a29b | |||
| 5090ab71b3 | |||
| db6dff304e | |||
| 04f8fd019a | |||
| 2ee217a1a1 | |||
| b00f054bd4 | |||
| b615b68e40 | |||
| bf19ffb38d | |||
| 060454780f | |||
| b9005acffd | |||
| f4b92a0f20 | |||
| 3ebd185a7e | |||
| 0d6a35a933 | |||
| e12a3dfa4f | |||
| 1a282f5ec6 | |||
| bbe37c4fd2 | |||
| 66baa73fa9 | |||
| baf9540071 | |||
| 5ee5b32d64 | |||
| 5dc2b923d1 | |||
|
fa26d154b1
|
|||
|
73e57d1cf1
|
|||
| 0de1d1bd6b | |||
| 1d0db79ca3 | |||
| ae40e5cce3 | |||
| 984dfc09ec | |||
| 1b97bc9795 | |||
| 8a4e9f5ed3 | |||
| 3f9f515296 | |||
| 3510a7910b | |||
| 0f9f00c441 | |||
| d49b379481 | |||
| 4c7b89b4e3 | |||
| d5cd2c6204 | |||
| 82009494b5 | |||
| 65662a4f9b | |||
| 00f82646c2 | |||
| 4d959b649f | |||
| 4d0a9d6d90 | |||
| 62f96a1e53 | |||
| 0b7158f465 | |||
| 8b9da52fe7 | |||
| 9d3b690aca | |||
| 599fa42bb3 | |||
| 59ee8f5760 | |||
| 20d08f6c7c | |||
| e3deac2731 | |||
| 070409c280 | |||
| 19a4440e7f | |||
| 4e43b5b533 | |||
| a1c0755ccc | |||
| 6d058d4c7d | |||
| 621865d69f | |||
| 59dde88b2e | |||
| 0459bf16b1 | |||
| 65e1073ea0 | |||
| a4b014d66a | |||
| 66989b306f | |||
| c510efb331 | |||
| 1955d2491d | |||
| 4f35be539f | |||
| f637cf23d3 | |||
| 53ce663adc | |||
| 630a02789c | |||
| 088a683c62 | |||
| 55c70f0fb7 | |||
| 86ca1a60fb | |||
| c1a7eaa72d | |||
| ca88eec131 | |||
| 6591fcfec7 | |||
| 81890b15a8 | |||
| a7719bd7a9 | |||
| c5b441f75b | |||
| a1d56dc1d0 | |||
| d085f96838 | |||
| 273283708e | |||
| e243f0e4cc | |||
| 28dab2d00f | |||
| e025be5fff | |||
| db792cc9d8 | |||
| 4b1252a3e3 | |||
| eb3c69df39 | |||
| 7b75049f4f | |||
| 6d2d647511 | |||
| ae5edabc27 | |||
| 091810c6cb | |||
| 63ba6882cd | |||
| 8e59e205a8 | |||
| 4b34a39745 | |||
| 144afbae8e | |||
| 3cc36d9c24 | |||
| cadb60f99d | |||
| 6ce1b5be9d | |||
| 0aa2fcbbbb | |||
| 0d8314cab6 | |||
| b7fb9adef7 | |||
| 410fc89185 | |||
| d7f5dc83f8 | |||
| ab9604dee9 | |||
| 673dc372e7 | |||
| 1ebdbe91a9 | |||
| adddfc1c3b | |||
| 0a8a98ea54 | |||
| d8e9397cf6 | |||
| ef6d45b878 | |||
| 8425d0bab6 | |||
| 1a2ae96b6a | |||
| 5d83fba8f5 | |||
| 30bf0f56ac | |||
| 079313313d | |||
| ab53d3ae3b | |||
| 81195f2159 | |||
| da31275a30 | |||
| 046449974e | |||
| e5004e9a67 | |||
| 8718a5d8d1 | |||
| 6f9658375f | |||
| 8b28f3615f | |||
| cf67574571 | |||
| 55fde37924 | |||
| a53bd7a08f | |||
| a7ae47ff00 | |||
| 2dfe4a30dc | |||
| 176b3e306c | |||
| 3a739069c0 | |||
| c24d5d0152 | |||
| 211c3baf16 | |||
| 1bfc23fec7 | |||
| a8f7e9c8de | |||
| 5aa9502e37 | |||
| dc376c9afb | |||
| 0073f11c15 | |||
| 2b6df0e738 | |||
| 44a4c0df24 | |||
| bfc5e531cc | |||
| dbb58fa2b7 | |||
| 0e856073be | |||
| 9f2306aa71 | |||
| 995ddd7306 | |||
| 4d32f017a8 | |||
| 55693bb6e8 | |||
| a4a3f1464e | |||
| 5f3ea48ef7 | |||
| cf11c16096 | |||
| c4ca178097 | |||
| 56041874da | |||
| 1199ab85e9 | |||
| 7bd1a70607 | |||
| a22a648b5d | |||
| 715e6eb9aa | |||
| 0daf9e27ae | |||
| dfc0f220e8 | |||
| f610bf7db3 | |||
| b9c58844f0 | |||
| 3dbf590472 | |||
| af9b44f4d4 | |||
| b3293f1de3 | |||
| 7b4bcb6f04 | |||
| 1500a26761 | |||
| 8811aa59ee | |||
| 88ccc7ab81 | |||
| 4a3273653a | |||
| 25eec2a7a8 | |||
| 548aa540ab | |||
| b257f69389 | |||
| 2feecbfe4f | |||
| dba2f0a9fb | |||
| b98475037c | |||
| 10c4b735cf | |||
| 04706151b1 | |||
| 3dd02d4208 | |||
| 8608ec76ad | |||
| 998f454a3e | |||
| 50e1c44722 | |||
| f7f0e46021 | |||
| 042aab7199 | |||
| 90d57a290e | |||
| 3489a74855 | |||
| d9a496cfaf | |||
| 80aadf54ab | |||
| e4db312814 | |||
| 6e34323cb1 | |||
| df409a2cd3 | |||
| 7828402ab6 | |||
| ef1ffb11ec | |||
| af50c2b6ee | |||
| 62471c0336 | |||
| 5205c067fc | |||
| 1a88a5e5de | |||
| 964a131408 | |||
| 4c5e04bfc3 | |||
| b5f40fc58a | |||
| b042d679fa | |||
| d44390db00 | |||
| 7eaa39ea4a | |||
| d7d5674683 | |||
| 494d8979a8 | |||
| 9a4a238dcf | |||
| e60f3db4b2 | |||
| 91e599cae0 | |||
| 245e025134 | |||
| ee7f0360fb | |||
| 22482a34c3 | |||
| 0a1447bb4d | |||
| c67e8feb9c | |||
| 822a19588f | |||
| a0ca4ceeda | |||
| a8e09f2f0b | |||
| be497b78cb | |||
| a3b1702710 | |||
| 454ff94e47 | |||
| e5428209d8 | |||
| 37c36d196e | |||
| b142d68ca2 | |||
| 75de1e50b9 | |||
| 90b981ef49 | |||
| 3b606688bc | |||
| 96edf7fc09 | |||
| 57d5b0633c | |||
| 83342628fa | |||
| 20fbd3c2cd | |||
| 2bda78d724 | |||
| e9830adaea | |||
| 079310fcc4 | |||
| a8aeeaca4e | |||
| eba13ac2c2 | |||
| 771cbd1987 | |||
| a664a5f1a1 | |||
| e7ddaa7300 | |||
| a7582f17f2 | |||
| c4007f16e3 | |||
| 3d137fabb1 | |||
| e7512b3cf0 | |||
| 2c128127e6 | |||
| 004eea0000 | |||
| 28cd7fe577 | |||
| 11f1a095f6 | |||
| e609022356 | |||
| 5f91287369 | |||
| 4e9f1d2be6 | |||
| ee25271275 | |||
| bceb049e0e | |||
| cbd500edae | |||
| 74d06e7a7c | |||
| a941c77039 | |||
| 1be08b245c | |||
| 6ef16f2cf4 | |||
| 025eb237b2 |
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
# Enable CPU optimizations for better performance
|
# Enable CPU optimizations for better performance
|
||||||
# Set target-cpu to native to use all available CPU features on the build machine
|
# Set target-cpu to native to use all available CPU features on the build machine
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
|
[submodule "docs"]
|
||||||
|
path = docs
|
||||||
|
url = http://github.com/sgrams/trx-rs.wiki.git
|
||||||
@@ -39,30 +39,24 @@ This is a Cargo workspace. All crates live under `src/`:
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
trx-core/ # Core types, traits, state machine, controller (~3,500 LOC)
|
trx-core/ # Core types, traits, state machine, controller
|
||||||
trx-protocol/ # Client↔server protocol DTOs, auth, codec, mapping (~1,100 LOC)
|
trx-protocol/ # Client↔server protocol conversion, auth, codec
|
||||||
trx-app/ # Shared application helpers (config paths, logging init)
|
trx-app/ # Shared application helpers (config, plugins, logging)
|
||||||
trx-reporting/ # PSKReporter UDP uplink + APRS-IS TCP uplink (~1,150 LOC)
|
trx-server/ # Server binary (rig_task, audio, APRS-IS, PSKReporter)
|
||||||
trx-server/ # Server binary: rig_task, audio pipeline, listener (~3,700 LOC)
|
trx-backend/ # Backend abstraction trait + factory
|
||||||
trx-backend/ # Backend abstraction trait + factory + dummy
|
trx-backend-ft817/ # Yaesu FT-817 CAT implementation
|
||||||
trx-backend-ft817/ # Yaesu FT-817 binary CAT (BCD encoding)
|
trx-backend-ft450d/ # Yaesu FT-450D CAT implementation
|
||||||
trx-backend-ft450d/ # Yaesu FT-450D ASCII CAT
|
trx-client/ # Client binary (connects to server, runs frontends)
|
||||||
trx-backend-soapysdr/ # SoapySDR RX with full DSP pipeline (~5,000+ LOC)
|
trx-frontend/ # Frontend trait (FrontendSpawner)
|
||||||
trx-client/ # Client binary: remote connection, frontend spawning (~1,500 LOC)
|
trx-frontend-http/ # Web UI with REST API, SSE, and auth
|
||||||
trx-frontend/ # Frontend trait (FrontendSpawner), runtime context
|
|
||||||
trx-frontend-http/ # Web UI: REST API, SSE, WebSocket audio, session auth
|
|
||||||
trx-frontend-http-json/ # JSON-over-TCP control frontend
|
trx-frontend-http-json/ # JSON-over-TCP control frontend
|
||||||
trx-frontend-rigctl/ # Hamlib-compatible rigctl TCP interface
|
trx-frontend-rigctl/ # Hamlib-compatible rigctl TCP interface
|
||||||
trx-configurator/ # Interactive setup wizard
|
|
||||||
decoders/
|
decoders/
|
||||||
trx-aprs/ # APRS packet decoder (AX.25 + APRS-IS)
|
trx-aprs/ # APRS packet decoder
|
||||||
trx-cw/ # CW (Morse) decoder (Goertzel tone detection)
|
trx-cw/ # CW (Morse) decoder
|
||||||
trx-ftx/ # Pure Rust FTx decoder (FT8/FT4/FT2, LDPC/OSD) (~3,000+ LOC)
|
trx-ft8/ # FT8 decoder (wraps external ft8_lib C library)
|
||||||
trx-wspr/ # WSPR weak-signal decoder
|
trx-wspr/ # WSPR decoder
|
||||||
trx-ais/ # AIS maritime transponder decoder
|
trx-decode-log/ # Shared decoder logging (JSON Lines, date-rotated files)
|
||||||
trx-rds/ # RDS decoder for WFM (~2,000 LOC)
|
|
||||||
trx-vdes/ # VDES maritime data exchange decoder (~1,300 LOC)
|
|
||||||
trx-decode-log/ # JSON Lines file logging with date rotation
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -71,14 +65,14 @@ The project is split into a **server** (connects to the radio hardware) and a **
|
|||||||
|
|
||||||
### Data flow
|
### Data flow
|
||||||
|
|
||||||
```mermaid
|
```
|
||||||
graph TD
|
Radio hardware
|
||||||
Radio["Radio Hardware"] <-->|serial / TCP| Server["trx-server (rig_task.rs)"]
|
↕ serial/TCP
|
||||||
Server <-->|"JSON-TCP :4530"| Client["trx-client (remote_client.rs)"]
|
trx-server (rig_task.rs)
|
||||||
Server -->|"Opus-TCP :4531"| Client
|
↕ trx-protocol JSON-TCP (port 4530)
|
||||||
Client <-->|internal channels| F1["HTTP Frontend :8080"]
|
trx-client (remote_client.rs)
|
||||||
Client <-->|internal channels| F2["rigctl Frontend :4532"]
|
↕ internal channels
|
||||||
Client <-->|internal channels| F3["JSON-TCP Frontend"]
|
Frontends: HTTP (8080), rigctl (4532), http-json (ephemeral)
|
||||||
```
|
```
|
||||||
|
|
||||||
### trx-core controller
|
### trx-core controller
|
||||||
@@ -92,11 +86,11 @@ The rig controller (`src/trx-core/src/rig/controller/`) is the central state man
|
|||||||
|
|
||||||
### Decoders
|
### Decoders
|
||||||
|
|
||||||
Signal decoders run as background tasks in `trx-server`, consuming decoded audio. `trx-ftx` provides the FT8/FT4/FT2 decoder in pure Rust. Decoded frames can be forwarded to PSKReporter and APRS-IS (IGate) uplinks, or logged via `trx-decode-log`.
|
Signal decoders run as background tasks in `trx-server`, consuming decoded audio. `trx-ft8` wraps a C library (`external/ft8_lib`). Decoded frames can be forwarded to PSKReporter and APRS-IS (IGate) uplinks, or logged via `trx-decode-log`.
|
||||||
|
|
||||||
## Diagrams
|
### Plugin system
|
||||||
|
|
||||||
Always use [Mermaid](https://mermaid.js.org/) for diagrams in Markdown files. Never use ASCII art, box-drawing characters, or plain-text diagrams. GitHub renders Mermaid natively in ```mermaid fenced code blocks.
|
Both `trx-server` and `trx-client` can load shared-library plugins exporting a `trx_register` symbol. Search paths: `./plugins`, `~/.config/trx-rs/plugins`, `TRX_PLUGIN_DIRS` env var.
|
||||||
|
|
||||||
## Commit Format
|
## Commit Format
|
||||||
|
|
||||||
@@ -105,27 +99,3 @@ Always use [Mermaid](https://mermaid.js.org/) for diagrams in Markdown files. Ne
|
|||||||
```
|
```
|
||||||
|
|
||||||
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`. Use `(trx-rs)` for repo-wide changes. Sign commits with `git commit -s`. Write isolated commits per crate.
|
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`. Use `(trx-rs)` for repo-wide changes. Sign commits with `git commit -s`. Write isolated commits per crate.
|
||||||
|
|
||||||
## Codebase Review Observations
|
|
||||||
|
|
||||||
Full architecture documentation: `docs/Architecture.md`
|
|
||||||
Improvement plan: `docs/Improvement-Areas.md`
|
|
||||||
|
|
||||||
*Last reviewed: 2026-03-29*
|
|
||||||
|
|
||||||
### Strengths
|
|
||||||
|
|
||||||
- **Explicit state machine**: `RigMachineState` FSM (7 states) prevents invalid states with a deterministic transition table and exhaustive matching. Well-tested with lifecycle, error recovery, and invalid transition tests. `ReadyStateData`/`TransmittingStateData` use `pub(crate)` fields with controlled accessors.
|
|
||||||
- **Trait-based polymorphism**: Clean abstraction boundaries (`RigCat`, `RigSdr`, `AudioSource`, `RigListener`, `RigCommandHandler`, `CommandExecutor`, `TokenValidator`, `FrontendSpawner`) enable loose coupling and testability. `RigCat`/`RigSdr` split cleanly separates CAT ops from SDR-specific methods.
|
|
||||||
- **Multi-rig architecture**: Per-rig task isolation with `HashMap<rig_id, RigHandle>` routing, per-rig state/spectrum/audio/decoder-history channels, dual-connection model (main + spectrum) in the client, and backward-compatible single-rig mode.
|
|
||||||
- **Async concurrency model**: Proper use of tokio channels -- `watch` for state snapshots, `broadcast` for PCM/decode fan-out, `mpsc` for commands. No mutex contention on hot paths. Spectrum deduplication collapses concurrent GetSpectrum requests.
|
|
||||||
- **Comprehensive SDR support**: Full DSP pipeline with multi-mode demodulation (SSB, AM, SAM, FM, WFM, AIS, VDES), virtual channel management, squelch, noise blanker, spectrum FFT, RDS decoding. AVX2-optimized FM discriminator with scalar fallbacks.
|
|
||||||
- **Pure Rust decoders**: FT8/FT4/FT2, APRS, CW, WSPR, AIS, VDES, RDS -- all implemented without C FFI dependencies. Consistent decoder pattern: stateful struct → `process_block()` → `decode_if_ready()`.
|
|
||||||
- **Good test coverage** in protocol layer: codec, mapping, auth all have thorough unit tests with round-trip verification. 45+ mapping tests cover all command variants.
|
|
||||||
- **Feature-gated backends**: ft817, ft450d, soapysdr compiled conditionally to minimize binary size. Factory pattern with name normalization for registration.
|
|
||||||
- **Defensive error handling**: Lock poisoning recovery, stream error deduplication with 60s summaries, input truncation in logs (128 chars), per-IP rate limiting on auth endpoints.
|
|
||||||
- **Well-documented DSP guidelines**: `docs/Optimization-Guidelines.md` captures lessons on NCO design, polyphase resampling, AVX2 batching, and stereo FM decoding.
|
|
||||||
|
|
||||||
### Areas for Improvement
|
|
||||||
|
|
||||||
All P0–P3 items resolved or dropped. See `docs/Improvement-Areas.md` for details.
|
|
||||||
|
|||||||
Generated
+352
-1050
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||||
|
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|||||||
+3
-6
@@ -1,18 +1,16 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
# SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"src/decoders/trx-ais",
|
"src/decoders/trx-ais",
|
||||||
"src/decoders/trx-wxsat",
|
|
||||||
"src/decoders/trx-aprs",
|
"src/decoders/trx-aprs",
|
||||||
"src/decoders/trx-cw",
|
"src/decoders/trx-cw",
|
||||||
"src/decoders/trx-decode-log",
|
"src/decoders/trx-decode-log",
|
||||||
"src/decoders/trx-ftx",
|
"src/decoders/trx-ft8",
|
||||||
"src/decoders/trx-rds",
|
"src/decoders/trx-rds",
|
||||||
"src/decoders/trx-vdes",
|
"src/decoders/trx-vdes",
|
||||||
"src/decoders/trx-wefax",
|
|
||||||
"src/decoders/trx-wspr",
|
"src/decoders/trx-wspr",
|
||||||
"src/trx-core",
|
"src/trx-core",
|
||||||
"src/trx-protocol",
|
"src/trx-protocol",
|
||||||
@@ -28,7 +26,6 @@ members = [
|
|||||||
"src/trx-client/trx-frontend/trx-frontend-http",
|
"src/trx-client/trx-frontend/trx-frontend-http",
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http-json",
|
"src/trx-client/trx-frontend/trx-frontend-http-json",
|
||||||
"src/trx-client/trx-frontend/trx-frontend-rigctl",
|
"src/trx-client/trx-frontend/trx-frontend-rigctl",
|
||||||
"src/trx-configurator",
|
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -1,338 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 2, June 1991
|
|
||||||
|
|
||||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
|
||||||
<https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The licenses for most software are designed to take away your
|
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
|
||||||
License is intended to guarantee your freedom to share and change free
|
|
||||||
software--to make sure the software is free for all its users. This
|
|
||||||
General Public License applies to most of the Free Software
|
|
||||||
Foundation's software and to any other program whose authors commit to
|
|
||||||
using it. (Some other Free Software Foundation software is covered by
|
|
||||||
the GNU Lesser General Public License instead.) You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
this service if you wish), that you receive source code or can get it
|
|
||||||
if you want it, that you can change the software or use pieces of it
|
|
||||||
in new free programs; and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to make restrictions that forbid
|
|
||||||
anyone to deny you these rights or to ask you to surrender the rights.
|
|
||||||
These restrictions translate to certain responsibilities for you if you
|
|
||||||
distribute copies of the software, or if you modify it.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must give the recipients all the rights that
|
|
||||||
you have. You must make sure that they, too, receive or can get the
|
|
||||||
source code. And you must show them these terms so they know their
|
|
||||||
rights.
|
|
||||||
|
|
||||||
We protect your rights with two steps: (1) copyright the software, and
|
|
||||||
(2) offer you this license which gives you legal permission to copy,
|
|
||||||
distribute and/or modify the software.
|
|
||||||
|
|
||||||
Also, for each author's protection and ours, we want to make certain
|
|
||||||
that everyone understands that there is no warranty for this free
|
|
||||||
software. If the software is modified by someone else and passed on, we
|
|
||||||
want its recipients to know that what they have is not the original, so
|
|
||||||
that any problems introduced by others will not reflect on the original
|
|
||||||
authors' reputations.
|
|
||||||
|
|
||||||
Finally, any free program is threatened constantly by software
|
|
||||||
patents. We wish to avoid the danger that redistributors of a free
|
|
||||||
program will individually obtain patent licenses, in effect making the
|
|
||||||
program proprietary. To prevent this, we have made it clear that any
|
|
||||||
patent must be licensed for everyone's free use or not licensed at all.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. This License applies to any program or other work which contains
|
|
||||||
a notice placed by the copyright holder saying it may be distributed
|
|
||||||
under the terms of this General Public License. The "Program", below,
|
|
||||||
refers to any such program or work, and a "work based on the Program"
|
|
||||||
means either the Program or any derivative work under copyright law:
|
|
||||||
that is to say, a work containing the Program or a portion of it,
|
|
||||||
either verbatim or with modifications and/or translated into another
|
|
||||||
language. (Hereinafter, translation is included without limitation in
|
|
||||||
the term "modification".) Each licensee is addressed as "you".
|
|
||||||
|
|
||||||
Activities other than copying, distribution and modification are not
|
|
||||||
covered by this License; they are outside its scope. The act of
|
|
||||||
running the Program is not restricted, and the output from the Program
|
|
||||||
is covered only if its contents constitute a work based on the
|
|
||||||
Program (independent of having been made by running the Program).
|
|
||||||
Whether that is true depends on what the Program does.
|
|
||||||
|
|
||||||
1. You may copy and distribute verbatim copies of the Program's
|
|
||||||
source code as you receive it, in any medium, provided that you
|
|
||||||
conspicuously and appropriately publish on each copy an appropriate
|
|
||||||
copyright notice and disclaimer of warranty; keep intact all the
|
|
||||||
notices that refer to this License and to the absence of any warranty;
|
|
||||||
and give any other recipients of the Program a copy of this License
|
|
||||||
along with the Program.
|
|
||||||
|
|
||||||
You may charge a fee for the physical act of transferring a copy, and
|
|
||||||
you may at your option offer warranty protection in exchange for a fee.
|
|
||||||
|
|
||||||
2. You may modify your copy or copies of the Program or any portion
|
|
||||||
of it, thus forming a work based on the Program, and copy and
|
|
||||||
distribute such modifications or work under the terms of Section 1
|
|
||||||
above, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) You must cause the modified files to carry prominent notices
|
|
||||||
stating that you changed the files and the date of any change.
|
|
||||||
|
|
||||||
b) You must cause any work that you distribute or publish, that in
|
|
||||||
whole or in part contains or is derived from the Program or any
|
|
||||||
part thereof, to be licensed as a whole at no charge to all third
|
|
||||||
parties under the terms of this License.
|
|
||||||
|
|
||||||
c) If the modified program normally reads commands interactively
|
|
||||||
when run, you must cause it, when started running for such
|
|
||||||
interactive use in the most ordinary way, to print or display an
|
|
||||||
announcement including an appropriate copyright notice and a
|
|
||||||
notice that there is no warranty (or else, saying that you provide
|
|
||||||
a warranty) and that users may redistribute the program under
|
|
||||||
these conditions, and telling the user how to view a copy of this
|
|
||||||
License. (Exception: if the Program itself is interactive but
|
|
||||||
does not normally print such an announcement, your work based on
|
|
||||||
the Program is not required to print an announcement.)
|
|
||||||
|
|
||||||
These requirements apply to the modified work as a whole. If
|
|
||||||
identifiable sections of that work are not derived from the Program,
|
|
||||||
and can be reasonably considered independent and separate works in
|
|
||||||
themselves, then this License, and its terms, do not apply to those
|
|
||||||
sections when you distribute them as separate works. But when you
|
|
||||||
distribute the same sections as part of a whole which is a work based
|
|
||||||
on the Program, the distribution of the whole must be on the terms of
|
|
||||||
this License, whose permissions for other licensees extend to the
|
|
||||||
entire whole, and thus to each and every part regardless of who wrote it.
|
|
||||||
|
|
||||||
Thus, it is not the intent of this section to claim rights or contest
|
|
||||||
your rights to work written entirely by you; rather, the intent is to
|
|
||||||
exercise the right to control the distribution of derivative or
|
|
||||||
collective works based on the Program.
|
|
||||||
|
|
||||||
In addition, mere aggregation of another work not based on the Program
|
|
||||||
with the Program (or with a work based on the Program) on a volume of
|
|
||||||
a storage or distribution medium does not bring the other work under
|
|
||||||
the scope of this License.
|
|
||||||
|
|
||||||
3. You may copy and distribute the Program (or a work based on it,
|
|
||||||
under Section 2) in object code or executable form under the terms of
|
|
||||||
Sections 1 and 2 above provided that you also do one of the following:
|
|
||||||
|
|
||||||
a) Accompany it with the complete corresponding machine-readable
|
|
||||||
source code, which must be distributed under the terms of Sections
|
|
||||||
1 and 2 above on a medium customarily used for software interchange; or,
|
|
||||||
|
|
||||||
b) Accompany it with a written offer, valid for at least three
|
|
||||||
years, to give any third party, for a charge no more than your
|
|
||||||
cost of physically performing source distribution, a complete
|
|
||||||
machine-readable copy of the corresponding source code, to be
|
|
||||||
distributed under the terms of Sections 1 and 2 above on a medium
|
|
||||||
customarily used for software interchange; or,
|
|
||||||
|
|
||||||
c) Accompany it with the information you received as to the offer
|
|
||||||
to distribute corresponding source code. (This alternative is
|
|
||||||
allowed only for noncommercial distribution and only if you
|
|
||||||
received the program in object code or executable form with such
|
|
||||||
an offer, in accord with Subsection b above.)
|
|
||||||
|
|
||||||
The source code for a work means the preferred form of the work for
|
|
||||||
making modifications to it. For an executable work, complete source
|
|
||||||
code means all the source code for all modules it contains, plus any
|
|
||||||
associated interface definition files, plus the scripts used to
|
|
||||||
control compilation and installation of the executable. However, as a
|
|
||||||
special exception, the source code distributed need not include
|
|
||||||
anything that is normally distributed (in either source or binary
|
|
||||||
form) with the major components (compiler, kernel, and so on) of the
|
|
||||||
operating system on which the executable runs, unless that component
|
|
||||||
itself accompanies the executable.
|
|
||||||
|
|
||||||
If distribution of executable or object code is made by offering
|
|
||||||
access to copy from a designated place, then offering equivalent
|
|
||||||
access to copy the source code from the same place counts as
|
|
||||||
distribution of the source code, even though third parties are not
|
|
||||||
compelled to copy the source along with the object code.
|
|
||||||
|
|
||||||
4. You may not copy, modify, sublicense, or distribute the Program
|
|
||||||
except as expressly provided under this License. Any attempt
|
|
||||||
otherwise to copy, modify, sublicense or distribute the Program is
|
|
||||||
void, and will automatically terminate your rights under this License.
|
|
||||||
However, parties who have received copies, or rights, from you under
|
|
||||||
this License will not have their licenses terminated so long as such
|
|
||||||
parties remain in full compliance.
|
|
||||||
|
|
||||||
5. You are not required to accept this License, since you have not
|
|
||||||
signed it. However, nothing else grants you permission to modify or
|
|
||||||
distribute the Program or its derivative works. These actions are
|
|
||||||
prohibited by law if you do not accept this License. Therefore, by
|
|
||||||
modifying or distributing the Program (or any work based on the
|
|
||||||
Program), you indicate your acceptance of this License to do so, and
|
|
||||||
all its terms and conditions for copying, distributing or modifying
|
|
||||||
the Program or works based on it.
|
|
||||||
|
|
||||||
6. Each time you redistribute the Program (or any work based on the
|
|
||||||
Program), the recipient automatically receives a license from the
|
|
||||||
original licensor to copy, distribute or modify the Program subject to
|
|
||||||
these terms and conditions. You may not impose any further
|
|
||||||
restrictions on the recipients' exercise of the rights granted herein.
|
|
||||||
You are not responsible for enforcing compliance by third parties to
|
|
||||||
this License.
|
|
||||||
|
|
||||||
7. If, as a consequence of a court judgment or allegation of patent
|
|
||||||
infringement or for any other reason (not limited to patent issues),
|
|
||||||
conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot
|
|
||||||
distribute so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you
|
|
||||||
may not distribute the Program at all. For example, if a patent
|
|
||||||
license would not permit royalty-free redistribution of the Program by
|
|
||||||
all those who receive copies directly or indirectly through you, then
|
|
||||||
the only way you could satisfy both it and this License would be to
|
|
||||||
refrain entirely from distribution of the Program.
|
|
||||||
|
|
||||||
If any portion of this section is held invalid or unenforceable under
|
|
||||||
any particular circumstance, the balance of the section is intended to
|
|
||||||
apply and the section as a whole is intended to apply in other
|
|
||||||
circumstances.
|
|
||||||
|
|
||||||
It is not the purpose of this section to induce you to infringe any
|
|
||||||
patents or other property right claims or to contest validity of any
|
|
||||||
such claims; this section has the sole purpose of protecting the
|
|
||||||
integrity of the free software distribution system, which is
|
|
||||||
implemented by public license practices. Many people have made
|
|
||||||
generous contributions to the wide range of software distributed
|
|
||||||
through that system in reliance on consistent application of that
|
|
||||||
system; it is up to the author/donor to decide if he or she is willing
|
|
||||||
to distribute software through any other system and a licensee cannot
|
|
||||||
impose that choice.
|
|
||||||
|
|
||||||
This section is intended to make thoroughly clear what is believed to
|
|
||||||
be a consequence of the rest of this License.
|
|
||||||
|
|
||||||
8. If the distribution and/or use of the Program is restricted in
|
|
||||||
certain countries either by patents or by copyrighted interfaces, the
|
|
||||||
original copyright holder who places the Program under this License
|
|
||||||
may add an explicit geographical distribution limitation excluding
|
|
||||||
those countries, so that distribution is permitted only in or among
|
|
||||||
countries not thus excluded. In such case, this License incorporates
|
|
||||||
the limitation as if written in the body of this License.
|
|
||||||
|
|
||||||
9. The Free Software Foundation may publish revised and/or new versions
|
|
||||||
of the General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Program
|
|
||||||
specifies a version number of this License which applies to it and "any
|
|
||||||
later version", you have the option of following the terms and conditions
|
|
||||||
either of that version or of any later version published by the Free
|
|
||||||
Software Foundation. If the Program does not specify a version number of
|
|
||||||
this License, you may choose any version ever published by the Free Software
|
|
||||||
Foundation.
|
|
||||||
|
|
||||||
10. If you wish to incorporate parts of the Program into other free
|
|
||||||
programs whose distribution conditions are different, write to the author
|
|
||||||
to ask for permission. For software which is copyrighted by the Free
|
|
||||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
|
||||||
make exceptions for this. Our decision will be guided by the two goals
|
|
||||||
of preserving the free status of all derivatives of our free software and
|
|
||||||
of promoting the sharing and reuse of software generally.
|
|
||||||
|
|
||||||
NO WARRANTY
|
|
||||||
|
|
||||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
|
||||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
|
||||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
|
||||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
|
||||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
|
||||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
|
||||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
|
||||||
REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
|
||||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
|
||||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
|
||||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
|
||||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
|
||||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGES.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
convey the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 2 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along
|
|
||||||
with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program is interactive, make it output a short notice like this
|
|
||||||
when it starts in an interactive mode:
|
|
||||||
|
|
||||||
Gnomovision version 69, Copyright (C) year name of author
|
|
||||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, the commands you use may
|
|
||||||
be called something other than `show w' and `show c'; they could even be
|
|
||||||
mouse-clicks or menu items--whatever suits your program.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or your
|
|
||||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
|
||||||
necessary. Here is a sample; alter the names:
|
|
||||||
|
|
||||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
|
||||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
|
||||||
|
|
||||||
<signature of Moe Ghoul>, 1 April 1989
|
|
||||||
Moe Ghoul, President of Vice
|
|
||||||
|
|
||||||
This General Public License does not permit incorporating your program into
|
|
||||||
proprietary programs. If your program is a subroutine library, you may
|
|
||||||
consider it more useful to permit linking proprietary applications with the
|
|
||||||
library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License.
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
SIL OPEN FONT LICENSE
|
|
||||||
|
|
||||||
Version 1.1 - 26 February 2007
|
|
||||||
|
|
||||||
PREAMBLE
|
|
||||||
|
|
||||||
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
|
|
||||||
|
|
||||||
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
|
|
||||||
|
|
||||||
DEFINITIONS
|
|
||||||
|
|
||||||
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
|
|
||||||
|
|
||||||
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
|
|
||||||
|
|
||||||
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
|
|
||||||
|
|
||||||
"Modified Version" refers to any derivative made by adding to, deleting, or substituting — in part or in whole — any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
|
|
||||||
|
|
||||||
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
|
|
||||||
|
|
||||||
PERMISSION & CONDITIONS
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
|
|
||||||
|
|
||||||
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
|
|
||||||
|
|
||||||
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
|
|
||||||
|
|
||||||
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
|
|
||||||
|
|
||||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
|
|
||||||
|
|
||||||
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
|
|
||||||
|
|
||||||
TERMINATION
|
|
||||||
|
|
||||||
This license becomes null and void if any of the above conditions are not met.
|
|
||||||
|
|
||||||
DISCLAIMER
|
|
||||||
|
|
||||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# DSP Optimization Guidelines
|
# DSP Chain Performance Optimization Guidelines
|
||||||
|
|
||||||
This document captures lessons learned and best practices for optimizing
|
This document captures lessons learned and best practices for optimizing
|
||||||
the real-time DSP pipelines in trx-rs, particularly the WFM stereo decoder
|
the real-time DSP pipelines in trx-rs, particularly the WFM stereo decoder
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# trx-rs Architecture
|
# trx-rs Code Design & Architecture Overview
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
@@ -52,34 +52,42 @@ Target users are amateur radio operators who want networked, automated, or multi
|
|||||||
| CAT serial | tokio-serial |
|
| CAT serial | tokio-serial |
|
||||||
| CLI | clap |
|
| CLI | clap |
|
||||||
| Logging | tracing / tracing-subscriber |
|
| Logging | tracing / tracing-subscriber |
|
||||||
| FTx decode | trx-ftx (pure Rust) |
|
| FT8 decode | ft8_lib (external C library via FFI) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## High-Level Architecture
|
## High-Level Architecture
|
||||||
|
|
||||||
```mermaid
|
```
|
||||||
graph TD
|
┌──────────────────────────────────────────────────────────┐
|
||||||
subgraph server["trx-server"]
|
│ trx-server │
|
||||||
HW["Radio Hardware"] <-->|"CAT protocol<br/>serial / TCP"| Backend["Rig Backend<br/>(ft817 / ft450d / sdr)"]
|
│ │
|
||||||
Backend --> RigTask["rig_task.rs<br/>(state machine)"]
|
│ Radio Hardware (serial/TCP) │
|
||||||
RigTask --> Listener["listener.rs<br/>(JSON TCP :4530)"]
|
│ ↕ CAT protocol │
|
||||||
RigTask --> Audio["audio.rs<br/>(Opus :4531)"]
|
│ Rig Backend ──────── rig_task.rs ─── listener.rs │
|
||||||
Audio --> Decoders["Decoders<br/>(APRS, CW, FT8, WSPR, RDS)"]
|
│ (ft817/ft450d/sdr) (state machine) (JSON TCP :4530) │
|
||||||
Decoders --> Uplinks["PSKReporter / APRS-IS"]
|
│ │ │
|
||||||
end
|
│ audio.rs │
|
||||||
|
│ (Opus :4531) │
|
||||||
subgraph client["trx-client"]
|
│ │ │
|
||||||
Remote["remote_client.rs<br/>(polls state, routes commands)"]
|
│ Decoders │
|
||||||
Remote <-->|"mpsc / watch channels"| HTTP["trx-frontend-http<br/>(Web UI :8080)"]
|
│ (APRS, CW, FT8, WSPR, RDS) │
|
||||||
Remote <-->|"mpsc / watch channels"| Rigctl["trx-frontend-rigctl<br/>(rigctl :4532)"]
|
│ PSKReporter / APRS-IS │
|
||||||
Remote <-->|"mpsc / watch channels"| JSON["trx-frontend-http-json<br/>(JSON/TCP)"]
|
└──────────────────────────────────────────────────────────┘
|
||||||
end
|
↕ JSON TCP (port 4530)
|
||||||
|
↕ Opus audio TCP (port 4531)
|
||||||
Listener <-->|"JSON TCP :4530"| Remote
|
┌──────────────────────────────────────────────────────────┐
|
||||||
Audio -->|"Opus TCP :4531"| Remote
|
│ trx-client │
|
||||||
|
│ │
|
||||||
HTTP & Rigctl & JSON <--> Users["End Users<br/>(Browser / Hamlib / Custom tools)"]
|
│ remote_client.rs (polls state, routes commands) │
|
||||||
|
│ ↕ internal mpsc/watch channels │
|
||||||
|
│ Frontends: │
|
||||||
|
│ trx-frontend-http (Web UI :8080) │
|
||||||
|
│ trx-frontend-rigctl (rigctl :4532) │
|
||||||
|
│ trx-frontend-http-json (JSON/TCP ephemeral) │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
↕ Browser / Hamlib / Custom tools
|
||||||
|
End Users
|
||||||
```
|
```
|
||||||
|
|
||||||
The server and client are separate binaries. They communicate over **JSON-over-TCP** (control) and **Opus-encoded TCP** (audio). Both binaries can load shared-library plugins at startup.
|
The server and client are separate binaries. They communicate over **JSON-over-TCP** (control) and **Opus-encoded TCP** (audio). Both binaries can load shared-library plugins at startup.
|
||||||
@@ -91,6 +99,7 @@ The server and client are separate binaries. They communicate over **JSON-over-T
|
|||||||
```
|
```
|
||||||
trx-rs/ # Workspace root
|
trx-rs/ # Workspace root
|
||||||
├── Cargo.toml # Workspace manifest (shared dependencies)
|
├── Cargo.toml # Workspace manifest (shared dependencies)
|
||||||
|
├── CLAUDE.md # Contributor notes
|
||||||
│
|
│
|
||||||
└── src/
|
└── src/
|
||||||
├── trx-core/ # Core types, traits, state machine
|
├── trx-core/ # Core types, traits, state machine
|
||||||
@@ -135,7 +144,7 @@ trx-rs/ # Workspace root
|
|||||||
└── decoders/
|
└── decoders/
|
||||||
├── trx-aprs/ # APRS packet decoder
|
├── trx-aprs/ # APRS packet decoder
|
||||||
├── trx-cw/ # CW / Morse decoder
|
├── trx-cw/ # CW / Morse decoder
|
||||||
├── trx-ftx/ # Pure Rust FTx decoder (FT8/FT4/FT2)
|
├── trx-ft8/ # FT8 decoder (wraps ft8_lib C library)
|
||||||
├── trx-wspr/ # WSPR beacon decoder
|
├── trx-wspr/ # WSPR beacon decoder
|
||||||
├── trx-rds/ # FM RDS decoder
|
├── trx-rds/ # FM RDS decoder
|
||||||
└── trx-decode-log/ # JSON Lines log rotation for decoded frames
|
└── trx-decode-log/ # JSON Lines log rotation for decoded frames
|
||||||
@@ -292,10 +301,10 @@ pub trait RetryPolicy: Send {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct ExponentialBackoff {
|
pub struct ExponentialBackoff {
|
||||||
max_attempts: u32,
|
initial_delay: Duration,
|
||||||
base_delay: Duration,
|
|
||||||
max_delay: Duration,
|
max_delay: Duration,
|
||||||
// Delays include ±25% randomized jitter to prevent thundering herd
|
multiplier: f64,
|
||||||
|
current_delay: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PollingPolicy: Send {
|
pub trait PollingPolicy: Send {
|
||||||
@@ -508,7 +517,7 @@ impl RegistrationContext {
|
|||||||
Built-in registrations (via `register_builtin_backends_on`):
|
Built-in registrations (via `register_builtin_backends_on`):
|
||||||
- `"ft817"` → `Ft817::new`
|
- `"ft817"` → `Ft817::new`
|
||||||
- `"ft450d"` → `Ft450d::new`
|
- `"ft450d"` → `Ft450d::new`
|
||||||
- `"soapysdr"` → `SoapySdrRig::new_from_config(SoapySdrConfig { ... })` (if `soapysdr` feature enabled)
|
- `"soapysdr"` → `SoapySdrRig::new_with_config` (if `soapysdr` feature enabled)
|
||||||
|
|
||||||
### RigCat Trait (from trx-core)
|
### RigCat Trait (from trx-core)
|
||||||
|
|
||||||
@@ -560,6 +569,8 @@ pub struct SoapySdrRig {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Known limitation:** IQ sample streaming (`real_iq_source.rs:149–157`) is not yet implemented — the IQ source currently returns zero buffers. The soapysdr 0.3 crate lacks streaming APIs; direct `soapysdr-sys` FFI or a crate upgrade would be required.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Client (trx-client)
|
## Client (trx-client)
|
||||||
@@ -647,8 +658,32 @@ Built on **Actix-web**, serves a browser-based control panel.
|
|||||||
| GET | `/audio` | WebSocket audio stream |
|
| GET | `/audio` | WebSocket audio stream |
|
||||||
| GET | `/favicon.png` | Static asset |
|
| GET | `/favicon.png` | Static asset |
|
||||||
|
|
||||||
|
`/status` response includes a `FrontendMeta` block:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct FrontendMeta {
|
||||||
|
http_clients: usize,
|
||||||
|
rigctl_clients: usize,
|
||||||
|
rigctl_addr: Option<String>,
|
||||||
|
active_rig_id: Option<String>,
|
||||||
|
rig_ids: Vec<String>,
|
||||||
|
owner_callsign: Option<String>,
|
||||||
|
show_sdr_gain_control: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**Web UI features:** frequency display/entry, mode selector, PTT indicator, S-meter/TX-power/SWR meters, decoder toggles, decode history, spectrum waterfall (SDR), rig picker (multi-rig).
|
**Web UI features:** frequency display/entry, mode selector, PTT indicator, S-meter/TX-power/SWR meters, decoder toggles, decode history, spectrum waterfall (SDR), rig picker (multi-rig).
|
||||||
|
|
||||||
|
**Modules:**
|
||||||
|
|
||||||
|
| File | Responsibility |
|
||||||
|
|------|---------------|
|
||||||
|
| `server.rs` | Actix app builder, middleware, CORS |
|
||||||
|
| `api.rs` | REST handler functions |
|
||||||
|
| `audio.rs` | WebSocket ↔ PCM audio bridge |
|
||||||
|
| `auth.rs` | Token or basic-auth middleware |
|
||||||
|
| `status.rs` | State formatting for JSON responses |
|
||||||
|
|
||||||
### Rigctl Frontend (`trx-frontend-rigctl/`)
|
### Rigctl Frontend (`trx-frontend-rigctl/`)
|
||||||
|
|
||||||
Hamlib-compatible plaintext TCP interface on port 4532. Allows WSJT-X, JS8Call, and other Hamlib-aware applications to control the rig without modification.
|
Hamlib-compatible plaintext TCP interface on port 4532. Allows WSJT-X, JS8Call, and other Hamlib-aware applications to control the rig without modification.
|
||||||
@@ -669,14 +704,14 @@ All decoders run as background Tokio tasks inside `trx-server`. They subscribe t
|
|||||||
|-------|---------|-------|
|
|-------|---------|-------|
|
||||||
| `trx-aprs` | APRS (AX.25) | Forwards to APRS-IS if enabled |
|
| `trx-aprs` | APRS (AX.25) | Forwards to APRS-IS if enabled |
|
||||||
| `trx-cw` | CW / Morse | Auto WPM detection |
|
| `trx-cw` | CW / Morse | Auto WPM detection |
|
||||||
| `trx-ftx` | FTx | Pure Rust FT8/FT4/FT2 decoder; posts to PSKReporter |
|
| `trx-ft8` | FT8 | Wraps `external/ft8_lib` C library via FFI; posts to PSKReporter |
|
||||||
| `trx-wspr` | WSPR beacons | Posts to PSKReporter |
|
| `trx-wspr` | WSPR beacons | Posts to PSKReporter |
|
||||||
| `trx-rds` | FM RDS | Station name, radiotext, time |
|
| `trx-rds` | FM RDS | Station name, radiotext, time |
|
||||||
| `trx-decode-log` | Logging infrastructure | JSON Lines, date-rotated files |
|
| `trx-decode-log` | Logging infrastructure | JSON Lines, date-rotated files |
|
||||||
|
|
||||||
Control commands (e.g., `SetAprsDecodeEnabled(bool)`, `ResetCwDecoder`) are routed through `rig_task.rs` to the active decoder tasks.
|
Control commands (e.g., `SetAprsDecodeEnabled(bool)`, `ResetCwDecoder`) are routed through `rig_task.rs` to the active decoder tasks.
|
||||||
|
|
||||||
Decoded events are multiplexed onto the audio stream wire protocol (`0x03`–`0x06` frame types) and also buffered in `DecoderHistories` for replay to newly connected clients.
|
Decoded events are multiplexed onto the audio stream wire protocol (`0x03–0x06` frame types) and also buffered in `DecoderHistories` for replay to newly connected clients.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -711,7 +746,7 @@ Decoders / Audio server
|
|||||||
|
|
||||||
WFM demodulator supports:
|
WFM demodulator supports:
|
||||||
- Stereo pilot detection and L+R/L−R matrix decoding
|
- Stereo pilot detection and L+R/L−R matrix decoding
|
||||||
- Configurable de-emphasis time constant (50 us EU / 75 us US)
|
- Configurable de-emphasis time constant (50 µs EU / 75 µs US)
|
||||||
- Optional noise reduction
|
- Optional noise reduction
|
||||||
|
|
||||||
### Spectrum (`spectrum.rs`)
|
### Spectrum (`spectrum.rs`)
|
||||||
@@ -998,90 +1033,4 @@ stream decoder messages HTTP WebSocket / local speakers
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Detailed Component Notes
|
*Generated from source as of commit `56d6d12` (March 2026).*
|
||||||
|
|
||||||
### Rig Task Internals (`rig_task.rs` — 1,315 lines)
|
|
||||||
|
|
||||||
The rig task is the heart of the server. Key implementation details:
|
|
||||||
|
|
||||||
- **Command batching**: Accumulates pending requests before processing sequentially in FIFO order.
|
|
||||||
- **Spectrum deduplication**: Concurrent `GetSpectrum` requests are collapsed — one DSP computation broadcasts to all waiting responders.
|
|
||||||
- **Adaptive polling**: Poll interval adjusts based on TX state (100ms during TX, 500ms idle).
|
|
||||||
- **Grace period**: 800ms pause on polling after power-on/off operations to let hardware settle.
|
|
||||||
- **VFO priming**: Optional initialization sequence that toggles VFO A/B to populate the state cache.
|
|
||||||
- **Per-rig decoder histories**: Each rig maintains independent `Arc<DecoderHistories>` for all 11 decoder types.
|
|
||||||
- **Configurable timeouts**: `command_exec_timeout` (default 10s) and `poll_refresh_timeout` (default 8s) are configurable via `RigTaskConfig` and the TOML `[timeouts]` section.
|
|
||||||
- **Crash recovery**: Rig tasks are monitored; on crash, an `Error` state is broadcast to clients via the watch channel so they see the failure instead of silent timeout.
|
|
||||||
|
|
||||||
### Audio Pipeline (`audio.rs` — 3,977 lines)
|
|
||||||
|
|
||||||
The audio module handles decoder history storage and stream management:
|
|
||||||
|
|
||||||
- **`DecoderHistories`**: Per-rig mutable store for 11 decoder history queues (AIS, VDES, APRS, HF_APRS, CW, FT8, FT4, FT2, WSPR, WXSAT, LRPT).
|
|
||||||
- **Time-based retention**: 24h TTL on all history with periodic pruning.
|
|
||||||
- **Capacity bounds**: Per-decoder max of 10,000 entries (`MAX_HISTORY_ENTRIES`) prevents unbounded memory growth on busy channels.
|
|
||||||
- **Atomic total count**: `AtomicUsize` with CAS loop avoids acquiring 11 mutex locks in `snapshot_all()`.
|
|
||||||
- **Lock poisoning recovery with logging**: Uses `lock_or_recover()` helper that logs a warning when recovering from a poisoned mutex.
|
|
||||||
- **`StreamErrorLogger`**: Suppresses duplicate stream errors with 60s periodic summaries and error classification (alsa_poll_failure, input/output_stream_error).
|
|
||||||
- **Device enumeration helpers**: `find_input_device()` and `find_output_device()` extract the repeated device lookup logic from `run_capture()`/`run_playback()`.
|
|
||||||
- **CRC filtering**: APRS records filtered by `crc_ok` before storage.
|
|
||||||
|
|
||||||
### Remote Client Dual-Connection Model
|
|
||||||
|
|
||||||
`remote_client.rs` maintains two independent TCP connections to the server:
|
|
||||||
|
|
||||||
1. **Main connection** (port 4530): State polling, command forwarding, rig discovery.
|
|
||||||
2. **Spectrum connection** (dedicated): Polls `GetSpectrum` at 50ms intervals (20 fps) independently to avoid blocking the main connection during command processing.
|
|
||||||
|
|
||||||
Constants: `CONNECT_TIMEOUT: 5s`, `IO_TIMEOUT: 15s`, `SPECTRUM_IO_TIMEOUT: 3s`. Exponential backoff with jitter on reconnect.
|
|
||||||
|
|
||||||
### FrontendRuntimeContext Sub-Structs
|
|
||||||
|
|
||||||
The `FrontendRuntimeContext` struct in `trx-frontend/src/lib.rs` is decomposed into coherent sub-structs:
|
|
||||||
|
|
||||||
| Sub-struct | Purpose | Key fields |
|
|
||||||
|-----------|---------|------------|
|
|
||||||
| `AudioContext` | Audio streaming channels | `rx`, `tx`, `info`, `decode_rx`, `clients` |
|
|
||||||
| `DecodeHistoryContext` | Decode history for all types | `ais`, `vdes`, `aprs`, `hf_aprs`, `cw`, `ft8`, `ft4`, `ft2`, `wspr` |
|
|
||||||
| `HttpAuthConfig` | HTTP auth settings | `enabled`, `rx_passphrase`, `session_ttl_secs`, `tokens` |
|
|
||||||
| `HttpUiConfig` | HTTP UI display config | `show_sdr_gain_control`, `initial_map_zoom`, `spectrum_*` |
|
|
||||||
| `RigRoutingContext` | Remote rig state & routing | `active_rig_id`, `remote_rigs`, `rig_states`, `server_connected` |
|
|
||||||
| `OwnerInfo` | Station metadata | `callsign`, `website_url`, `ais_vessel_url_base` |
|
|
||||||
| `VChanContext` | Virtual channel audio | `audio`, `audio_cmd`, `destroyed`, `rig_audio_cmd` |
|
|
||||||
| `SpectrumContext` | Spectrum data | `sender`, `per_rig` |
|
|
||||||
| `PerRigAudioContext` | Per-rig audio channels | `rx`, `info` |
|
|
||||||
|
|
||||||
### Decoder Implementation Patterns
|
|
||||||
|
|
||||||
All real-time decoders follow a consistent pattern:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// 1. Stateful decoder struct with sample buffer
|
|
||||||
pub struct XxxDecoder { sample_buf: Vec<f32>, ... }
|
|
||||||
|
|
||||||
// 2. Block/sample processing
|
|
||||||
pub fn process_block(&mut self, samples: &[f32]) { ... }
|
|
||||||
|
|
||||||
// 3. Result extraction
|
|
||||||
pub fn decode_if_ready(&mut self) -> Vec<XxxResult> { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
| Decoder | Algorithm | Sample Rate | Key Constants |
|
|
||||||
|---------|-----------|-------------|---------------|
|
|
||||||
| FT8/FT4/FT2 | Waterfall + LDPC/OSD | Varies | MAX_LDPC_ITERATIONS=20, MAX_CANDIDATES=120 |
|
|
||||||
| CW | Goertzel tone detection | Varies | 10ms windows, tone range 300–1200 Hz |
|
|
||||||
| APRS | Bell 202 AFSK (1200/2200 Hz) | 9600 | HDLC framing, NRZI, CRC-16-CCITT |
|
|
||||||
| AIS | GMSK 9600 baud | 9600 | Narrowband FM input |
|
|
||||||
| WSPR | Fano decoder | 12000 | 162 symbols, 120s slot, 1.46 Hz spacing |
|
|
||||||
| RDS | RRC matched filter + Costas PLL | Native | 57 kHz subcarrier, 1187.5 bps, OSD FEC |
|
|
||||||
| VDES | pi/4-QPSK 76.8 ksps | 100k | Burst detection, partial Turbo FEC |
|
|
||||||
|
|
||||||
### Backend Reliability Workarounds (FT-817)
|
|
||||||
|
|
||||||
The FT-817 CAT backend (`trx-backend-ft817/`) includes empirical workarounds for hardware quirks:
|
|
||||||
|
|
||||||
- **Duplicate frame sends**: `set_mode()` and `set_ptt()` send CAT frames twice with 80ms delay (radio sometimes drops first frame).
|
|
||||||
- **Panel unlock before commands**: Clears stale bytes from the serial buffer.
|
|
||||||
- **Power-on dummy frame**: CPU wakes before CAT framing locks; dummy frame ensures readiness.
|
|
||||||
- **VFO state inference**: Infers VFO A/B by matching frequencies against cached values (fragile when frequencies collide).
|
|
||||||
- **Read timeout**: 800ms per CAT read operation (not configurable).
|
|
||||||
@@ -1,147 +1,188 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="assets/trx-logo.png" alt="trx-rs logo" width="25%" />
|
<img src="assets/trx-logo.png" alt="trx-rs logo" width="25%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
# trx-rs
|
# trx-rs
|
||||||
|
|
||||||
A modular amateur radio control stack written in Rust.
|
`trx-rs` is a modular amateur radio control stack written in Rust.
|
||||||
|
It splits radio hardware access from user-facing interfaces so you can run
|
||||||
[](LICENSES)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
`trx-rs` splits radio hardware access from user-facing interfaces so you can run
|
|
||||||
rig control, SDR DSP, decoding, audio streaming, and web access as separate,
|
rig control, SDR DSP, decoding, audio streaming, and web access as separate,
|
||||||
composable pieces.
|
composable pieces.
|
||||||
|
|
||||||
| | |
|
The project is built around two primary binaries:
|
||||||
|---|---|
|
|
||||||
| **Backends** | Yaesu FT-817, Yaesu FT-450D, SoapySDR |
|
- `trx-server`: talks to radios and SDR backends
|
||||||
| **Frontends** | Web UI, rigctl-compatible TCP, JSON-over-TCP |
|
- `trx-client`: connects to the server and exposes frontends such as the web UI
|
||||||
| **Decoders** | AIS, APRS, CW, FT8, RDS, VDES, WSPR |
|
|
||||||
| **Audio** | Opus streaming between server, client, and browser |
|
## Web UI Demo
|
||||||
|
|
||||||
|
> GIF placeholder: add an animated walkthrough of the website here.
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
|
||||||
|
- Controls supported radios over networked client/server boundaries
|
||||||
|
- Exposes a browser UI, a rigctl-compatible frontend, and JSON-based control
|
||||||
|
- Supports SDR workflows with live spectrum, waterfall, demodulation, and decode
|
||||||
|
- Streams Opus audio between server, client, and browser
|
||||||
|
- Runs multiple decoders including AIS, APRS, CW, FT8, RDS, VDES, and WSPR
|
||||||
|
- Supports multi-rig deployments and SDR virtual channels
|
||||||
|
- Loads backends and frontends via plugins
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
At a high level:
|
||||||
|
|
||||||
|
1. `trx-server` owns the radio hardware and DSP pipeline.
|
||||||
|
2. `trx-client` connects to the server over TCP for control and audio.
|
||||||
|
3. Frontends hang off `trx-client`, including the HTTP web UI.
|
||||||
|
|
||||||
|
This separation is intentional: it keeps hardware access local to one host while
|
||||||
|
making control and monitoring available elsewhere on the network.
|
||||||
|
|
||||||
|
## Workspace Layout
|
||||||
|
|
||||||
|
- `src/trx-core`: shared types, rig state, controller logic
|
||||||
|
- `src/trx-protocol`: client/server protocol types and codecs
|
||||||
|
- `src/trx-app`: shared app bootstrapping, config, logging, plugins
|
||||||
|
- `src/trx-server`: server binary and backend integration
|
||||||
|
- `src/trx-client`: client binary and remote connection handling
|
||||||
|
- `src/trx-client/trx-frontend`: frontend abstraction
|
||||||
|
- `src/decoders`: protocol-specific decoder crates
|
||||||
|
- `examples/trx-plugin-example`: minimal plugin example
|
||||||
|
|
||||||
|
## Supported Pieces
|
||||||
|
|
||||||
|
### Backends
|
||||||
|
|
||||||
|
- Yaesu FT-817
|
||||||
|
- Yaesu FT-450D
|
||||||
|
- SoapySDR-based SDR backend
|
||||||
|
|
||||||
|
### Frontends
|
||||||
|
|
||||||
|
- HTTP web frontend
|
||||||
|
- rigctl-compatible TCP frontend
|
||||||
|
- JSON-over-TCP frontend
|
||||||
|
|
||||||
|
### Decoders
|
||||||
|
|
||||||
|
- AIS
|
||||||
|
- APRS
|
||||||
|
- CW
|
||||||
|
- FT8
|
||||||
|
- RDS
|
||||||
|
- VDES
|
||||||
|
- WSPR
|
||||||
|
|
||||||
|
## Build Requirements
|
||||||
|
|
||||||
|
You will need Rust plus a few system libraries.
|
||||||
|
|
||||||
|
### Common dependencies
|
||||||
|
|
||||||
|
- `libopus`
|
||||||
|
- `pkg-config` or `pkgconf`
|
||||||
|
- `cmake`
|
||||||
|
|
||||||
|
### SDR builds
|
||||||
|
|
||||||
|
- `libsoapysdr`
|
||||||
|
|
||||||
|
### Audio builds
|
||||||
|
|
||||||
|
- Core Audio on macOS, or ALSA development packages on Linux
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Both `trx-server` and `trx-client` read from a shared `trx-rs.toml`.
|
||||||
|
|
||||||
|
- Default lookup order: current directory, `~/.config/trx-rs`, `/etc/trx-rs`
|
||||||
|
- Use `--config <FILE>` to point at an explicit config file
|
||||||
|
- Use `--print-config` to print an example combined config
|
||||||
|
|
||||||
|
Start from [`trx-rs.toml.example`](trx-rs.toml.example).
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### 1. Install dependencies
|
### 1. Build
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><b>Debian / Ubuntu</b></summary>
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install build-essential pkg-config cmake libopus-dev libasound2-dev
|
cargo build
|
||||||
# Optional — SDR support
|
|
||||||
sudo apt install libsoapysdr-dev
|
|
||||||
```
|
```
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
### 2. Create a config file
|
||||||
<summary><b>Fedora</b></summary>
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dnf install gcc pkg-config cmake opus-devel alsa-lib-devel
|
cp trx-rs.toml.example trx-rs.toml
|
||||||
# Optional — SDR support
|
|
||||||
sudo dnf install SoapySDR-devel
|
|
||||||
```
|
```
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
Adjust backend, frontend, audio, and auth settings for your environment.
|
||||||
<summary><b>Arch Linux</b></summary>
|
|
||||||
|
### 3. Run the server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pacman -S base-devel pkgconf cmake opus alsa-lib
|
cargo run -p trx-server
|
||||||
# Optional — SDR support
|
|
||||||
sudo pacman -S soapysdr
|
|
||||||
```
|
```
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
### 4. Run the client
|
||||||
<summary><b>macOS (Homebrew)</b></summary>
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install cmake opus
|
cargo run -p trx-client
|
||||||
# Optional — SDR support
|
|
||||||
brew install soapysdr
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
See [Build Requirements](https://git.haxx.space/sjg/trx-rs/wiki/User-Manual#build-requirements)
|
|
||||||
in the wiki for details on each library.
|
|
||||||
|
|
||||||
> **Note:** `cmake` is required even when a system Opus library is installed.
|
|
||||||
> The `audiopus_sys` crate probes for Opus via `pkg-config`; if it is not found
|
|
||||||
> (or `pkg-config` is unavailable), it falls back to compiling a vendored copy
|
|
||||||
> of Opus with CMake. A missing `cmake` therefore fails the build with
|
|
||||||
> `is cmake not installed?` rather than a missing-Opus error.
|
|
||||||
|
|
||||||
### 2. Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --release
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Build without SDR support: `cargo build --release --no-default-features`
|
### 5. Open the web UI
|
||||||
|
|
||||||
### 3. Configure
|
Open the configured HTTP frontend address in a browser.
|
||||||
|
|
||||||
Run the interactive setup wizard to generate config files for your station:
|
## Web Frontend Highlights
|
||||||
|
|
||||||
```bash
|
- Real-time spectrum and waterfall
|
||||||
./target/release/trx-configurator
|
- Frequency, mode, and bandwidth control
|
||||||
```
|
- Decoder dashboards and history
|
||||||
|
- SDR virtual channels
|
||||||
|
- Browser RX/TX audio
|
||||||
|
- Optional authentication with read-only and control roles
|
||||||
|
|
||||||
The wizard walks you through rig selection, serial port detection, audio
|
## Authentication
|
||||||
settings, and frontend options, then writes `trx-server.toml` and
|
|
||||||
`trx-client.toml`.
|
|
||||||
|
|
||||||
Alternatively, generate example configs and edit them by hand:
|
The HTTP frontend supports optional passphrase-based authentication.
|
||||||
|
|
||||||
```bash
|
- `rx`: read-only access
|
||||||
./target/release/trx-server --print-config > trx-server.toml
|
- `control`: full control access
|
||||||
./target/release/trx-client --print-config > trx-client.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Run
|
When exposing the web UI beyond a trusted LAN, run it behind HTTPS and enable
|
||||||
|
secure cookie settings in the config.
|
||||||
|
|
||||||
```bash
|
## Audio
|
||||||
./target/release/trx-server --config trx-server.toml
|
|
||||||
./target/release/trx-client --config trx-client.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
Open the configured HTTP frontend address in a browser (default `http://localhost:8080`).
|
Audio is transported as Opus between server, client, and browser.
|
||||||
|
|
||||||
## How It Works
|
- `trx-server` captures and encodes audio
|
||||||
|
- `trx-client` relays audio to the HTTP frontend
|
||||||
|
- Browsers connect over `/audio`
|
||||||
|
|
||||||
```mermaid
|
## Plugins
|
||||||
graph TD
|
|
||||||
SDR1["SDR #1"] & SDR2["SDR #2"] <-->|USB| S1["trx-server A"]
|
|
||||||
SDR3["SDR #3"] & FT817["FT-817"] <-->|USB / serial| S2["trx-server B"]
|
|
||||||
|
|
||||||
S1 <-->|"JSON-TCP :4530"| C1["trx-client"]
|
Both binaries can discover shared-library plugins through:
|
||||||
S1 -->|"Opus-TCP per rig"| C1
|
|
||||||
S2 <-->|"JSON-TCP :4530"| C1
|
|
||||||
S2 -->|"Opus-TCP per rig"| C1
|
|
||||||
|
|
||||||
C1 <-->|internal channels| F1["Web UI :8080"]
|
- `./plugins`
|
||||||
C1 <-->|internal channels| F2["rigctl :4532"]
|
- `~/.config/trx-rs/plugins`
|
||||||
```
|
- `TRX_PLUGIN_DIRS`
|
||||||
|
|
||||||
Each `trx-server` owns one or more rigs and runs DSP, decoding, and audio capture locally.
|
See [`examples/trx-plugin-example/README.md`](examples/trx-plugin-example/README.md).
|
||||||
A `trx-client` connects to any number of servers over TCP and exposes them through
|
|
||||||
a unified set of frontends.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
| Resource | Description |
|
- [`OVERVIEW.md`](OVERVIEW.md): architecture and design overview
|
||||||
|----------|-------------|
|
- [`CONTRIBUTING.md`](CONTRIBUTING.md): contribution and commit rules
|
||||||
| [User Manual](https://git.haxx.space/sjg/trx-rs/wiki/User-Manual) | Configuration, features, and usage |
|
|
||||||
| [Architecture](https://git.haxx.space/sjg/trx-rs/wiki/Architecture) | System design, crate layout, data flow, and internals |
|
## Project Status
|
||||||
| [Optimization Guidelines](https://git.haxx.space/sjg/trx-rs/wiki/Optimization-Guidelines) | Performance guidelines for the real-time DSP pipeline |
|
|
||||||
| [Planned Features](https://git.haxx.space/sjg/trx-rs/wiki/Planned-Features) | Roadmap and design notes |
|
This is an active project with evolving APIs and frontend behavior. Expect some
|
||||||
| [Contributing](CONTRIBUTING.md) | Commit conventions, workflow, and code style |
|
rough edges and ongoing refactors.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL-2.0-or-later. See [`LICENSES`](LICENSES) for the full license text and
|
Licensed under BSD-2-Clause.
|
||||||
bundled third-party license files. Bundled third-party components (Leaflet and
|
|
||||||
the Leaflet AIS tracksymbol plugin under `assets/web/vendor/`) retain their
|
See [`LICENSES`](LICENSES) for bundled third-party license files.
|
||||||
original BSD-2-Clause license.
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Planned Features
|
# Recorder Feature Plan
|
||||||
|
|
||||||
## Recorder
|
## Overview
|
||||||
|
|
||||||
The recorder captures the demodulated audio stream alongside associated metadata (FFT data, decoded signals, rig state) into a structured session on disk, with full playback and seeking support from within the application.
|
This document describes the design and implementation plan for the recorder feature in trx-rs. The recorder captures the demodulated audio stream alongside associated metadata (FFT data, decoded signals, rig state) into a structured session on disk, with full playback and seeking support from within the application.
|
||||||
|
|
||||||
### Requirements
|
## Requirements
|
||||||
|
|
||||||
| ID | Description |
|
| ID | Description |
|
||||||
|----|-------------|
|
|----|-------------|
|
||||||
@@ -20,9 +20,9 @@ The recorder captures the demodulated audio stream alongside associated metadata
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Architecture
|
## Architecture
|
||||||
|
|
||||||
#### New Crate: `trx-recorder`
|
### New Crate: `trx-recorder`
|
||||||
|
|
||||||
A new crate `src/trx-server/trx-recorder/` handles all record and playback logic. It is a library crate consumed by `trx-server`.
|
A new crate `src/trx-server/trx-recorder/` handles all record and playback logic. It is a library crate consumed by `trx-server`.
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ src/trx-server/
|
|||||||
config.rs # RecorderConfig (serde, derives Default)
|
config.rs # RecorderConfig (serde, derives Default)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Integration Points in `trx-server`
|
### Integration Points in `trx-server`
|
||||||
|
|
||||||
| Source | What is tapped | How |
|
| Source | What is tapped | How |
|
||||||
|--------|---------------|-----|
|
|--------|---------------|-----|
|
||||||
@@ -56,7 +56,7 @@ No existing code paths are modified beyond:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Session Layout on Disk
|
## Session Layout on Disk
|
||||||
|
|
||||||
Each recording is a **session directory** named by UTC start time and opening rig state:
|
Each recording is a **session directory** named by UTC start time and opening rig state:
|
||||||
|
|
||||||
@@ -70,13 +70,13 @@ Each recording is a **session directory** named by UTC start time and opening ri
|
|||||||
|
|
||||||
`output_dir` defaults to `~/.local/share/trx-rs/recordings`.
|
`output_dir` defaults to `~/.local/share/trx-rs/recordings`.
|
||||||
|
|
||||||
#### Audio File (REQ-REC-001, REQ-REC-002, REQ-REC-003)
|
### Audio File (REQ-REC-001, REQ-REC-002, REQ-REC-003)
|
||||||
|
|
||||||
- **Format**: Opus, using the `opus` crate (already a workspace dependency via `trx-backend-soapysdr`). Seek index (`index.bin`) provides byte → time mapping.
|
- **Format**: Opus, using the `opus` crate (already a workspace dependency via `trx-backend-soapysdr`). Seek index (`index.bin`) provides byte → time mapping.
|
||||||
- **Channel count**: determined at session open from `AudioConfig.channels`. If `channels == 1` → mono; if `channels == 2` → stereo. Written into the file header and recorded in the session's first data event.
|
- **Channel count**: determined at session open from `AudioConfig.channels`. If `channels == 1` → mono; if `channels == 2` → stereo. Written into the file header and recorded in the session's first data event.
|
||||||
- **Sample rate**: preserved from `AudioConfig.sample_rate` (default 48 000 Hz).
|
- **Sample rate**: preserved from `AudioConfig.sample_rate` (default 48 000 Hz).
|
||||||
|
|
||||||
#### Data File (REQ-REC-004, REQ-REC-005)
|
### Data File (REQ-REC-004, REQ-REC-005)
|
||||||
|
|
||||||
`data.jsonl` — one JSON object per line, each with a required `offset_ms` field giving the millisecond offset from session start (satisfies REQ-SYNC-001 at ≥1 s resolution):
|
`data.jsonl` — one JSON object per line, each with a required `offset_ms` field giving the millisecond offset from session start (satisfies REQ-SYNC-001 at ≥1 s resolution):
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ Supported `type` values:
|
|||||||
| `cw` | `DecodedMessage` broadcast | on decode event |
|
| `cw` | `DecodedMessage` broadcast | on decode event |
|
||||||
| `cursor` | `RecorderCommand::MarkCursor { label }` | on user request |
|
| `cursor` | `RecorderCommand::MarkCursor { label }` | on user request |
|
||||||
|
|
||||||
#### Seek Index (REQ-PLAY-002)
|
### Seek Index (REQ-PLAY-002)
|
||||||
|
|
||||||
`index.bin` is a flat binary table of 16-byte records written every `index_interval_ms` (default 1 000 ms):
|
`index.bin` is a flat binary table of 16-byte records written every `index_interval_ms` (default 1 000 ms):
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ At playback seek time, binary search on `offset_ms` locates the nearest audio fr
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### RecorderConfig
|
## RecorderConfig
|
||||||
|
|
||||||
Added to `ServerConfig` under `[recorder]`:
|
Added to `ServerConfig` under `[recorder]`:
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ max_session_duration_s = 3600 # auto-split at 1 h; 0 = unlimited
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Command API
|
## Command API
|
||||||
|
|
||||||
New variants added to the existing command enum (handled in `rig_task.rs`):
|
New variants added to the existing command enum (handled in `rig_task.rs`):
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ These are exposed via:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Playback Engine (REQ-PLAY-001, REQ-PLAY-002)
|
## Playback Engine (REQ-PLAY-001, REQ-PLAY-002)
|
||||||
|
|
||||||
`PlaybackEngine` opens a session directory and:
|
`PlaybackEngine` opens a session directory and:
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ While `PlaybackState` is not `Live`, the server suppresses live hardware polling
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Time Synchronisation (REQ-SYNC-001)
|
## Time Synchronisation (REQ-SYNC-001)
|
||||||
|
|
||||||
All timestamps use a single `session_epoch: std::time::Instant` captured at `StartRecording`. Every PCM frame, every data event, and every seek-index entry is stamped as `(Instant::now() - session_epoch).as_millis() as u64`. This gives sub-millisecond internal precision; the requirement of ≥1 s resolution is met by orders of magnitude.
|
All timestamps use a single `session_epoch: std::time::Instant` captured at `StartRecording`. Every PCM frame, every data event, and every seek-index entry is stamped as `(Instant::now() - session_epoch).as_millis() as u64`. This gives sub-millisecond internal precision; the requirement of ≥1 s resolution is met by orders of magnitude.
|
||||||
|
|
||||||
@@ -178,16 +178,16 @@ Wall-clock UTC is embedded only in `session_start` (`wall_clock_utc`) and in the
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Implementation Phases
|
## Implementation Phases
|
||||||
|
|
||||||
#### Phase 1 — Audio recording (REQ-REC-001, REQ-REC-002, REQ-REC-003)
|
### Phase 1 — Audio recording (REQ-REC-001, REQ-REC-002, REQ-REC-003)
|
||||||
|
|
||||||
1. Add `trx-recorder` crate skeleton; `RecorderConfig`; `RecorderHandle`.
|
1. Add `trx-recorder` crate skeleton; `RecorderConfig`; `RecorderHandle`.
|
||||||
2. Implement `AudioWriter` with Opus output.
|
2. Implement `AudioWriter` with Opus output.
|
||||||
3. Subscribe `AudioWriter` to `pcm_tx` in `audio.rs`; open session on `StartRecording` command.
|
3. Subscribe `AudioWriter` to `pcm_tx` in `audio.rs`; open session on `StartRecording` command.
|
||||||
4. Auto-detect channel count from `AudioConfig.channels`.
|
4. Auto-detect channel count from `AudioConfig.channels`.
|
||||||
|
|
||||||
#### Phase 2 — Metadata recording (REQ-REC-004, REQ-REC-005, REQ-SYNC-001)
|
### Phase 2 — Metadata recording (REQ-REC-004, REQ-REC-005, REQ-SYNC-001)
|
||||||
|
|
||||||
1. Implement `DataFileWriter`; define full event schema.
|
1. Implement `DataFileWriter`; define full event schema.
|
||||||
2. Subscribe to `DecodedMessage` broadcast; fan-in all decoder types.
|
2. Subscribe to `DecodedMessage` broadcast; fan-in all decoder types.
|
||||||
@@ -195,12 +195,12 @@ Wall-clock UTC is embedded only in `session_start` (`wall_clock_utc`) and in the
|
|||||||
4. Emit `fft` events at configured interval from spectrum data.
|
4. Emit `fft` events at configured interval from spectrum data.
|
||||||
5. Write `SeekIndex` in parallel with audio.
|
5. Write `SeekIndex` in parallel with audio.
|
||||||
|
|
||||||
#### Phase 3 — Cursor (REQ-REC-006)
|
### Phase 3 — Cursor (REQ-REC-006)
|
||||||
|
|
||||||
1. Add `MarkCursor` command + HTTP endpoint.
|
1. Add `MarkCursor` command + HTTP endpoint.
|
||||||
2. Write `cursor` event to `data.jsonl` with current `offset_ms`.
|
2. Write `cursor` event to `data.jsonl` with current `offset_ms`.
|
||||||
|
|
||||||
#### Phase 4 — Playback (REQ-PLAY-001, REQ-PLAY-002)
|
### Phase 4 — Playback (REQ-PLAY-001, REQ-PLAY-002)
|
||||||
|
|
||||||
1. Implement `PlaybackEngine`; Opus decode + PCM broadcast.
|
1. Implement `PlaybackEngine`; Opus decode + PCM broadcast.
|
||||||
2. Add `PlaybackState` to `RigState`; suppress live capture during playback.
|
2. Add `PlaybackState` to `RigState`; suppress live capture during playback.
|
||||||
@@ -210,7 +210,7 @@ Wall-clock UTC is embedded only in `session_start` (`wall_clock_utc`) and in the
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Dependencies to Add
|
## Dependencies to Add
|
||||||
|
|
||||||
| Crate | Use | Already present? |
|
| Crate | Use | Already present? |
|
||||||
|-------|-----|-----------------|
|
|-------|-----|-----------------|
|
||||||
@@ -220,105 +220,8 @@ Wall-clock UTC is embedded only in `session_start` (`wall_clock_utc`) and in the
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Open Questions
|
## Open Questions
|
||||||
|
|
||||||
1. **Playback isolation**: Should playback be exclusive (block all CAT commands) or concurrent? Initial design blocks CAT polling; revisit if users need to change frequency during playback.
|
1. **Playback isolation**: Should playback be exclusive (block all CAT commands) or concurrent? Initial design blocks CAT polling; revisit if users need to change frequency during playback.
|
||||||
2. **Session listing API**: The HTTP frontend needs an endpoint to enumerate sessions (`GET /api/recorder/sessions`). Schema TBD in Phase 4.
|
2. **Session listing API**: The HTTP frontend needs an endpoint to enumerate sessions (`GET /api/recorder/sessions`). Schema TBD in Phase 4.
|
||||||
3. **Storage limits**: `max_session_duration_s` auto-splits sessions; a `max_total_size_gb` housekeeping option may be needed but is out of scope for initial phases.
|
3. **Storage limits**: `max_session_duration_s` auto-splits sessions; a `max_total_size_gb` housekeeping option may be needed but is out of scope for initial phases.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configurator Helper
|
|
||||||
|
|
||||||
An interactive CLI tool that guides users through creating configuration files
|
|
||||||
for trx-rs. Instead of editing TOML by hand, the user answers prompts and the
|
|
||||||
tool generates valid, commented configuration files.
|
|
||||||
|
|
||||||
### Overview
|
|
||||||
|
|
||||||
The configurator is a standalone Rust binary (`trx-configurator`) that reuses
|
|
||||||
the existing config structs from `trx-app`, `trx-server`, and `trx-client`. It
|
|
||||||
walks the user through a question-driven flow, validates inputs against the same
|
|
||||||
rules the binaries use at startup, and writes one or more of:
|
|
||||||
|
|
||||||
- `trx-server.toml` — server configuration
|
|
||||||
- `trx-client.toml` — client configuration
|
|
||||||
- `trx-rs.toml` — combined server + client configuration
|
|
||||||
|
|
||||||
The user chooses which file(s) to generate.
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
| ID | Description |
|
|
||||||
|----|-------------|
|
|
||||||
| REQ-CFG-001 | The tool shall interactively prompt the user for configuration values. |
|
|
||||||
| REQ-CFG-002 | The tool shall generate `trx-server.toml`, `trx-client.toml`, or `trx-rs.toml` per user selection. |
|
|
||||||
| REQ-CFG-003 | The tool shall validate all inputs using the same validation logic as the server and client binaries. |
|
|
||||||
| REQ-CFG-004 | The tool shall write commented TOML with descriptions of each field. |
|
|
||||||
| REQ-CFG-005 | The tool shall detect connected serial devices and offer them for rig access configuration. |
|
|
||||||
| REQ-CFG-006 | The tool shall detect available SoapySDR devices and offer them for SDR backend configuration. |
|
|
||||||
| REQ-CFG-007 | The tool shall support a non-interactive mode that generates a default config file. |
|
|
||||||
| REQ-CFG-008 | The tool shall not overwrite existing files without confirmation. |
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
#### New Crate: `trx-configurator`
|
|
||||||
|
|
||||||
A new binary crate at `src/trx-configurator/` that depends on `trx-app` for
|
|
||||||
config types and validation.
|
|
||||||
|
|
||||||
```
|
|
||||||
src/trx-configurator/
|
|
||||||
src/
|
|
||||||
main.rs # CLI entry point, mode selection
|
|
||||||
prompts.rs # Interactive prompt helpers (with defaults, validation)
|
|
||||||
detect.rs # Hardware detection (serial ports, SoapySDR devices)
|
|
||||||
writer.rs # TOML serialisation with inline comments
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
trx-configurator
|
|
||||||
├── What would you like to generate?
|
|
||||||
│ [ ] trx-server.toml
|
|
||||||
│ [ ] trx-client.toml
|
|
||||||
│ [ ] trx-rs.toml (combined)
|
|
||||||
│
|
|
||||||
├── (if server)
|
|
||||||
│ ├── General: callsign, location
|
|
||||||
│ ├── Rig: model selection, access (serial/tcp/sdr)
|
|
||||||
│ │ └── detect serial ports / SoapySDR devices
|
|
||||||
│ ├── Listen: address, port
|
|
||||||
│ ├── Audio: sample rate, channels, codec settings
|
|
||||||
│ ├── SDR: (if soapysdr selected) gain, channels, decoders
|
|
||||||
│ ├── Uplinks: PSKReporter, APRS-IS
|
|
||||||
│ └── Decode logs: enable, directory
|
|
||||||
│
|
|
||||||
├── (if client)
|
|
||||||
│ ├── Remote: server URL, auth token
|
|
||||||
│ ├── Frontends: HTTP, rigctl, http-json (enable/disable, ports)
|
|
||||||
│ └── Audio: bridge settings
|
|
||||||
│
|
|
||||||
└── Write file(s) with confirmation
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Hardware Detection
|
|
||||||
|
|
||||||
- **Serial ports**: enumerate available serial devices using `serialport` crate
|
|
||||||
(already a transitive dependency). Present as selectable list with device
|
|
||||||
path and description.
|
|
||||||
- **SoapySDR devices**: if built with `soapysdr` feature, call
|
|
||||||
`SoapySDR::enumerate("")` to list available SDR hardware. Present device
|
|
||||||
driver, label, and serial number.
|
|
||||||
|
|
||||||
#### Dependencies
|
|
||||||
|
|
||||||
| Crate | Use | Already present? |
|
|
||||||
|-------|-----|-----------------|
|
|
||||||
| `dialoguer` | Interactive prompts, selection, confirmation | No |
|
|
||||||
| `toml_edit` | TOML serialisation preserving comments | No |
|
|
||||||
| `trx-app` | Config types and validation | Yes |
|
|
||||||
| `serialport` | Serial port enumeration | Yes (transitive) |
|
|
||||||
| `soapysdr` | SDR device enumeration (optional) | Yes (feature-gated) |
|
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
version = 1
|
|
||||||
|
|
||||||
# Project-owned files without an in-file SPDX header (docs, config,
|
|
||||||
# repo metadata, logos, and bespoke web assets).
|
|
||||||
[[annotations]]
|
|
||||||
path = [
|
|
||||||
".gitattributes",
|
|
||||||
".gitignore",
|
|
||||||
"CLAUDE.md",
|
|
||||||
"CONTRIBUTING.md",
|
|
||||||
"README.md",
|
|
||||||
"trx-rs.toml.example",
|
|
||||||
"docs/**",
|
|
||||||
"src/decoders/trx-ftx/README.md",
|
|
||||||
"src/decoders/trx-wxsat/README.md",
|
|
||||||
"assets/trx-logo.png",
|
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http/assets/trx-favicon.png",
|
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http/assets/trx-logo.png",
|
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http/assets/web/bandplan.json",
|
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http/assets/web/leaflet-ais-tracksymbol.js",
|
|
||||||
]
|
|
||||||
SPDX-FileCopyrightText = "2026 Stan Grams <sjg@haxx.space>"
|
|
||||||
SPDX-License-Identifier = "GPL-2.0-or-later"
|
|
||||||
|
|
||||||
# Vendored Leaflet 1.9.4 (https://leafletjs.com), distributed under BSD-2-Clause.
|
|
||||||
[[annotations]]
|
|
||||||
path = [
|
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/leaflet.js",
|
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/leaflet.css",
|
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/layers.png",
|
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/layers-2x.png",
|
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/marker-icon.png",
|
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/marker-icon-2x.png",
|
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/marker-shadow.png",
|
|
||||||
]
|
|
||||||
SPDX-FileCopyrightText = "2010-2023 Vladimir Agafonkin, 2010-2011 CloudMade"
|
|
||||||
SPDX-License-Identifier = "BSD-2-Clause"
|
|
||||||
|
|
||||||
# Vendored DSEG14 font (https://github.com/keshikan/DSEG), SIL OFL 1.1.
|
|
||||||
[[annotations]]
|
|
||||||
path = ["src/trx-client/trx-frontend/trx-frontend-http/assets/web/vendor/dseg14-classic-latin-400-normal.woff2"]
|
|
||||||
SPDX-FileCopyrightText = "2020 The DSEG Authors (https://github.com/keshikan/DSEG)"
|
|
||||||
SPDX-License-Identifier = "OFL-1.1"
|
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
# Background Decoding Scheduler
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Background Decoding Scheduler automatically retunes the rig to pre-configured
|
||||||
|
bookmarks when no users are connected to the HTTP frontend. It runs as a background
|
||||||
|
tokio task inside `trx-frontend-http`, polling every 30 seconds.
|
||||||
|
|
||||||
|
## Modes
|
||||||
|
|
||||||
|
### Disabled (default)
|
||||||
|
Scheduler is inactive. Rig is not touched automatically.
|
||||||
|
|
||||||
|
### Grayline
|
||||||
|
Retunes around the solar terminator (day/night boundary).
|
||||||
|
|
||||||
|
The user provides:
|
||||||
|
- Station latitude and longitude (decimal degrees)
|
||||||
|
- Optional transition window width (minutes, default 20)
|
||||||
|
- Bookmark IDs for four periods:
|
||||||
|
- **Dawn** – window around sunrise (`sunrise ± window_min/2`)
|
||||||
|
- **Day** – after dawn until dusk
|
||||||
|
- **Dusk** – window around sunset (`sunset ± window_min/2`)
|
||||||
|
- **Night** – after dusk until next dawn
|
||||||
|
|
||||||
|
Period precedence (most specific wins): Dawn > Dusk > Day > Night.
|
||||||
|
|
||||||
|
If no bookmark is assigned to a period, the rig is not retuned for that period.
|
||||||
|
|
||||||
|
Sunrise/sunset is computed inline using the NOAA simplified algorithm.
|
||||||
|
Polar regions (midnight sun / polar night) fall back to Day/Night accordingly.
|
||||||
|
|
||||||
|
### TimeSpan
|
||||||
|
Retunes according to a list of user-defined time windows (UTC).
|
||||||
|
|
||||||
|
Each entry specifies:
|
||||||
|
- `start_hhmm` – start of window (e.g. 600 = 06:00 UTC)
|
||||||
|
- `end_hhmm` – end of window (e.g. 700 = 07:00 UTC)
|
||||||
|
- `bookmark_id` – bookmark to apply
|
||||||
|
- `label` – optional human-readable description
|
||||||
|
|
||||||
|
Windows that span midnight (`end_hhmm < start_hhmm`) are supported.
|
||||||
|
When multiple entries overlap, the first match (by list order) wins.
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Configuration is stored in PickleDB at `~/.config/trx-rs/scheduler.db`.
|
||||||
|
|
||||||
|
Keys: `sch:{rig_id}` → JSON `SchedulerConfig`.
|
||||||
|
|
||||||
|
## HTTP API
|
||||||
|
|
||||||
|
All read endpoints are accessible at the **Rx** role level.
|
||||||
|
Write endpoints require the **Control** role.
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| GET | `/scheduler/{rig_id}` | Get scheduler config for a rig |
|
||||||
|
| PUT | `/scheduler/{rig_id}` | Save scheduler config (Control only) |
|
||||||
|
| DELETE | `/scheduler/{rig_id}` | Reset config to Disabled (Control only) |
|
||||||
|
| GET | `/scheduler/{rig_id}/status` | Get last-applied bookmark and next event |
|
||||||
|
|
||||||
|
## Activation logic
|
||||||
|
|
||||||
|
Every 30 seconds the scheduler task checks:
|
||||||
|
1. `context.sse_clients.load() == 0` — no users connected
|
||||||
|
2. Active rig has a non-Disabled scheduler config
|
||||||
|
3. Current UTC time matches a scheduled window or grayline period
|
||||||
|
4. If the matching bookmark differs from `last_applied`, send `SetFreq` + `SetMode`
|
||||||
|
|
||||||
|
The scheduler **does not** revert changes when users reconnect. Bookmarks serve as
|
||||||
|
a frequency map — the user can retune manually after connecting.
|
||||||
|
|
||||||
|
## Data model (Rust)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum SchedulerMode { Disabled, Grayline, TimeSpan }
|
||||||
|
|
||||||
|
pub struct GraylineConfig {
|
||||||
|
pub lat: f64,
|
||||||
|
pub lon: f64,
|
||||||
|
pub transition_window_min: u32,
|
||||||
|
pub day_bookmark_id: Option<String>,
|
||||||
|
pub night_bookmark_id: Option<String>,
|
||||||
|
pub dawn_bookmark_id: Option<String>,
|
||||||
|
pub dusk_bookmark_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScheduleEntry {
|
||||||
|
pub id: String,
|
||||||
|
pub start_hhmm: u32,
|
||||||
|
pub end_hhmm: u32,
|
||||||
|
pub bookmark_id: String,
|
||||||
|
pub label: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SchedulerConfig {
|
||||||
|
pub rig_id: String,
|
||||||
|
pub mode: SchedulerMode,
|
||||||
|
pub grayline: Option<GraylineConfig>,
|
||||||
|
pub entries: Vec<ScheduleEntry>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## UI (Scheduler tab)
|
||||||
|
|
||||||
|
A dedicated sixth tab with a clock icon.
|
||||||
|
|
||||||
|
- **Rig selector**: shows active rig (read-only).
|
||||||
|
- **Mode picker**: Disabled / Grayline / TimeSpan radio buttons.
|
||||||
|
- **Grayline section** (visible when mode = Grayline):
|
||||||
|
- Lat/lon inputs
|
||||||
|
- Transition window slider (5–60 min)
|
||||||
|
- Four bookmark selectors (Dawn / Day / Dusk / Night)
|
||||||
|
- **TimeSpan section** (visible when mode = TimeSpan):
|
||||||
|
- Table of entries with Start, End, Bookmark, Label, Remove button
|
||||||
|
- "Add Entry" row at the bottom
|
||||||
|
- **Status card**: last applied bookmark name and timestamp.
|
||||||
|
- Save button (Control only; form is read-only for Rx users).
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
- Workspace root contains `Cargo.toml`, `README.md`, and contributor docs.
|
||||||
|
- Core crates live under `src/`: `src/trx-core`, `src/trx-server`, and `src/trx-client`.
|
||||||
|
- Server backends are under `src/trx-server/trx-backend` (example: `trx-backend-ft817`).
|
||||||
|
- Client frontends are under `src/trx-client/trx-frontend` (HTTP, JSON, rigctl).
|
||||||
|
- Examples live in `examples/` and static assets in `assets/`.
|
||||||
|
- Reference configs are `trx-server.toml.example` and `trx-client.toml.example`.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
- `cargo build --release` builds optimized binaries.
|
||||||
|
- `cargo test` runs the workspace test suite.
|
||||||
|
- `cargo clippy` runs lint checks.
|
||||||
|
- Example server run (release build): `./target/release/trx-server -r ft817 "/dev/ttyUSB0 9600"`.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
- Rust standard style: 4-space indentation and rustfmt-compatible formatting.
|
||||||
|
- Naming: `snake_case` for modules/functions, `CamelCase` for types/traits, `SCREAMING_SNAKE_CASE` for constants.
|
||||||
|
- Prefer small, crate-focused commits; keep changes localized to the relevant crate.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
- Tests are run via `cargo test` across the workspace.
|
||||||
|
- Add tests near the code they cover (module-level unit tests are preferred).
|
||||||
|
- If you change behavior in a crate, add or update tests in that crate.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
- Commit title format: `[<type>](<crate>): <description>` (example: `[fix](trx-frontend-http): handle disconnect`).
|
||||||
|
- Use `(trx-rs)` for repo-wide changes that are not specific to any crate.
|
||||||
|
- Allowed types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`.
|
||||||
|
- Use imperative mood, keep lines under 80 chars, and separate body with a blank line.
|
||||||
|
- Sign commits with `git commit -s` and include `Co-authored-by:` for LLM assistance.
|
||||||
|
- Write isolated commits for each crate.
|
||||||
|
- Pull requests should include a clear summary, test status, and note any config or runtime changes.
|
||||||
|
|
||||||
|
## Contribution Workflow
|
||||||
|
- Fork the repository and create a new branch for your changes.
|
||||||
|
- Follow the project's coding style and conventions.
|
||||||
|
- Ensure changes are tested and pass existing tests.
|
||||||
|
|
||||||
|
## Configuration & Plugins
|
||||||
|
- Configs use TOML. See the example files for required sections and defaults.
|
||||||
|
- Plugins can be loaded from `./plugins`, `~/.config/trx-rs/plugins`, or `TRX_PLUGIN_DIRS`.
|
||||||
+190
@@ -0,0 +1,190 @@
|
|||||||
|
# HTTP Frontend Authentication Draft
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Add optional passphrase authentication for `trx-frontend-http` with two roles:
|
||||||
|
- `rx` passphrase: read-only access
|
||||||
|
- `control` passphrase: read + control (RX+TX)
|
||||||
|
|
||||||
|
API/control routes stay locked until a user logs in from the web UI.
|
||||||
|
|
||||||
|
This design keeps current behavior when auth is disabled.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Protect HTTP API endpoints used by the web UI.
|
||||||
|
- Protect SSE (`/events`, `/decode`) and audio WebSocket (`/audio`).
|
||||||
|
- Keep static assets and login page accessible so user can authenticate.
|
||||||
|
- Do not change rigctl/http_json auth behavior in this draft.
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
- Two optional passphrases configured locally (`rx`, `control`).
|
||||||
|
- On successful login, server issues short-lived session cookie.
|
||||||
|
- Session required for all protected routes, with role attached.
|
||||||
|
- Brute-force mitigation via simple per-IP rate limiting.
|
||||||
|
- TX access can be globally hidden/blocked unless `control` role is present.
|
||||||
|
|
||||||
|
This is not multi-user IAM; it is a pragmatic local/ham-shack gate.
|
||||||
|
|
||||||
|
## Config Proposal
|
||||||
|
Add to `trx-client.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[frontends.http.auth]
|
||||||
|
enabled = false
|
||||||
|
# Plaintext passphrases (as requested)
|
||||||
|
rx_passphrase = "rx-only-passphrase"
|
||||||
|
control_passphrase = "full-control-passphrase"
|
||||||
|
|
||||||
|
# If true, TX/PTT controls/endpoints are never available without control auth.
|
||||||
|
tx_access_control_enabled = true
|
||||||
|
|
||||||
|
# Session lifetime in minutes
|
||||||
|
session_ttl_min = 480
|
||||||
|
|
||||||
|
# Cookie security
|
||||||
|
cookie_secure = false # true if served via HTTPS
|
||||||
|
cookie_same_site = "Lax" # Strict|Lax|None
|
||||||
|
```
|
||||||
|
|
||||||
|
Validation rules:
|
||||||
|
- If `enabled=false`, all auth fields ignored.
|
||||||
|
- If `enabled=true`, require at least one passphrase (`rx` and/or `control`).
|
||||||
|
- `rx_passphrase` only: read-only deployment.
|
||||||
|
- `control_passphrase` only: control-capable deployment.
|
||||||
|
- both set: mixed deployment with role split.
|
||||||
|
|
||||||
|
Behavior by mode:
|
||||||
|
- `enabled=false` (default): no authentication, current behavior unchanged.
|
||||||
|
- `enabled=true`: authentication enforced per role/route rules in this document.
|
||||||
|
|
||||||
|
## Runtime Structures
|
||||||
|
Add in `src/trx-client/trx-frontend/src/lib.rs` (or HTTP crate-local state):
|
||||||
|
- `HttpAuthConfig`:
|
||||||
|
- `enabled: bool`
|
||||||
|
- `rx_passphrase: Option<String>`
|
||||||
|
- `control_passphrase: Option<String>`
|
||||||
|
- `tx_access_control_enabled: bool`
|
||||||
|
- `session_ttl: Duration`
|
||||||
|
- `cookie_secure: bool`
|
||||||
|
- `same_site: SameSite`
|
||||||
|
- `SessionStore` in-memory map:
|
||||||
|
- key: random session id (128-bit+)
|
||||||
|
- value: `{ role, issued_at, expires_at, last_seen, ip_hash? }`
|
||||||
|
|
||||||
|
Role enum:
|
||||||
|
- `AuthRole::Rx`
|
||||||
|
- `AuthRole::Control`
|
||||||
|
|
||||||
|
Periodic cleanup task (e.g., every 5 min) removes expired sessions.
|
||||||
|
|
||||||
|
## Route Design
|
||||||
|
New endpoints:
|
||||||
|
- `POST /auth/login`
|
||||||
|
- body: `{ "passphrase": "..." }`
|
||||||
|
- server checks passphrase against `control` first, then `rx`
|
||||||
|
- on success: set `HttpOnly` cookie `trx_http_sid`, return `{ role: "rx"|"control" }`
|
||||||
|
- on failure: 401 generic error
|
||||||
|
- `POST /auth/logout`
|
||||||
|
- clears cookie and invalidates server session
|
||||||
|
- `GET /auth/session`
|
||||||
|
- returns `{ authenticated: true|false, role?: "rx"|"control" }`
|
||||||
|
|
||||||
|
Protected existing endpoints:
|
||||||
|
- Control APIs (`control` role required): `/set_freq`, `/set_mode`, `/set_ptt`, `/toggle_power`, `/toggle_vfo`, `/lock`, `/unlock`, `/set_tx_limit`, `/toggle_*_decode`, `/clear_*_decode`, CW tuning endpoints, etc.
|
||||||
|
- Read APIs (`rx` or `control`): `/status`, `/events`, `/decode`, `/audio`
|
||||||
|
|
||||||
|
TX/PTT hard-gate behavior when `tx_access_control_enabled=true`:
|
||||||
|
- Do not render TX/PTT controls for unauthenticated or `rx` role.
|
||||||
|
- Reject TX/PTT and mutating control endpoints unless role is `control`.
|
||||||
|
- Prefer returning `404` for hidden TX/PTT endpoints to avoid capability leakage
|
||||||
|
(or `403` if explicit error semantics are preferred).
|
||||||
|
|
||||||
|
Public endpoints:
|
||||||
|
- `/` (HTML shell)
|
||||||
|
- static assets (`/style.css`, `/app.js`, plugin js, logo, favicon)
|
||||||
|
- `/auth/*`
|
||||||
|
|
||||||
|
## Middleware Behavior
|
||||||
|
Implement Actix middleware/wrap fn in `trx-frontend-http`:
|
||||||
|
- Resolve session from cookie.
|
||||||
|
- Validate in store and expiry.
|
||||||
|
- If missing/invalid:
|
||||||
|
- API routes: return `401` JSON/text
|
||||||
|
- SSE/WS routes: return `401`
|
||||||
|
- If valid:
|
||||||
|
- enforce route role (`rx` or `control`)
|
||||||
|
- return `403` when authenticated but role is insufficient
|
||||||
|
- continue request
|
||||||
|
- optionally slide expiry (`last_seen + ttl`) with cap.
|
||||||
|
|
||||||
|
Keep middleware route-aware by checking request path against allowlist.
|
||||||
|
|
||||||
|
## Passphrase Handling
|
||||||
|
- Use exact passphrase comparison against config values (no hash layer in this draft).
|
||||||
|
- Still use constant-time string comparison helper to reduce timing leakage.
|
||||||
|
- Keep passphrases out of logs and API responses.
|
||||||
|
|
||||||
|
## Cookie Settings
|
||||||
|
Session cookie:
|
||||||
|
- `HttpOnly=true`
|
||||||
|
- `Secure` configurable (true for TLS)
|
||||||
|
- `SameSite=Lax` default
|
||||||
|
- `Path=/`
|
||||||
|
- Max-Age = session TTL
|
||||||
|
|
||||||
|
## Frontend Flow
|
||||||
|
In `assets/web/app.js`:
|
||||||
|
1. On startup call `/auth/session`.
|
||||||
|
2. If unauthenticated, show blocking screen with logo + `Access denied`.
|
||||||
|
3. Submit to `/auth/login`.
|
||||||
|
4. On success initialize normal app flow (`connect()`, decode stream).
|
||||||
|
5. If role is `rx`, disable/hide all TX/PTT/mutating controls.
|
||||||
|
6. If role is `control`, enable full UI.
|
||||||
|
7. If protected call returns 401/403, stop streams and return to login panel.
|
||||||
|
8. Add logout button in About tab or header.
|
||||||
|
|
||||||
|
UI minimal requirement:
|
||||||
|
- Default unauthenticated view: logo + `Access denied` + passphrase field + login button.
|
||||||
|
- Generic error message on failure.
|
||||||
|
- No passphrase persistence in localStorage.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
1. Extend client config structs + parser defaults.
|
||||||
|
2. Build auth state (passphrases + session store) in HTTP server startup.
|
||||||
|
3. Add `/auth/login`, `/auth/logout`, `/auth/session` handlers.
|
||||||
|
4. Add middleware and protect selected routes.
|
||||||
|
5. Update frontend JS with login gate and 401 handling.
|
||||||
|
6. Add docs to `README.md` + `trx-client.toml.example`.
|
||||||
|
7. Add role matrix tests and frontend role UI handling.
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
Unit tests:
|
||||||
|
- Config validation combinations.
|
||||||
|
- Login success/failure.
|
||||||
|
- Session expiry.
|
||||||
|
- Middleware path allowlist/protection.
|
||||||
|
- Role enforcement (`rx` denied on control routes).
|
||||||
|
- TX visibility policy (`tx_access_control_enabled`) endpoint behavior.
|
||||||
|
|
||||||
|
Integration tests (Actix test server):
|
||||||
|
- Unauthed call to `/set_freq` -> 401.
|
||||||
|
- `rx` login -> cookie set -> `/status` accepted, `/set_freq` -> 403.
|
||||||
|
- `control` login -> `/set_freq` accepted.
|
||||||
|
- With `tx_access_control_enabled=true`, unauth/`rx` cannot use `/set_ptt`.
|
||||||
|
- Expired session -> 401.
|
||||||
|
- `/events` and `/audio` reject unauthenticated clients.
|
||||||
|
|
||||||
|
Manual checks:
|
||||||
|
- Browser login works.
|
||||||
|
- WSJT-X/hamlib unaffected (non-http frontends).
|
||||||
|
- Auth disabled mode behaves exactly as before.
|
||||||
|
|
||||||
|
## Operational Notes
|
||||||
|
- This is in-memory session state. Restart invalidates sessions.
|
||||||
|
- For reverse proxy deployments, use TLS and set `cookie_secure=true`.
|
||||||
|
- If remote exposure is possible, use strong passphrase and firewall.
|
||||||
|
|
||||||
|
## Future Extensions
|
||||||
|
- Optional API bearer token for automation scripts.
|
||||||
|
- Optional migration to hashed passphrases if threat model increases.
|
||||||
|
- Persistent sessions with signed tokens/JWT (if needed).
|
||||||
|
- Optional TOTP second factor for internet-exposed deployments.
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
This document lists all currently supported configuration options for `trx-server` and `trx-client`.
|
||||||
|
|
||||||
|
## File Locations
|
||||||
|
|
||||||
|
### `trx-server`
|
||||||
|
Configuration lookup order:
|
||||||
|
1. `--config <FILE>`
|
||||||
|
2. `./trx-server.toml`
|
||||||
|
3. `~/.trx-server.toml`
|
||||||
|
4. `~/.config/trx-rs/server.toml`
|
||||||
|
5. `/etc/trx-rs/server.toml`
|
||||||
|
|
||||||
|
### `trx-client`
|
||||||
|
Configuration lookup order:
|
||||||
|
1. `--config <FILE>`
|
||||||
|
2. `./trx-client.toml`
|
||||||
|
3. `~/.config/trx-rs/client.toml`
|
||||||
|
4. `/etc/trx-rs/client.toml`
|
||||||
|
|
||||||
|
CLI options override file values.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `TRX_PLUGIN_DIRS`: additional plugin directories (path-separated), used by both server and client.
|
||||||
|
|
||||||
|
## `trx-server` Options
|
||||||
|
|
||||||
|
### `[general]`
|
||||||
|
- `callsign` (`string`, default: `"N0CALL"`)
|
||||||
|
- `log_level` (`string`, optional): one of `trace|debug|info|warn|error`
|
||||||
|
- `latitude` (`float`, optional): `-90..=90`
|
||||||
|
- `longitude` (`float`, optional): `-180..=180`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `latitude` and `longitude` must be set together or both omitted.
|
||||||
|
|
||||||
|
### `[rig]`
|
||||||
|
- `model` (`string`, required effectively unless provided by CLI `--rig`)
|
||||||
|
- `initial_freq_hz` (`u64`, default: `144300000`, must be `> 0`)
|
||||||
|
- `initial_mode` (`string`, default: `"USB"`): one of `LSB|USB|CW|CWR|AM|WFM|FM|DIG|PKT`
|
||||||
|
|
||||||
|
### `[rig.access]`
|
||||||
|
- `type` (`string`, default behavior: `serial` if omitted): `serial|tcp|sdr`
|
||||||
|
- Serial mode:
|
||||||
|
- `port` (`string`)
|
||||||
|
- `baud` (`u32`)
|
||||||
|
- TCP mode:
|
||||||
|
- `host` (`string`)
|
||||||
|
- `tcp_port` (`u16`)
|
||||||
|
- SDR mode:
|
||||||
|
- `args` (`string`, required when `type = "sdr"`): SoapySDR device args string (e.g. `"driver=rtlsdr"` or `"driver=airspy,serial=00000001"`). Passed verbatim to `SoapySDR::Device::new()`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- For `serial`, both `port` and `baud` are required.
|
||||||
|
- For `tcp`, both `host` and `tcp_port` are required.
|
||||||
|
- For `sdr`, `args` must be non-empty. The `port`, `baud`, `host`, and `tcp_port` fields are ignored.
|
||||||
|
|
||||||
|
### `[behavior]`
|
||||||
|
- `poll_interval_ms` (`u64`, default: `500`, must be `> 0`)
|
||||||
|
- `poll_interval_tx_ms` (`u64`, default: `100`, must be `> 0`)
|
||||||
|
- `max_retries` (`u32`, default: `3`, must be `> 0`)
|
||||||
|
- `retry_base_delay_ms` (`u64`, default: `100`, must be `> 0`)
|
||||||
|
|
||||||
|
### `[listen]`
|
||||||
|
- `enabled` (`bool`, default: `true`)
|
||||||
|
- `listen` (`ip`, default: `127.0.0.1`)
|
||||||
|
- `port` (`u16`, default: `4530`, must be `> 0` when enabled)
|
||||||
|
|
||||||
|
### `[listen.auth]`
|
||||||
|
- `tokens` (`string[]`, default: `[]`)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Empty token strings are invalid.
|
||||||
|
- Empty list means no auth required.
|
||||||
|
|
||||||
|
### `[audio]`
|
||||||
|
- `enabled` (`bool`, default: `true`)
|
||||||
|
- `listen` (`ip`, default: `127.0.0.1`)
|
||||||
|
- `port` (`u16`, default: `4531`, must be `> 0` when enabled)
|
||||||
|
- `rx_enabled` (`bool`, default: `true`)
|
||||||
|
- `tx_enabled` (`bool`, default: `true`)
|
||||||
|
- `device` (`string`, optional)
|
||||||
|
- `sample_rate` (`u32`, default: `48000`, valid: `8000..=192000`)
|
||||||
|
- `channels` (`u8`, default: `1`, valid: `1|2`)
|
||||||
|
- `frame_duration_ms` (`u16`, default: `20`, valid: `3|5|10|20|40|60`)
|
||||||
|
- `bitrate_bps` (`u32`, default: `24000`, must be `> 0`)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- When `[audio].enabled = true`, at least one of `rx_enabled` or `tx_enabled` must be true.
|
||||||
|
|
||||||
|
### `[pskreporter]`
|
||||||
|
- `enabled` (`bool`, default: `false`)
|
||||||
|
- `host` (`string`, default: `"report.pskreporter.info"`, must not be empty when enabled)
|
||||||
|
- `port` (`u16`, default: `4739`, must be `> 0` when enabled)
|
||||||
|
- `receiver_locator` (`string`, optional)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- If `receiver_locator` is omitted, server tries deriving it from `[general].latitude`/`longitude`.
|
||||||
|
- PSK Reporter software ID is hardcoded to: `trx-server v<version> by SP2SJG`.
|
||||||
|
|
||||||
|
### `[aprsfi]`
|
||||||
|
- `enabled` (`bool`, default: `false`)
|
||||||
|
- `host` (`string`, default: `"rotate.aprs.net"`, must not be empty when enabled)
|
||||||
|
- `port` (`u16`, default: `14580`, must be `> 0` when enabled)
|
||||||
|
- `passcode` (`i32`, default: `-1`)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- When `passcode = -1` (the default), the passcode is auto-computed from `[general].callsign` using the standard APRS-IS hash algorithm.
|
||||||
|
- `[general].callsign` must be non-empty when `[aprsfi].enabled = true`; otherwise the IGate is silently disabled at startup.
|
||||||
|
- Only APRS packets with valid CRC are forwarded; packets from other decoders (FT8, WSPR, CW) are ignored.
|
||||||
|
- The IGate reconnects automatically with exponential backoff (1 s → 2 s → … → 60 s) on TCP errors.
|
||||||
|
- Requires `[audio].enabled = true` (APRS packets are decoded from audio).
|
||||||
|
|
||||||
|
### `[sdr]`
|
||||||
|
- `sample_rate` (`u32`, default: `1920000`, must be `> 0`): IQ capture rate in Hz. Must be supported by the device.
|
||||||
|
- `bandwidth` (`u32`, default: `1500000`): Hardware IF filter bandwidth in Hz.
|
||||||
|
- `center_offset_hz` (`i64`, default: `100000`): The SDR tunes this many Hz below the dial frequency to keep the signal off the DC spur. Negative values tune above.
|
||||||
|
|
||||||
|
### `[sdr.gain]`
|
||||||
|
- `mode` (`string`, default: `"auto"`): `"auto"` enables hardware AGC (falls back to `"manual"` with a warning if the device does not support it); `"manual"` uses the fixed `value`.
|
||||||
|
- `value` (`f64`, default: `30.0`): Gain in dB. Used only when `mode = "manual"`.
|
||||||
|
|
||||||
|
### `[sdr.squelch]`
|
||||||
|
- `enabled` (`bool`, default: `false`): Enables virtual software squelch for demodulated audio except WFM on the primary SDR channel.
|
||||||
|
- `threshold_db` (`f32`, default: `-65.0`, valid: `-140..=0`): Open threshold in dBFS.
|
||||||
|
- `hysteresis_db` (`f32`, default: `3.0`, valid: `0..=40`): Close hysteresis in dB.
|
||||||
|
- `tail_ms` (`u32`, default: `180`, valid: `0..=10000`): Tail hold time after signal drops below threshold.
|
||||||
|
|
||||||
|
### `[[sdr.channels]]`
|
||||||
|
|
||||||
|
Defines one virtual receiver channel within the wideband IQ stream. At least one channel is required when using the `soapysdr` backend. The **first** channel in the list is the *primary* channel: `set_freq` and `set_mode` from rig control apply to it, and `get_status` reads from it.
|
||||||
|
|
||||||
|
- `id` (`string`, default: `""`): Human-readable label used in logs.
|
||||||
|
- `offset_hz` (`i64`, default: `0`): Frequency offset from the dial frequency in Hz. Primary channel should be `0`.
|
||||||
|
- `mode` (`string`, default: `"auto"`): Demodulation mode. `"auto"` follows the RigCat `set_mode` command; or a fixed mode string: `LSB`, `USB`, `CW`, `CWR`, `AM`, `WFM`, `FM`, `DIG`, `PKT`.
|
||||||
|
- `audio_bandwidth_hz` (`u32`, default: `3000`): One-sided bandwidth of the post-demodulation audio BPF in Hz.
|
||||||
|
- `fir_taps` (`usize`, default: `64`): FIR filter tap count. Higher values give sharper roll-off at the cost of latency.
|
||||||
|
- `cw_center_hz` (`u32`, default: `700`): CW tone centre frequency in the audio domain (Hz).
|
||||||
|
- `wfm_bandwidth_hz` (`u32`, default: `75000`): Pre-demodulation filter bandwidth for WFM only (Hz).
|
||||||
|
- `decoders` (`string[]`, default: `[]`): Decoder IDs that receive this channel's PCM audio. Valid values: `"ft8"`, `"wspr"`, `"aprs"`, `"cw"`. Each decoder ID may appear in at most one channel.
|
||||||
|
- `stream_opus` (`bool`, default: `false`): Encode this channel's audio as Opus and stream to clients over the TCP audio port. At most one channel may set this to `true`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Requires `libSoapySDR` installed (`brew install soapysdr` on macOS; `libsoapysdr-dev` on Debian/Ubuntu).
|
||||||
|
- The SDR backend is RX-only. `[audio] tx_enabled` must be `false`.
|
||||||
|
- Channel IF constraint: `|center_offset_hz + offset_hz| < sample_rate / 2` for every channel; violated channels cause a startup error.
|
||||||
|
- `[audio] sample_rate` must match the output audio rate of the SDR pipeline (48000 Hz recommended).
|
||||||
|
- Use `trx-server --print-config` to see all defaults. SDR fields appear only if the binary was built with `--features soapysdr`.
|
||||||
|
|
||||||
|
### `[decode_logs]`
|
||||||
|
- `enabled` (`bool`, default: `false`)
|
||||||
|
- `dir` (`string`, default: `"$XDG_DATA_HOME/trx-rs/decoders"`; fallback: `"logs/decoders"`, must not be empty when enabled)
|
||||||
|
- `aprs_file` (`string`, default: `"TRXRS-APRS-%YYYY%-%MM%-%DD%.log"`, must not be empty when enabled)
|
||||||
|
- `cw_file` (`string`, default: `"TRXRS-CW-%YYYY%-%MM%-%DD%.log"`, must not be empty when enabled)
|
||||||
|
- `ft8_file` (`string`, default: `"TRXRS-FT8-%YYYY%-%MM%-%DD%.log"`, must not be empty when enabled)
|
||||||
|
- `wspr_file` (`string`, default: `"TRXRS-WSPR-%YYYY%-%MM%-%DD%.log"`, must not be empty when enabled)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Decoder logs are server-side and split by decoder name (APRS/CW/FT8/WSPR).
|
||||||
|
- Files are appended in JSON Lines format (one JSON object per line).
|
||||||
|
- Supported filename date tokens: `%YYYY%`, `%MM%`, `%DD%` (UTC date).
|
||||||
|
|
||||||
|
## `trx-client` Options
|
||||||
|
|
||||||
|
### `[general]`
|
||||||
|
- `callsign` (`string`, default: `"N0CALL"`)
|
||||||
|
- `log_level` (`string`, optional): one of `trace|debug|info|warn|error`
|
||||||
|
|
||||||
|
### `[remote]`
|
||||||
|
- `url` (`string`, optional in file but required at runtime unless provided by CLI `--url`)
|
||||||
|
- `poll_interval_ms` (`u64`, default: `750`, must be `> 0`)
|
||||||
|
|
||||||
|
### `[remote.auth]`
|
||||||
|
- `token` (`string`, optional)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- If provided, token must not be empty/whitespace.
|
||||||
|
|
||||||
|
### `[frontends.http]`
|
||||||
|
- `enabled` (`bool`, default: `true`)
|
||||||
|
- `listen` (`ip`, default: `127.0.0.1`)
|
||||||
|
- `port` (`u16`, default: `8080`, must be `> 0` when enabled)
|
||||||
|
|
||||||
|
### `[frontends.rigctl]`
|
||||||
|
- `enabled` (`bool`, default: `false`)
|
||||||
|
- `listen` (`ip`, default: `127.0.0.1`)
|
||||||
|
- `port` (`u16`, default: `4532`, must be `> 0` when enabled)
|
||||||
|
|
||||||
|
### `[frontends.http_json]`
|
||||||
|
- `enabled` (`bool`, default: `true`)
|
||||||
|
- `listen` (`ip`, default: `127.0.0.1`)
|
||||||
|
- `port` (`u16`, default: `0`)
|
||||||
|
- `auth.tokens` (`string[]`, default: `[]`)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `port = 0` means ephemeral bind (allowed).
|
||||||
|
- Empty token strings are invalid.
|
||||||
|
|
||||||
|
### `[frontends.audio]`
|
||||||
|
- `enabled` (`bool`, default: `true`)
|
||||||
|
- `server_port` (`u16`, default: `4531`, must be `> 0` when enabled)
|
||||||
|
- `bridge.enabled` (`bool`, default: `false`): enables local `cpal` audio bridge
|
||||||
|
- `bridge.rx_output_device` (`string`, optional): exact local playback device name
|
||||||
|
- `bridge.tx_input_device` (`string`, optional): exact local capture device name
|
||||||
|
- `bridge.rx_gain` (`float`, default: `1.0`, must be finite and `>= 0`)
|
||||||
|
- `bridge.tx_gain` (`float`, default: `1.0`, must be finite and `>= 0`)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The bridge is intended for local WSJT-X integration via virtual audio devices.
|
||||||
|
- Linux: typically use ALSA loopback (`snd-aloop`).
|
||||||
|
- macOS: install a virtual CoreAudio device (e.g. BlackHole), then set device names above.
|
||||||
|
|
||||||
|
## CLI Override Summary
|
||||||
|
|
||||||
|
### `trx-server`
|
||||||
|
- `--config`, `--print-config`
|
||||||
|
- `--rig`, `--access`, positional `RIG_ADDR`
|
||||||
|
- `--callsign`
|
||||||
|
- `--listen`, `--port` (JSON listener)
|
||||||
|
- SDR backend: all SDR options are file-only (`[sdr]` and `[[sdr.channels]]`).
|
||||||
|
|
||||||
|
### `trx-client`
|
||||||
|
- `--config`, `--print-config`
|
||||||
|
- `--url`, `--token`, `--poll-interval`
|
||||||
|
- `--frontend` (comma-separated)
|
||||||
|
- `--http-listen`, `--http-port`
|
||||||
|
- `--rigctl-listen`, `--rigctl-port`
|
||||||
|
- `--http-json-listen`, `--http-json-port`
|
||||||
|
- `--callsign`
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Top 5 Real Architecture Issues (Post-Refactor)
|
||||||
|
|
||||||
|
## 1) Plugin ABI is still brittle and unversioned
|
||||||
|
### Files
|
||||||
|
- `src/trx-app/src/plugins.rs`
|
||||||
|
- `examples/trx-plugin-example/src/lib.rs`
|
||||||
|
|
||||||
|
### Why this matters
|
||||||
|
Plugin loading is now explicit (good), but still assumes exact symbol names and raw FFI contracts with no ABI/version handshake. A plugin built against an older/newer ABI can fail at runtime in hard-to-diagnose ways.
|
||||||
|
|
||||||
|
### Fix steps
|
||||||
|
1. Add an ABI version symbol/handshake (`trx_plugin_abi_version`) and reject incompatible plugins with clear errors.
|
||||||
|
2. Split plugin capability metadata (backend/frontend/both) from registration symbols to avoid noisy failed-load logs.
|
||||||
|
3. Provide a tiny shared plugin-API crate for stable entrypoint signatures.
|
||||||
|
|
||||||
|
## 2) Runtime supervision is still ad-hoc (sleep + abort)
|
||||||
|
### Files
|
||||||
|
- `src/trx-server/src/main.rs`
|
||||||
|
- `src/trx-client/src/main.rs`
|
||||||
|
|
||||||
|
### Why this matters
|
||||||
|
Shutdown is coordinated, but supervision still uses a fixed delay plus manual `abort()` over `Vec<JoinHandle<_>>`. This can mask task failures, race shutdown ordering, and make lifecycle behavior harder to reason about.
|
||||||
|
|
||||||
|
### Fix steps
|
||||||
|
1. Move to `JoinSet` (or a small supervisor type) for task ownership and result handling.
|
||||||
|
2. Replace fixed sleep with bounded graceful-join timeout logic.
|
||||||
|
3. Surface task failure reasons consistently in one place.
|
||||||
|
|
||||||
|
## 3) JSON/TCP transport logic is duplicated across modules
|
||||||
|
### Files
|
||||||
|
- `src/trx-server/src/listener.rs`
|
||||||
|
- `src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs`
|
||||||
|
- `src/trx-client/src/remote_client.rs`
|
||||||
|
|
||||||
|
### Why this matters
|
||||||
|
`read_limited_line`, timeout handling, and response write patterns are repeated in multiple places. This increases drift risk and makes protocol hardening changes expensive.
|
||||||
|
|
||||||
|
### Fix steps
|
||||||
|
1. Extract shared JSON-over-TCP helpers into `trx-protocol` (or a small transport crate/module).
|
||||||
|
2. Keep one source of truth for max line size, timeout behavior, and framing errors.
|
||||||
|
3. Cover shared transport with focused tests once instead of per-module copies.
|
||||||
|
|
||||||
|
## 4) Boundary tests are present but mostly ignored in constrained envs
|
||||||
|
### Files
|
||||||
|
- `src/trx-server/src/listener.rs`
|
||||||
|
- `src/trx-client/src/remote_client.rs`
|
||||||
|
- `src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs`
|
||||||
|
|
||||||
|
### Why this matters
|
||||||
|
Important network-path tests exist, but are marked `#[ignore]` in this environment due bind restrictions. Without a clear CI strategy, regressions can still slip through.
|
||||||
|
|
||||||
|
### Fix steps
|
||||||
|
1. Add CI jobs/environment where bind-based tests run by default.
|
||||||
|
2. Split pure transport logic from socket bind/accept so more behavior can be tested without real sockets.
|
||||||
|
3. Keep ignored tests minimal and document how/when they run.
|
||||||
|
|
||||||
|
## 5) Decode/history shared state still relies on global mutexes
|
||||||
|
### Files
|
||||||
|
- `src/trx-server/src/audio.rs`
|
||||||
|
- `src/trx-client/trx-frontend/src/lib.rs`
|
||||||
|
- `src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs`
|
||||||
|
|
||||||
|
### Why this matters
|
||||||
|
History/state paths still use shared mutex-backed globals/contexts with `expect` on lock poisoning in hot paths. This is workable but fragile for long-running async services.
|
||||||
|
|
||||||
|
### Fix steps
|
||||||
|
1. Replace panic-on-poison lock usage with resilient handling.
|
||||||
|
2. Consider bounded channel or lock-free append/read model for decode history.
|
||||||
|
3. Define explicit ownership/lifetime for history data instead of implicit shared mutation.
|
||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
# Multi-Rig Support
|
||||||
|
|
||||||
|
This document specifies the requirements for running N simultaneous rig backends in one `trx-server` process and the protocol/config changes required to support them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
> **For AI agents:** This section is the single source of truth for implementation status.
|
||||||
|
> Each task has a unique ID (e.g. `MR-01`), a status badge, a description, the files it touches, and any blocking dependencies.
|
||||||
|
>
|
||||||
|
> Status legend: `[ ]` not started · `[~]` in progress · `[x]` done · `[!]` blocked
|
||||||
|
|
||||||
|
### Foundational (parallel)
|
||||||
|
|
||||||
|
| ID | Status | Task | Files | Needs |
|
||||||
|
|----|--------|------|-------|-------|
|
||||||
|
| MR-01 | `[x]` | Add `rig_id: Option<String>` to `ClientEnvelope`; add `rig_id: Option<String>` to `ClientResponse`; add `ClientCommand::GetRigs`; add `GetRigsResponseBody` + `RigEntry`; add sentinel arm in `mapping.rs` | `src/trx-protocol/src/types.rs`, `mapping.rs`, `lib.rs` | — |
|
||||||
|
| MR-02 | `[x]` | Add `RigInstanceConfig`; add `rigs: Vec<RigInstanceConfig>` to `ServerConfig`; implement `resolved_rigs()`; extend `validate()` for unique IDs + unique audio ports | `src/trx-server/src/config.rs` | — |
|
||||||
|
| MR-03 | `[x]` | Remove four `OnceLock` statics from `audio.rs`; add `DecoderHistories { aprs, ft8, wspr }` struct + `new()`; convert history free-fns to take `&DecoderHistories`; update decoder task signatures + `run_audio_listener` | `src/trx-server/src/audio.rs` | — |
|
||||||
|
| MR-04 | `[x]` | Create `src/trx-server/src/rig_handle.rs` with `RigHandle { rig_id, rig_tx, state_rx }`; declare mod in `main.rs` | `src/trx-server/src/rig_handle.rs`, `main.rs` | — |
|
||||||
|
|
||||||
|
### Sequential
|
||||||
|
|
||||||
|
| ID | Status | Task | Files | Needs |
|
||||||
|
|----|--------|------|-------|-------|
|
||||||
|
| MR-05 | `[x]` | Add `rig_id: String` + `histories: Arc<DecoderHistories>` to `RigTaskConfig`; fix `clear_*_history` calls in `process_command` | `src/trx-server/src/rig_task.rs` | MR-03 |
|
||||||
|
| MR-06 | `[x]` | Rewrite `run_listener` to take `Arc<HashMap<String, RigHandle>>` + `default_rig_id`; route by `envelope.rig_id`; add `GetRigs` fast path; populate `rig_id` in every `ClientResponse` | `src/trx-server/src/listener.rs` | MR-01, MR-04 |
|
||||||
|
| MR-07 | `[x]` | Rewrite `main.rs` spawn loop over `resolved_rigs()`; extract `spawn_rig_audio_stack()`; per-rig pskreporter + aprsfi; build `HashMap<String, RigHandle>`; pass to `run_listener` | `src/trx-server/src/main.rs` | MR-02–06 |
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
| ID | Status | Task | Files | Needs |
|
||||||
|
|----|--------|------|-------|-------|
|
||||||
|
| MR-08 | `[x]` | Config tests: `resolved_rigs()` with multi-rig TOML and legacy TOML; duplicate ID/port rejection | `src/trx-server/src/config.rs` | MR-02 |
|
||||||
|
| MR-09 | `[x]` | Protocol tests: `ClientEnvelope` absent `rig_id` parses; `rig_id` in responses; `GetRigs` round-trip; existing tests still pass | `src/trx-protocol/src/codec.rs` | MR-01 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Run N simultaneous rig backends (SDR, transceivers, or any mix) in one server process
|
||||||
|
- Route control commands to the correct rig via `rig_id` in the JSON protocol
|
||||||
|
- Backward compatibility: single-rig configs (`[rig]`/`[audio]` at top level) continue to work unchanged
|
||||||
|
- Per-rig audio streaming on separate TCP ports
|
||||||
|
- New `GetRigs` command to enumerate all connected rigs and their states
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Load-balancing or failover between rigs
|
||||||
|
- Sharing a single audio port across multiple rigs (each rig keeps its own port)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Single [listen] port (4530)
|
||||||
|
└─ listener.rs: Arc<HashMap<rig_id, RigHandle>>
|
||||||
|
├─ route by envelope.rig_id (absent → first rig, backward compat)
|
||||||
|
└─ GetRigs → aggregate all states
|
||||||
|
|
||||||
|
Per-rig:
|
||||||
|
rig_task ←→ RigHandle (rig_tx + state_rx)
|
||||||
|
audio capture → pcm_tx → decoder tasks → decode_tx
|
||||||
|
run_audio_listener (own TCP port per rig)
|
||||||
|
pskreporter + aprsfi tasks
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TOML Format
|
||||||
|
|
||||||
|
### Multi-rig (`[[rigs]]` array)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[general]
|
||||||
|
callsign = "W1AW"
|
||||||
|
|
||||||
|
[listen]
|
||||||
|
port = 4530
|
||||||
|
|
||||||
|
[[rigs]]
|
||||||
|
id = "hf"
|
||||||
|
[rigs.rig]
|
||||||
|
model = "ft450d"
|
||||||
|
initial_freq_hz = 14074000
|
||||||
|
[rigs.rig.access]
|
||||||
|
type = "serial"
|
||||||
|
port = "/dev/ttyUSB0"
|
||||||
|
baud = 9600
|
||||||
|
[rigs.audio]
|
||||||
|
port = 4531
|
||||||
|
|
||||||
|
[[rigs]]
|
||||||
|
id = "sdr"
|
||||||
|
[rigs.rig]
|
||||||
|
model = "soapysdr"
|
||||||
|
[rigs.rig.access]
|
||||||
|
type = "sdr"
|
||||||
|
args = "driver=rtlsdr"
|
||||||
|
[rigs.audio]
|
||||||
|
port = 4532
|
||||||
|
[rigs.sdr]
|
||||||
|
sample_rate = 1920000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Legacy (flat `[rig]` + `[audio]`) — continues to work unchanged
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[rig]
|
||||||
|
model = "ft817"
|
||||||
|
[rig.access]
|
||||||
|
type = "serial"
|
||||||
|
port = "/dev/ttyUSB0"
|
||||||
|
baud = 9600
|
||||||
|
[audio]
|
||||||
|
port = 4531
|
||||||
|
```
|
||||||
|
|
||||||
|
Legacy configs are synthesised into a single-element `[[rigs]]` list with `id = "default"` via `resolved_rigs()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Wire Format
|
||||||
|
|
||||||
|
Request (`rig_id` optional; absent = first rig):
|
||||||
|
```json
|
||||||
|
{"rig_id": "hf", "cmd": "set_freq", "freq_hz": 14074000}
|
||||||
|
{"cmd": "get_state"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response (`rig_id` always present):
|
||||||
|
```json
|
||||||
|
{"success": true, "rig_id": "hf", "state": {...}}
|
||||||
|
{"success": false, "rig_id": "default", "error": "Unknown rig_id: xyz"}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GetRigs` response:
|
||||||
|
```json
|
||||||
|
{"success": true, "rig_id": "server", "rigs": [
|
||||||
|
{"rig_id": "hf", "state": {...}},
|
||||||
|
{"rig_id": "sdr", "state": {...}}
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Rules (startup)
|
||||||
|
|
||||||
|
- When `[[rigs]]` is non-empty: each `id` must be unique (case-sensitive).
|
||||||
|
- When `[[rigs]]` is non-empty: each `audio.port` must be unique.
|
||||||
|
- When `[[rigs]]` is empty: legacy flat fields are used with `id = "default"`.
|
||||||
|
- Mixing `[[rigs]]` and legacy flat `[rig]`/`[audio]` is undefined; `[[rigs]]` takes precedence.
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
# trx-rs Project Overview
|
||||||
|
|
||||||
|
## What is trx-rs?
|
||||||
|
|
||||||
|
**trx-rs** is a modular transceiver (radio) control stack written in Rust. It provides a backend service for controlling amateur radio transceivers via CAT (Computer-Aided Transceiver) protocols, with multiple frontend interfaces for access and monitoring.
|
||||||
|
|
||||||
|
### Current Capabilities
|
||||||
|
|
||||||
|
| Feature | Status |
|
||||||
|
|---------|--------|
|
||||||
|
| Yaesu FT-817 CAT control | Implemented |
|
||||||
|
| HTTP/Web UI with SSE | Implemented |
|
||||||
|
| rigctl-compatible TCP | Implemented |
|
||||||
|
| VFO A/B switching | Implemented |
|
||||||
|
| PTT control | Implemented |
|
||||||
|
| Signal/TX power metering | Implemented |
|
||||||
|
| Front panel lock | Implemented |
|
||||||
|
| Multiple rig backends | Extensible (only FT-817) |
|
||||||
|
| Backend/frontend registry | Implemented |
|
||||||
|
| TCP CAT transport | Partial (config wiring only) |
|
||||||
|
| JSON TCP control (line-delimited) | Implemented (configurable frontend) |
|
||||||
|
| Plugin registry loading | Implemented (shared libraries) |
|
||||||
|
| Configuration file (TOML) | Implemented |
|
||||||
|
| Rig state machine | Implemented |
|
||||||
|
| Command handlers | Implemented |
|
||||||
|
| Event notifications | Implemented (rig task emits events) |
|
||||||
|
| Retry/polling policies | Implemented |
|
||||||
|
| Controller-based rig task | Implemented |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ trx-server/trx-client │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Application │ │
|
||||||
|
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
|
||||||
|
│ │ │ Config │ │ CLI │ │ Rig Task │ │ │
|
||||||
|
│ │ │ (TOML file) │ │ (clap) │ │ (main loop) │ │ │
|
||||||
|
│ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────────────┴───────────────────┐ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ ┌─────────────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ trx-core │ │ Frontend Layer │ │
|
||||||
|
│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
|
||||||
|
│ │ │ controller/ │ │ │ │ HTTP │ │ │
|
||||||
|
│ │ │ - machine │ │ │ │ (REST+SSE) │ │ │
|
||||||
|
│ │ │ - handlers │ │ │ └───────────────┘ │ │
|
||||||
|
│ │ │ - events │ │ │ ┌───────────────┐ │ │
|
||||||
|
│ │ │ - policies │ │ │ │ HTTP JSON │ │ │
|
||||||
|
│ │ └───────────────┘ │ │ │ (TCP/JSON) │ │ │
|
||||||
|
│ └─────────────────────┘ │ └───────────────┘ │ │
|
||||||
|
│ │ │ ┌───────────────┐ │ │
|
||||||
|
│ │ │ │ rigctl │ │ │
|
||||||
|
│ │ │ │ (TCP/hamlib) │ │ │
|
||||||
|
│ │ │ └───────────────┘ │ │
|
||||||
|
│ │ └─────────────────────┘ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────┐ │
|
||||||
|
│ │ trx-backend │ │
|
||||||
|
│ │ ┌───────────────┐ │ │
|
||||||
|
│ │ │ FT-817 Driver │ │ │
|
||||||
|
│ │ └───────────────┘ │ │
|
||||||
|
│ └─────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
| Component | Purpose |
|
||||||
|
|-----------|---------|
|
||||||
|
| `trx-core` | Core types, traits (`Rig`, `RigCat`), state definitions, controller components |
|
||||||
|
| `trx-core/rig/controller` | State machine, command handlers, event system, policies |
|
||||||
|
| `trx-backend` | Backend factory and abstraction layer |
|
||||||
|
| `trx-backend-ft817` | FT-817 CAT protocol implementation |
|
||||||
|
| `trx-frontend` | Frontend trait (`FrontendSpawner`) |
|
||||||
|
| `trx-frontend-http` | Web UI with REST API and SSE |
|
||||||
|
| `trx-frontend-http-json` | JSON-over-TCP control frontend |
|
||||||
|
| `trx-frontend-rigctl` | Hamlib rigctl-compatible TCP interface |
|
||||||
|
| `trx-server` | Server binary — connects to rig backend, exposes JSON TCP control |
|
||||||
|
| `trx-client` | Client binary — connects to server, runs frontends (HTTP, rigctl) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
trx-rs supports TOML configuration files with the following search order:
|
||||||
|
|
||||||
|
1. `--config <path>` (explicit CLI argument)
|
||||||
|
2. `./trx-server.toml` or `./trx-client.toml` (current directory)
|
||||||
|
3. `~/.config/trx-rs/config.toml` (XDG user config)
|
||||||
|
4. `/etc/trx-rs/config.toml` (system-wide)
|
||||||
|
|
||||||
|
CLI arguments override config file values.
|
||||||
|
|
||||||
|
Plugin discovery:
|
||||||
|
- Uses shared libraries with a `trx_register` entrypoint.
|
||||||
|
- Searches `./plugins`, `~/.config/trx-rs/plugins`, and any paths in `TRX_PLUGIN_DIRS`.
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[general]
|
||||||
|
callsign = "N0CALL"
|
||||||
|
|
||||||
|
[rig]
|
||||||
|
model = "ft817"
|
||||||
|
initial_freq_hz = 144300000
|
||||||
|
initial_mode = "USB"
|
||||||
|
|
||||||
|
[rig.access]
|
||||||
|
type = "serial"
|
||||||
|
port = "/dev/ttyUSB0"
|
||||||
|
baud = 9600
|
||||||
|
|
||||||
|
[frontends.http]
|
||||||
|
enabled = true
|
||||||
|
listen = "127.0.0.1"
|
||||||
|
port = 8080
|
||||||
|
|
||||||
|
[frontends.rigctl]
|
||||||
|
enabled = true
|
||||||
|
listen = "127.0.0.1"
|
||||||
|
port = 4532
|
||||||
|
|
||||||
|
[frontends.http_json]
|
||||||
|
enabled = true
|
||||||
|
listen = "127.0.0.1"
|
||||||
|
port = 9000
|
||||||
|
auth.tokens = ["demo-token"]
|
||||||
|
|
||||||
|
[behavior]
|
||||||
|
poll_interval_ms = 500
|
||||||
|
poll_interval_tx_ms = 100
|
||||||
|
max_retries = 3
|
||||||
|
retry_base_delay_ms = 100
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `trx-server --print-config` or `trx-client --print-config` to generate an example configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rig Controller Components
|
||||||
|
|
||||||
|
Located in `trx-core/src/rig/controller/`:
|
||||||
|
|
||||||
|
### State Machine (`machine.rs`)
|
||||||
|
|
||||||
|
Explicit state machine for rig lifecycle management:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum RigMachineState {
|
||||||
|
Disconnected,
|
||||||
|
Connecting { started_at: Option<u64> },
|
||||||
|
Initializing { rig_info: Option<RigInfo> },
|
||||||
|
PoweredOff { rig_info: RigInfo },
|
||||||
|
Ready(ReadyStateData),
|
||||||
|
Transmitting(TransmittingStateData),
|
||||||
|
Error { error: RigStateError, previous_state: Box<RigMachineState> },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Events trigger state transitions:
|
||||||
|
- `RigEvent::Connected`, `Initialized`, `PoweredOn`, `PoweredOff`
|
||||||
|
- `RigEvent::PttOn`, `PttOff`
|
||||||
|
- `RigEvent::Error(RigStateError)`, `Recovered`, `Disconnected`
|
||||||
|
|
||||||
|
### Command Handlers (`handlers.rs`)
|
||||||
|
|
||||||
|
Trait-based command system with validation:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait RigCommandHandler: Debug + Send + Sync {
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
fn can_execute(&self, ctx: &dyn CommandContext) -> ValidationResult;
|
||||||
|
fn execute<'a>(&'a self, executor: &'a mut dyn CommandExecutor)
|
||||||
|
-> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implemented commands:
|
||||||
|
- `SetFreqCommand`, `SetModeCommand`, `SetPttCommand`
|
||||||
|
- `PowerOnCommand`, `PowerOffCommand`
|
||||||
|
- `ToggleVfoCommand`, `LockCommand`, `UnlockCommand`
|
||||||
|
- `GetTxLimitCommand`, `SetTxLimitCommand`, `GetSnapshotCommand`
|
||||||
|
|
||||||
|
The rig task (`trx-server/src/rig_task.rs`) now syncs the state machine to the live `RigState`
|
||||||
|
and emits events whenever rig status changes.
|
||||||
|
|
||||||
|
### Event Notifications (`events.rs`)
|
||||||
|
|
||||||
|
Typed event system for rig state changes:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait RigListener: Send + Sync {
|
||||||
|
fn on_frequency_change(&self, old: Option<Freq>, new: Freq);
|
||||||
|
fn on_mode_change(&self, old: Option<&RigMode>, new: &RigMode);
|
||||||
|
fn on_ptt_change(&self, transmitting: bool);
|
||||||
|
fn on_state_change(&self, old: &RigMachineState, new: &RigMachineState);
|
||||||
|
fn on_meter_update(&self, rx: Option<&RigRxStatus>, tx: Option<&RigTxStatus>);
|
||||||
|
fn on_lock_change(&self, locked: bool);
|
||||||
|
fn on_power_change(&self, powered: bool);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RigEventEmitter {
|
||||||
|
// Manages listeners and dispatches events
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Policies (`policies.rs`)
|
||||||
|
|
||||||
|
Configurable retry and polling behavior:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait RetryPolicy: Send + Sync {
|
||||||
|
fn should_retry(&self, attempt: u32, error: &RigError) -> bool;
|
||||||
|
fn delay(&self, attempt: u32) -> Duration;
|
||||||
|
fn max_attempts(&self) -> u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PollingPolicy: Send + Sync {
|
||||||
|
fn interval(&self, transmitting: bool) -> Duration;
|
||||||
|
fn should_poll(&self, transmitting: bool) -> bool;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementations:
|
||||||
|
- `ExponentialBackoff` - Exponential delay with max cap
|
||||||
|
- `FixedDelay` - Constant delay between retries
|
||||||
|
- `NoRetry` - Fail immediately
|
||||||
|
- `AdaptivePolling` - Faster polling during TX
|
||||||
|
- `FixedPolling` - Constant interval
|
||||||
|
- `NoPolling` - Disable automatic polling
|
||||||
|
|
||||||
|
### Error Types
|
||||||
|
|
||||||
|
`RigError` now includes error classification:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct RigError {
|
||||||
|
pub message: String,
|
||||||
|
pub kind: RigErrorKind, // Transient or Permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RigError {
|
||||||
|
pub fn timeout() -> Self; // Transient
|
||||||
|
pub fn communication(msg) -> Self; // Transient
|
||||||
|
pub fn invalid_state(msg) -> Self; // Permanent
|
||||||
|
pub fn not_supported(op) -> Self; // Permanent
|
||||||
|
pub fn is_transient(&self) -> bool;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remaining Improvement Opportunities
|
||||||
|
|
||||||
|
### Integration Work
|
||||||
|
|
||||||
|
1. **Plugin UX improvements** - Add structured plugin metadata (name, version, capabilities) and surface it in CLI help.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Add integration tests with mock backends
|
||||||
|
- Add more backend/frontend unit tests
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add more rig backends (IC-7300, TS-590, etc.)
|
||||||
|
- Add TX limit support for FT-817 (or document per-backend constraints in UI)
|
||||||
|
- Add WebSocket support for bidirectional communication
|
||||||
|
- Add metrics/telemetry export (Prometheus)
|
||||||
|
- Add authentication for HTTP frontend
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- Add CI/CD pipeline
|
||||||
|
- Add pre-commit hooks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Status
|
||||||
|
|
||||||
|
| Component | Status | Tests |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| State Machine | Implemented | 5 tests |
|
||||||
|
| Command Handlers | Implemented | 3 tests |
|
||||||
|
| Event Notifications | Implemented | 2 tests |
|
||||||
|
| Retry/Polling Policies | Implemented | 5 tests |
|
||||||
|
| Config File Support | Implemented | 4 tests |
|
||||||
|
| rigctl Frontend | Implemented | - |
|
||||||
|
| HTTP Frontend | Implemented | - |
|
||||||
|
| FT-817 Backend | Implemented | - |
|
||||||
|
|
||||||
|
**Total: 19 unit tests passing**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Run server with CLI args
|
||||||
|
./target/release/trx-server -r ft817 "/dev/ttyUSB0 9600"
|
||||||
|
|
||||||
|
# Run server with config file
|
||||||
|
./target/release/trx-server --config /path/to/config.toml
|
||||||
|
|
||||||
|
# Run client
|
||||||
|
./target/release/trx-client --config /path/to/client-config.toml
|
||||||
|
|
||||||
|
# Print example config
|
||||||
|
./target/release/trx-server --print-config > trx-server.toml
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Run clippy
|
||||||
|
cargo clippy
|
||||||
|
```
|
||||||
+401
@@ -0,0 +1,401 @@
|
|||||||
|
# SDR Backend Requirements
|
||||||
|
|
||||||
|
This document specifies the requirements for a SoapySDR-based RX-only backend (`trx-backend-soapysdr`) and the associated IQ-to-audio pipeline changes in `trx-server`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
> **For AI agents:** This section is the single source of truth for implementation status.
|
||||||
|
> Each task has a unique ID (e.g. `SDR-01`), a status badge, a description, the files it touches, and any blocking dependencies.
|
||||||
|
> Pick any task whose status is `[ ]` and whose `Needs` list is fully `[x]`. Update status to `[~]` while working, `[x]` when merged. Record notes under the task if you hit non-obvious issues.
|
||||||
|
>
|
||||||
|
> Status legend: `[ ]` not started · `[~]` in progress · `[x]` done · `[!]` blocked
|
||||||
|
|
||||||
|
### Foundational (must land first)
|
||||||
|
|
||||||
|
| ID | Status | Task | Touches |
|
||||||
|
|----|--------|------|---------|
|
||||||
|
| SDR-01 | `[x]` | Add `AudioSource` trait to `trx-core`; add `as_audio_source()` default on `RigCat` | `src/trx-core/src/rig/mod.rs` |
|
||||||
|
| SDR-02 | `[x]` | Add `RigAccess::Sdr { args: String }` variant; register `soapysdr` factory (feature-gated `soapysdr`) | `src/trx-server/trx-backend/src/lib.rs` |
|
||||||
|
| SDR-03 | `[x]` | Add `SdrConfig`, `SdrGainConfig`, `SdrChannelConfig` structs; parse `type = "sdr"` in `AccessConfig`; add `sdr: SdrConfig` to `ServerConfig`; add startup validation rules (§11) | `src/trx-server/src/config.rs` |
|
||||||
|
|
||||||
|
### New crate: `trx-backend-soapysdr`
|
||||||
|
|
||||||
|
| ID | Status | Task | Touches | Needs |
|
||||||
|
|----|--------|------|---------|-------|
|
||||||
|
| SDR-04 | `[x]` | Create crate scaffold: `Cargo.toml` (deps: `soapysdr`, `num-complex`, `tokio`), empty `lib.rs` | `src/trx-server/trx-backend/trx-backend-soapysdr/` | SDR-01, SDR-02 |
|
||||||
|
| SDR-05 | `[x]` | Implement `demod.rs`: SSB (USB/LSB), AM envelope, FM quadrature, CW narrow BPF+envelope | `…/src/demod.rs` | SDR-04 |
|
||||||
|
| SDR-06 | `[x]` | Implement `dsp.rs`: IQ broadcast loop (SoapySDR read thread → `broadcast::Sender<Vec<Complex<f32>>>`); per-channel mixer → FIR LPF → decimator → demod → frame accumulator → `broadcast::Sender<Vec<f32>>` | `…/src/dsp.rs` | SDR-04, SDR-05 |
|
||||||
|
| SDR-07 | `[x]` | Implement `SoapySdrRig` in `lib.rs`: `RigCat` (RX methods + `not_supported` stubs for TX), `AudioSource`, gain control (manual/auto with fallback), primary channel freq/mode tracking | `…/src/lib.rs` | SDR-03, SDR-06 |
|
||||||
|
|
||||||
|
### Server integration
|
||||||
|
|
||||||
|
| ID | Status | Task | Touches | Needs |
|
||||||
|
|----|--------|------|---------|-------|
|
||||||
|
| SDR-08 | `[x]` | `main.rs`: after building rig, if `as_audio_source()` is `Some` skip cpal, subscribe each decoder and the Opus encoder to the appropriate channel PCM senders; validate `stream_opus` count ≤ 1 | `src/trx-server/src/main.rs` | SDR-03, SDR-07 |
|
||||||
|
| SDR-09 | `[x]` | Add `trx-backend-soapysdr` to workspace `Cargo.toml`; update `CONFIGURATION.md` with new `[sdr]` / `[[sdr.channels]]` options | `Cargo.toml`, `CONFIGURATION.md` | SDR-04 |
|
||||||
|
|
||||||
|
### Validation & tests
|
||||||
|
|
||||||
|
| ID | Status | Task | Touches | Needs |
|
||||||
|
|----|--------|------|---------|-------|
|
||||||
|
| SDR-10 | `[x]` | Unit tests for `demod.rs`: known-input tone through each demodulator, check output frequency correct | `…/src/demod.rs` | SDR-05 |
|
||||||
|
| SDR-11 | `[x]` | Unit tests for config validation: channel IF out-of-range, dual `stream_opus`, TX enabled with SDR backend, AGC fallback warning | `src/trx-server/src/config.rs` | SDR-03 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Receive-only backend that uses any SoapySDR-compatible device (RTL-SDR, Airspy, HackRF, SDRplay, etc.) as the rig
|
||||||
|
- Full IQ pipeline: raw IQ samples → demodulated PCM → existing decoders (FT8, WSPR, APRS, CW) with zero decoder-side changes
|
||||||
|
- Wideband capture: one SDR IQ stream feeds multiple simultaneous virtual receivers, each independently tuned and demodulated
|
||||||
|
- Configurable per-channel filters and demodulation modes
|
||||||
|
- Demodulated audio streamed to clients as Opus over the existing TCP audio channel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Transmit (TX/PTT) of any kind
|
||||||
|
- Replacing or deprecating the existing cpal-based audio path (it stays for transceiver backends)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Device Abstraction
|
||||||
|
|
||||||
|
### 1.1 `RigAccess` extension
|
||||||
|
|
||||||
|
A new access type `sdr` is added alongside `serial` and `tcp`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[rig.access]
|
||||||
|
type = "sdr"
|
||||||
|
args = "driver=rtlsdr" # SoapySDR device args string
|
||||||
|
```
|
||||||
|
|
||||||
|
The `args` value is passed verbatim to `SoapySDR::Device::new(args)`. It follows SoapySDR's key=value comma-separated convention (e.g., `driver=airspy`, `driver=rtlsdr,serial=00000001`).
|
||||||
|
|
||||||
|
### 1.2 `AudioSource` trait
|
||||||
|
|
||||||
|
A new trait is added to `trx-core` (`src/trx-core/src/rig/mod.rs`):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait AudioSource: Send + Sync {
|
||||||
|
/// Subscribe to demodulated PCM audio from the primary channel.
|
||||||
|
fn subscribe_pcm(&self) -> broadcast::Receiver<Vec<f32>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`RigCat` gains a default opt-in method:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait RigCat: Rig + Send {
|
||||||
|
// ... existing methods ...
|
||||||
|
fn as_audio_source(&self) -> Option<&dyn AudioSource> { None }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`SoapySdrRig` overrides `as_audio_source()` to return `Some(self)`. When the server detects this, it skips spawning the cpal capture thread entirely.
|
||||||
|
|
||||||
|
### 1.3 TX-only `RigCat` methods
|
||||||
|
|
||||||
|
The following methods return `RigError::not_supported(...)` on the SDR backend:
|
||||||
|
|
||||||
|
- `set_ptt()`
|
||||||
|
- `power_on()` / `power_off()`
|
||||||
|
- `get_tx_power()`
|
||||||
|
- `get_tx_limit()` / `set_tx_limit()`
|
||||||
|
- `toggle_vfo()` (not applicable; channels are defined statically in config)
|
||||||
|
- `lock()` / `unlock()`
|
||||||
|
|
||||||
|
The following methods are fully supported:
|
||||||
|
|
||||||
|
- `get_status()` → returns primary channel's current `(freq, mode, None)`
|
||||||
|
- `set_freq()` → re-tunes the SDR center frequency (keeping `center_offset_hz` invariant) and updates all channel mixer offsets
|
||||||
|
- `set_mode()` → changes the primary channel's demodulator
|
||||||
|
- `get_signal_strength()` → returns instantaneous RSSI for the primary channel (dBFS mapped to 0–255 S-unit range)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. IQ Pipeline Architecture
|
||||||
|
|
||||||
|
### 2.1 Center frequency offset
|
||||||
|
|
||||||
|
SDR hardware has a DC offset spur at exactly 0 Hz in the IQ spectrum. To keep the primary channel off DC, the SDR is tuned to a frequency offset from the desired dial frequency:
|
||||||
|
|
||||||
|
```
|
||||||
|
sdr_center_freq = dial_freq - center_offset_hz
|
||||||
|
```
|
||||||
|
|
||||||
|
With `center_offset_hz = 200000` and dial freq 14.074 MHz, the SDR tunes to 13.874 MHz. The 14.074 MHz signal appears at +200 kHz in the IQ spectrum and is mixed down to baseband in software.
|
||||||
|
|
||||||
|
`center_offset_hz` is a global SDR parameter (not per-channel). A reasonable default is `100000` (100 kHz).
|
||||||
|
|
||||||
|
### 2.2 Wideband channel model
|
||||||
|
|
||||||
|
One SoapySDR RX stream produces IQ samples at `sdr.sample_rate` (e.g. 1.92 MHz). This stream is shared among all configured channels. Each channel defines an independent virtual receiver:
|
||||||
|
|
||||||
|
```
|
||||||
|
SoapySDR RX stream (complex f32, sdr_sample_rate Hz)
|
||||||
|
│
|
||||||
|
├──► Channel 0 (primary) offset_hz=0, mode=USB, bw=3000 Hz
|
||||||
|
├──► Channel 1 (wspr) offset_hz=+21600, mode=USB, bw=3000 Hz
|
||||||
|
└──► Channel N ...
|
||||||
|
```
|
||||||
|
|
||||||
|
A **channel's frequency** in the real spectrum is:
|
||||||
|
|
||||||
|
```
|
||||||
|
channel_real_freq = dial_freq + channel.offset_hz
|
||||||
|
```
|
||||||
|
|
||||||
|
A **channel's IF frequency** within the IQ stream is:
|
||||||
|
|
||||||
|
```
|
||||||
|
channel_if_hz = center_offset_hz + channel.offset_hz
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the frequency at which the channel's signal appears in the captured IQ bandwidth, and is what the channel's mixer shifts to baseband.
|
||||||
|
|
||||||
|
**Constraint:** `|channel_if_hz|` must be less than `sdr_sample_rate / 2` for every channel. The server validates this at startup and rejects invalid configs.
|
||||||
|
|
||||||
|
### 2.3 Per-channel DSP chain
|
||||||
|
|
||||||
|
Each channel runs the following stages independently on the shared IQ stream:
|
||||||
|
|
||||||
|
```
|
||||||
|
IQ input (complex f32, sdr_sample_rate)
|
||||||
|
1. Mixer: multiply by exp(-j·2π·channel_if_hz·n/sdr_sample_rate)
|
||||||
|
→ complex f32 centred at 0 Hz
|
||||||
|
2. FIR LPF: cutoff = audio_bandwidth_hz / 2, order configurable
|
||||||
|
3. Decimator: sdr_sample_rate / audio_sample_rate (must be integer; resampler used otherwise)
|
||||||
|
4. Demodulator (mode-dependent, see §3)
|
||||||
|
5. Output: real f32 at audio_sample_rate
|
||||||
|
6. Frame accumulator: chunks of frame_duration_ms
|
||||||
|
7. broadcast::Sender<Vec<f32>> → decoders + optional Opus encoder
|
||||||
|
```
|
||||||
|
|
||||||
|
Channels run concurrently in separate tasks, all reading from the same raw IQ broadcast channel.
|
||||||
|
|
||||||
|
### 2.4 IQ broadcast channel
|
||||||
|
|
||||||
|
The SoapySDR read loop runs in a dedicated OS thread (matching the existing cpal thread model). It reads IQ sample blocks from the device and publishes them on:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
broadcast::Sender<Vec<Complex<f32>>> // capacity: configurable, default 64 blocks
|
||||||
|
```
|
||||||
|
|
||||||
|
Each channel task subscribes to this sender. Lagged receivers log a warning and continue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Demodulators
|
||||||
|
|
||||||
|
Demodulator is selected per-channel based on `mode`. Modes map as follows:
|
||||||
|
|
||||||
|
| `RigMode` | Demodulator |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `USB` | SSB: mix to IF, take real part (upper sideband) |
|
||||||
|
| `LSB` | SSB: mix to IF, take real part (lower sideband, negate IF) |
|
||||||
|
| `AM` | Envelope detector: `sqrt(I² + Q²)`, DC-remove, normalize |
|
||||||
|
| `FM` | Quadrature: `arg(s[n] · conj(s[n-1]))`, i.e. instantaneous frequency |
|
||||||
|
| `WFM` | Same as FM, wider pre-demod filter (`wfm_bandwidth_hz`) |
|
||||||
|
| `CW` | Narrow BPF centred at `cw_center_hz` (audio domain), then envelope |
|
||||||
|
| `DIG`/`PKT` | Same as USB (pass audio through for downstream digital decoders) |
|
||||||
|
| `CWR` | Same as CW (reversed sideband, uses same audio envelope) |
|
||||||
|
|
||||||
|
For SSB modes (USB/LSB), after mixing to baseband the channel's `audio_bandwidth_hz` defines the one-sided cutoff of the post-demod LPF.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Gain Control
|
||||||
|
|
||||||
|
Gain is configured globally under `[sdr.gain]`.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[sdr.gain]
|
||||||
|
mode = "auto" # "auto" (AGC via SoapySDR) or "manual"
|
||||||
|
value = 30.0 # dB; ignored when mode = "auto"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`auto`**: calls `device.set_gain_mode(SOAPY_SDR_RX, 0, true)` to enable hardware AGC if the device supports it. If the device does not support hardware AGC, falls back to `manual` with a warning.
|
||||||
|
- **`manual`**: calls `device.set_gain(SOAPY_SDR_RX, 0, value)` with the specified total gain in dB.
|
||||||
|
|
||||||
|
Advanced per-element gain is out of scope for this phase (no `lna`/`vga`/`if` sub-keys initially).
|
||||||
|
|
||||||
|
### 4.1 Virtual Squelch
|
||||||
|
|
||||||
|
Software squelch is configured globally under `[sdr.squelch]` and currently applies to the primary channel's demodulated audio path except WFM.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[sdr.squelch]
|
||||||
|
enabled = false
|
||||||
|
threshold_db = -65.0 # dBFS open threshold
|
||||||
|
hysteresis_db = 3.0 # dB close hysteresis
|
||||||
|
tail_ms = 180 # hold time after signal drops
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Filter Configuration
|
||||||
|
|
||||||
|
Filters are configured per-channel. The following are settable:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[sdr.channels]]
|
||||||
|
audio_bandwidth_hz = 3000 # One-sided bandwidth of post-demod BPF (Hz)
|
||||||
|
# For FM: deviation hint for deemphasis
|
||||||
|
fir_taps = 64 # FIR filter tap count (default 64); higher = sharper roll-off
|
||||||
|
cw_center_hz = 700 # CW tone centre in audio domain (default 700 Hz)
|
||||||
|
wfm_bandwidth_hz = 75000 # Pre-demod bandwidth for WFM only (default 75 kHz)
|
||||||
|
```
|
||||||
|
|
||||||
|
`fir_taps` controls the same FIR used in stage 2 of the DSP chain (§2.3). It applies uniformly to both the pre-demod decimation filter and the post-demod audio BPF in this phase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Channel Configuration and Decoder Binding
|
||||||
|
|
||||||
|
Channels are declared as a TOML array. The first channel in the list is the **primary channel** and is the one exposed via `RigCat` (`set_freq`/`set_mode` affect it; `get_status` reads from it).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[sdr.channels]]
|
||||||
|
id = "primary" # Identifier, used in logs
|
||||||
|
offset_hz = 0 # Offset from dial frequency (Hz)
|
||||||
|
mode = "auto" # "auto" = follows RigCat set_mode; or fixed RigMode string
|
||||||
|
audio_bandwidth_hz = 3000
|
||||||
|
fir_taps = 64
|
||||||
|
decoders = ["ft8", "cw"] # Which decoders receive this channel's PCM
|
||||||
|
stream_opus = true # Encode and stream via TCP audio channel
|
||||||
|
|
||||||
|
[[sdr.channels]]
|
||||||
|
id = "wspr-14"
|
||||||
|
offset_hz = 21600 # 14.0956 MHz when dial = 14.074 MHz
|
||||||
|
mode = "USB" # Fixed mode, ignores RigCat set_mode
|
||||||
|
audio_bandwidth_hz = 3000
|
||||||
|
decoders = ["wspr"]
|
||||||
|
stream_opus = false
|
||||||
|
|
||||||
|
[[sdr.channels]]
|
||||||
|
id = "aprs"
|
||||||
|
offset_hz = -673600 # e.g. 144.390 MHz when dial = 145.0635 MHz
|
||||||
|
mode = "FM"
|
||||||
|
audio_bandwidth_hz = 8000
|
||||||
|
decoders = ["aprs"]
|
||||||
|
stream_opus = false
|
||||||
|
```
|
||||||
|
|
||||||
|
**`mode = "auto"`** means the channel's demodulator tracks whatever `set_mode()` last set on the backend. Only the primary channel should use `"auto"` in typical use.
|
||||||
|
|
||||||
|
**`decoders`** maps to the decoder task IDs: `"ft8"`, `"wspr"`, `"aprs"`, `"cw"`. Each named decoder subscribes to the PCM broadcast channel of the listed channel(s). A decoder can only be bound to one channel (first binding wins if duplicated).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Opus Streaming
|
||||||
|
|
||||||
|
Channels with `stream_opus = true` have their demodulated PCM Opus-encoded and streamed over the server's existing TCP audio port (default 4531).
|
||||||
|
|
||||||
|
For this phase, only **one channel** may have `stream_opus = true` (validation error otherwise). This channel's Opus stream replaces what cpal would have produced — the TCP audio protocol and client-side handling are unchanged.
|
||||||
|
|
||||||
|
The Opus encoder uses the `[audio]` config for `frame_duration_ms`, `bitrate_bps`, and `sample_rate`. The SDR pipeline must output PCM at the same `sample_rate` as `[audio]`; a mismatch is a startup validation error.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Full Configuration Example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[rig]
|
||||||
|
model = "soapysdr"
|
||||||
|
initial_freq_hz = 14074000
|
||||||
|
initial_mode = "USB"
|
||||||
|
|
||||||
|
[rig.access]
|
||||||
|
type = "sdr"
|
||||||
|
args = "driver=rtlsdr"
|
||||||
|
|
||||||
|
[sdr]
|
||||||
|
sample_rate = 1920000 # IQ capture rate (Hz) — must be supported by device
|
||||||
|
bandwidth = 1500000 # Hardware IF filter (Hz)
|
||||||
|
center_offset_hz = 200000 # SDR tunes this many Hz below dial frequency
|
||||||
|
|
||||||
|
[sdr.gain]
|
||||||
|
mode = "auto"
|
||||||
|
value = 30.0 # Effective only when mode = "manual"
|
||||||
|
|
||||||
|
[sdr.squelch]
|
||||||
|
enabled = false
|
||||||
|
threshold_db = -65.0
|
||||||
|
hysteresis_db = 3.0
|
||||||
|
tail_ms = 180
|
||||||
|
|
||||||
|
[[sdr.channels]]
|
||||||
|
id = "primary"
|
||||||
|
offset_hz = 0
|
||||||
|
mode = "auto"
|
||||||
|
audio_bandwidth_hz = 3000
|
||||||
|
fir_taps = 64
|
||||||
|
decoders = ["ft8", "cw"]
|
||||||
|
stream_opus = true
|
||||||
|
|
||||||
|
[[sdr.channels]]
|
||||||
|
id = "wspr"
|
||||||
|
offset_hz = 21600
|
||||||
|
mode = "USB"
|
||||||
|
audio_bandwidth_hz = 3000
|
||||||
|
decoders = ["wspr"]
|
||||||
|
stream_opus = false
|
||||||
|
|
||||||
|
[audio]
|
||||||
|
enabled = true
|
||||||
|
listen = "127.0.0.1"
|
||||||
|
port = 4531
|
||||||
|
rx_enabled = true
|
||||||
|
tx_enabled = false # No TX on SDR backend
|
||||||
|
sample_rate = 48000
|
||||||
|
channels = 1
|
||||||
|
frame_duration_ms = 20
|
||||||
|
bitrate_bps = 24000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Code Changes Map
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `Cargo.toml` (workspace) | Add `src/trx-server/trx-backend/trx-backend-soapysdr` member |
|
||||||
|
| `src/trx-core/src/rig/mod.rs` | Add `AudioSource` trait; add `as_audio_source()` default to `RigCat` |
|
||||||
|
| `src/trx-server/trx-backend/src/lib.rs` | Add `RigAccess::Sdr { args }` variant; register `soapysdr` factory (feature-gated) |
|
||||||
|
| `src/trx-server/src/config.rs` | Add `SdrConfig`, `SdrGainConfig`, `SdrChannelConfig`; parse `type = "sdr"` in `AccessConfig`; add `sdr: SdrConfig` to `ServerConfig` |
|
||||||
|
| `src/trx-server/src/main.rs` | After building rig: if `as_audio_source()` is `Some`, skip cpal, use `AudioSource::subscribe_pcm()` for each decoder and for the Opus encoder; validate at most one `stream_opus = true` channel |
|
||||||
|
| `src/trx-server/src/audio.rs` | Expose `spawn_audio_capture` and `run_*_decoder` without assuming cpal as the sole source; no functional change needed — decoders already take `broadcast::Receiver<Vec<f32>>` |
|
||||||
|
| `src/trx-server/trx-backend/trx-backend-soapysdr/Cargo.toml` | New crate |
|
||||||
|
| `src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs` | `SoapySdrRig`: implements `RigCat` + `AudioSource`; spawns IQ read thread and channel DSP tasks |
|
||||||
|
| `src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs` | IQ broadcast loop; per-channel mixer, FIR, decimator, demodulator, frame accumulator |
|
||||||
|
| `src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs` | Mode-specific demodulators: SSB, AM envelope, FM quadrature, CW envelope |
|
||||||
|
| `CONFIGURATION.md` | Document new `[rig.access] type = "sdr"`, `[sdr]`, `[[sdr.channels]]` options |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. External Dependencies
|
||||||
|
|
||||||
|
| Crate | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `soapysdr` | Rust bindings to `libSoapySDR` (C++) |
|
||||||
|
| `num-complex` | `Complex<f32>` for IQ arithmetic |
|
||||||
|
|
||||||
|
System requirement: `libSoapySDR` installed (e.g. `brew install soapysdr` on macOS, `libsoapysdr-dev` on Debian/Ubuntu).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Validation Rules (startup)
|
||||||
|
|
||||||
|
- `[rig.access] type = "sdr"` requires `args` to be non-empty.
|
||||||
|
- `[sdr] sample_rate` must be non-zero.
|
||||||
|
- For every channel: `|center_offset_hz + channel.offset_hz| < sdr_sample_rate / 2`.
|
||||||
|
- Exactly one channel must have `stream_opus = true` (or none; zero means no TCP audio stream).
|
||||||
|
- The audio `sample_rate` in `[audio]` must equal the target audio rate in the SDR pipeline (no cross-rate mismatch).
|
||||||
|
- `[audio] tx_enabled` must be `false` when `model = "soapysdr"`.
|
||||||
|
- A decoder name may appear in at most one channel's `decoders` list.
|
||||||
|
- If the device does not support hardware AGC and `gain.mode = "auto"`, warn and fall back to `manual` using `gain.value`.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Project Skills
|
||||||
|
|
||||||
|
Custom slash commands (skills) available in this repository.
|
||||||
|
Invoke with `/skill-name [args]` inside Claude Code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `/frontend-design` — HTTP frontend work
|
||||||
|
|
||||||
|
**When to use:** Any time you need to add or modify UI in the HTTP web frontend — new control rows, panels, visual polish, capability-gated elements, or JS behaviour wired to REST endpoints.
|
||||||
|
|
||||||
|
**What it loads:** Design system context (palette, layout primitives, patterns), key file paths, and coding conventions so Claude writes code that matches the existing UI without needing to re-read the style guide each time.
|
||||||
|
|
||||||
|
**File:** `.claude/commands/frontend-design.md`
|
||||||
|
|
||||||
|
**Example invocations**
|
||||||
|
|
||||||
|
```
|
||||||
|
/frontend-design Add a CW keyer speed row (wpm slider) that POSTs to /set_cw_wpm, shown only when capabilities.tx is true.
|
||||||
|
/frontend-design Polish the filters panel — align the bandwidth label with the FIR taps label and add a unit suffix to the slider readout.
|
||||||
|
/frontend-design Add a waterfall canvas below the signal meter that renders frequency vs. time from a new SSE stream.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding new skills
|
||||||
|
|
||||||
|
Place a Markdown file in `.claude/commands/<skill-name>.md`.
|
||||||
|
Use `$ARGUMENTS` as the placeholder for user-supplied text.
|
||||||
|
Skills in `.claude/commands/` are project-scoped and not committed if `.claude/` is in `.gitignore`.
|
||||||
|
|
||||||
|
To make a skill part of the repo (shared with all contributors), add it to `aidocs/` as documentation and track the command file in version control by removing `.claude/` from `.gitignore` or adding a specific exception.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Global skills (available in all projects)
|
||||||
|
|
||||||
|
| Skill | When to use |
|
||||||
|
|-------|------------|
|
||||||
|
| `frontend-design` | Also installed globally; project version takes precedence here |
|
||||||
|
| `keybindings-help` | Customise Claude Code keyboard shortcuts |
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# UI Capability Gating
|
||||||
|
|
||||||
|
This document specifies how `trx-client`'s HTTP frontend adapts its controls to the capabilities of the connected rig backend. Devices such as SDR receivers expose filter controls but not TX controls; traditional transceivers are the reverse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
> **For AI agents:** This section is the single source of truth for implementation status.
|
||||||
|
> Each task has a unique ID (e.g. `UC-01`), a status badge, a description, the files it touches, and any blocking dependencies.
|
||||||
|
>
|
||||||
|
> Status legend: `[ ]` not started · `[~]` in progress · `[x]` done · `[!]` blocked
|
||||||
|
|
||||||
|
### Foundational (parallel)
|
||||||
|
|
||||||
|
| ID | Status | Task | Files | Needs |
|
||||||
|
|----|--------|------|-------|-------|
|
||||||
|
| UC-01 | `[x]` | Extend `RigCapabilities` with `tx`, `tx_limit`, `vfo_switch`, `filter_controls`, `signal_meter` bool flags | `src/trx-core/src/rig/state.rs` | — |
|
||||||
|
| UC-02 | `[x]` | Update capability declarations in all backends to set new flags | `src/trx-server/trx-backend/trx-backend-ft817/src/lib.rs`, `trx-backend-ft450d/src/lib.rs`, `trx-backend-soapysdr/src/lib.rs` | UC-01 |
|
||||||
|
| UC-03 | `[x]` | Add `RigFilterState` struct; add `filter: Option<RigFilterState>` to `RigSnapshot`; populate from SDR rig state | `src/trx-core/src/rig/state.rs`, `src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs` | — |
|
||||||
|
| UC-04 | `[x]` | Add `SetBandwidth`, `SetFirTaps` to `ClientCommand`; add mapping arms; update `rig_task.rs` to dispatch them | `src/trx-protocol/src/types.rs`, `mapping.rs`, `src/trx-server/src/rig_task.rs` | UC-03 |
|
||||||
|
|
||||||
|
### HTTP layer
|
||||||
|
|
||||||
|
| ID | Status | Task | Files | Needs |
|
||||||
|
|----|--------|------|-------|-------|
|
||||||
|
| UC-05 | `[x]` | Add `/set_bandwidth` and `/set_fir_taps` HTTP endpoints | `src/trx-client/trx-frontend/trx-frontend-http/src/api.rs` | UC-04 |
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
| ID | Status | Task | Files | Needs |
|
||||||
|
|----|--------|------|-------|-------|
|
||||||
|
| UC-06 | `[x]` | Read `state.info.capabilities` on each SSE event; toggle visibility of TX controls, meter rows, VFO button, lock button | `assets/web/app.js` | UC-01, UC-02 |
|
||||||
|
| UC-07 | `[x]` | Add "Filters" control panel (bandwidth, FIR taps, CW tone Hz); show only when `capabilities.filter_controls` | `assets/web/index.html`, `assets/web/app.js` | UC-05, UC-06 |
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
| ID | Status | Task | Files | Needs |
|
||||||
|
|----|--------|------|-------|-------|
|
||||||
|
| UC-08 | `[x]` | Unit tests: SDR backend declares `tx=false`, `filter_controls=true`; FT-817/450D declare `tx=true`, `filter_controls=false` | `src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs`, `trx-backend-ft817`, `trx-backend-ft450d` | UC-02 |
|
||||||
|
| UC-09 | `[x]` | Protocol round-trip test: `RigSnapshot` serialises `filter` field when `Some`, omits it when `None` | `src/trx-protocol/src/codec.rs` or `types.rs` | UC-03 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- All UI control groups are **shown/hidden purely from `RigCapabilities`** flags received in the initial `GET /status` and each SSE `status` event — no hard-coding per model name
|
||||||
|
- SDR backends show filter controls (bandwidth, FIR taps, CW tone); hide TX controls (PTT, power, TX limit, TX meters, TX audio)
|
||||||
|
- Transceiver backends show TX controls; hide filter controls
|
||||||
|
- Adding a new backend requires only setting the right capability flags — no frontend changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Per-channel filter control (multi-channel SDR tuning) — out of scope; only the primary channel is exposed here
|
||||||
|
- Dynamic capability changes at runtime (capability flags are set once at rig init and treated as static)
|
||||||
|
- Changing the rigctl or http-json frontends (HTTP frontend only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capability Flags
|
||||||
|
|
||||||
|
### New flags added to `RigCapabilities` (UC-01)
|
||||||
|
|
||||||
|
| Flag | Type | Meaning |
|
||||||
|
|------|------|---------|
|
||||||
|
| `tx` | `bool` | Backend supports transmit: PTT, power on/off, TX meters, TX audio |
|
||||||
|
| `tx_limit` | `bool` | Backend supports `get_tx_limit` / `set_tx_limit` |
|
||||||
|
| `vfo_switch` | `bool` | Backend supports `toggle_vfo` |
|
||||||
|
| `filter_controls` | `bool` | Backend supports runtime filter adjustment (bandwidth, FIR taps) |
|
||||||
|
| `signal_meter` | `bool` | Backend returns a meaningful RX signal strength value |
|
||||||
|
|
||||||
|
Existing flags `lock` and `lockable` are unchanged.
|
||||||
|
|
||||||
|
### Backend declarations (UC-02)
|
||||||
|
|
||||||
|
| Backend | `tx` | `tx_limit` | `vfo_switch` | `filter_controls` | `signal_meter` | `lock`/`lockable` |
|
||||||
|
|---------|------|-----------|--------------|-------------------|----------------|-------------------|
|
||||||
|
| FT-817 | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ / ✓ |
|
||||||
|
| FT-450D | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ / ✓ |
|
||||||
|
| SoapySDR | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ / ✗ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filter State
|
||||||
|
|
||||||
|
### `RigFilterState` struct (UC-03)
|
||||||
|
|
||||||
|
Added to `trx-core/src/rig/state.rs`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RigFilterState {
|
||||||
|
pub bandwidth_hz: u32, // Audio bandwidth of primary channel
|
||||||
|
pub fir_taps: u32, // FIR filter tap count
|
||||||
|
pub cw_center_hz: u32, // CW tone centre frequency (audio domain)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Added to `RigSnapshot`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub filter: Option<RigFilterState>,
|
||||||
|
```
|
||||||
|
|
||||||
|
The SDR backend populates this from the primary channel's live DSP state. All other backends leave it `None`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New Protocol Commands
|
||||||
|
|
||||||
|
### `ClientCommand` additions (UC-04)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
SetBandwidth { bandwidth_hz: u32 },
|
||||||
|
SetFirTaps { taps: u32 },
|
||||||
|
```
|
||||||
|
|
||||||
|
`SetCwToneHz` already exists and is reused.
|
||||||
|
|
||||||
|
### Mapping (UC-04)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
ClientCommand::SetBandwidth { bandwidth_hz } =>
|
||||||
|
RigCommand::SetBandwidth(bandwidth_hz),
|
||||||
|
ClientCommand::SetFirTaps { taps } =>
|
||||||
|
RigCommand::SetFirTaps(taps),
|
||||||
|
```
|
||||||
|
|
||||||
|
The SDR backend applies changes to the live DSP chain immediately. Other backends return `RigError::not_supported(...)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New HTTP Endpoints (UC-05)
|
||||||
|
|
||||||
|
| Endpoint | Method | Query param | Action |
|
||||||
|
|----------|--------|-------------|--------|
|
||||||
|
| `/set_bandwidth` | POST | `hz: u32` | Sets primary channel audio bandwidth |
|
||||||
|
| `/set_fir_taps` | POST | `taps: u32` | Sets primary channel FIR tap count |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Visibility Map (UC-06, UC-07)
|
||||||
|
|
||||||
|
| UI element / group | Shown when |
|
||||||
|
|--------------------|-----------|
|
||||||
|
| PTT button | `capabilities.tx` |
|
||||||
|
| Power button | `capabilities.tx` |
|
||||||
|
| TX meters (power bar, SWR bar) | `capabilities.tx && state.status.tx_en` |
|
||||||
|
| TX Limit row | `capabilities.tx_limit` |
|
||||||
|
| TX Audio toggle + volume | `capabilities.tx` |
|
||||||
|
| VFO selector buttons | `capabilities.vfo_switch` |
|
||||||
|
| Lock button | `capabilities.lock` |
|
||||||
|
| Signal meter | `capabilities.signal_meter` |
|
||||||
|
| Filters panel | `capabilities.filter_controls` |
|
||||||
|
|
||||||
|
Visibility is applied in a single `applyCapabilities(caps)` function called from the SSE `status` handler, using `element.classList.toggle('hidden', !condition)`.
|
||||||
|
|
||||||
|
### Filter panel layout (UC-07)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ Filters ──────────────────────────────────┐
|
||||||
|
│ Bandwidth [──────●──────] 3000 Hz │
|
||||||
|
│ FIR taps [32 ▾] (32 / 64 / 128 / 256) │
|
||||||
|
│ CW tone [──●───────────] 700 Hz │
|
||||||
|
└────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Each control dispatches to its REST endpoint on `change`/`input` (debounced 200 ms). The panel is hidden by default (`class="hidden"`) and revealed when `capabilities.filter_controls` is set.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- `applyCapabilities()` must run **before** the first paint (call it synchronously on the initial `/status` response, not only on SSE events) to avoid layout flash of unsupported controls.
|
||||||
|
- `hidden` CSS class should set `display: none` and `aria-hidden: true`.
|
||||||
|
- The existing `set_cw_tone` endpoint and CW decoder panel remain in the CW decoder tab — they are decoder settings, not filter settings. The Filters panel bandwidth/taps apply to the DSP chain; CW tone moves to both places or is de-duplicated in a follow-up.
|
||||||
|
- If a future backend supports TX but not `tx_limit`, only the TX Limit row is hidden; PTT remains.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Canvas2D to WebGL Transition Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
- Replace all runtime Canvas2D rendering in the frontend with WebGL.
|
||||||
|
- Remove Canvas2D code paths after feature parity is reached.
|
||||||
|
- Keep existing interaction behavior (zoom/pan/tune/BW drag/tooltips/overlays) intact.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- `src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js`
|
||||||
|
- `overview-canvas`
|
||||||
|
- `spectrum-canvas`
|
||||||
|
- `signal-overlay-canvas`
|
||||||
|
- `src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js`
|
||||||
|
- `cw-tone-waterfall`
|
||||||
|
- New shared WebGL utility module:
|
||||||
|
- `assets/web/webgl-renderer.js`
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
- No Canvas2D fallback path.
|
||||||
|
- No feature redesign outside rendering internals.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- Must preserve existing data flow and event wiring.
|
||||||
|
- Must keep map/decoder/bookmark integrations unchanged.
|
||||||
|
- Must remain dependency-free (no external rendering libraries).
|
||||||
|
|
||||||
|
## 2-Phase Migration
|
||||||
|
1. Phase 1 (Rendering engine insertion)
|
||||||
|
- Add shared WebGL renderer utility (primitives + textures + color parsing).
|
||||||
|
- Keep existing business logic and interaction handlers untouched.
|
||||||
|
- Swap draw targets from 2D contexts to WebGL primitives.
|
||||||
|
|
||||||
|
2. Phase 2 (Canvas2D removal and parity closure)
|
||||||
|
- Remove `getContext("2d")` usage from app and plugins.
|
||||||
|
- Remove obsolete 2D-specific cache paths.
|
||||||
|
- Validate behavior on resize/theme/style/stream reconnect/decoder mode changes.
|
||||||
|
|
||||||
|
## Parallel Workstreams ("Agents")
|
||||||
|
1. Agent A: Shared WebGL core
|
||||||
|
- Build `webgl-renderer.js` with:
|
||||||
|
- HiDPI resize handling
|
||||||
|
- Solid/gradient rects
|
||||||
|
- Polyline/segment/fill primitives
|
||||||
|
- RGBA texture upload + blit
|
||||||
|
- CSS color parser helpers
|
||||||
|
|
||||||
|
2. Agent B: Main spectrum/overview migration
|
||||||
|
- Port `drawSpectrum`, `drawHeaderSignalGraph`, `drawSignalOverlay`, and clear paths.
|
||||||
|
- Replace 2D offscreen waterfall cache with WebGL texture updates.
|
||||||
|
- Keep frequency axis/bookmark axis DOM behavior unchanged.
|
||||||
|
|
||||||
|
3. Agent C: CW tone picker migration
|
||||||
|
- Port `drawCwTonePicker` primitives to WebGL.
|
||||||
|
- Preserve auto/manual tone interactions and mode gating.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- No frontend `getContext("2d")` usage remains.
|
||||||
|
- All four canvases render using WebGL and respond to resize/DPR changes.
|
||||||
|
- Spectrum interactions still work:
|
||||||
|
- wheel zoom
|
||||||
|
- drag pan
|
||||||
|
- BW edge drag
|
||||||
|
- click tune
|
||||||
|
- Overview strip continues showing waterfall/history.
|
||||||
|
- CW tone picker remains interactive and reflects current spectrum/tone.
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
- Static:
|
||||||
|
- `rg -n 'getContext\\("2d"\\)' src/trx-client/trx-frontend/trx-frontend-http/assets/web`
|
||||||
|
- Runtime smoke:
|
||||||
|
- Open main tab: verify overview + spectrum + overlay.
|
||||||
|
- Toggle theme/style.
|
||||||
|
- Resize window and spectrum grip.
|
||||||
|
- Enable CW decoder and validate tone picker updates/click-to-set.
|
||||||
|
- Confirm no rendering exceptions in browser console.
|
||||||
|
|
||||||
|
## Rollout Notes
|
||||||
|
- Initial rollout is WebGL-only.
|
||||||
|
- If a browser lacks WebGL, canvases remain blank by design until a dedicated fallback policy is defined.
|
||||||
Submodule
+1
Submodule docs added at c98dc5ab75
@@ -1,14 +0,0 @@
|
|||||||
# trx-rs
|
|
||||||
|
|
||||||
`trx-rs` is a modular amateur radio control stack written in Rust. It splits
|
|
||||||
hardware access, DSP, transport, and user-facing interfaces into separate
|
|
||||||
components so a radio or SDR can be controlled locally while audio, decoding,
|
|
||||||
and remote control are exposed elsewhere on the network.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- [User Manual](User-Manual) — configuration, features, and usage
|
|
||||||
- [Architecture](Architecture) — system design, crate layout, data flow, and internals
|
|
||||||
- [Optimization Guidelines](Optimization-Guidelines) — performance guidelines for the real-time DSP pipeline
|
|
||||||
- [Planned Features](Planned-Features) — planned features and design notes
|
|
||||||
- [Improvement Areas](Improvement-Areas) — codebase audit: quality, architecture, security, performance, and improvement plan
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
# Improvement Areas
|
|
||||||
|
|
||||||
A comprehensive audit of the trx-rs codebase covering code quality, architecture,
|
|
||||||
security, testing, and performance. Each item includes the affected location and
|
|
||||||
a suggested fix.
|
|
||||||
|
|
||||||
*Last updated: 2026-03-29*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Resolved Items
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Click to expand resolved items from previous audits</summary>
|
|
||||||
|
|
||||||
### Plugin signing and cross-platform validation — DROPPED
|
|
||||||
|
|
||||||
Plugin system has been removed from the codebase. No longer applicable.
|
|
||||||
|
|
||||||
### Session store mutex poisoning (auth.rs) — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs`
|
|
||||||
|
|
||||||
All 6 `.write().unwrap()` / `.lock().unwrap()` calls replaced with
|
|
||||||
`.unwrap_or_else(|e| { warn!(...); e.into_inner() })` pattern. Lock poisoning now
|
|
||||||
logs a warning and recovers the inner data instead of crashing.
|
|
||||||
|
|
||||||
### No rate limiting on TCP listener — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-server/src/listener.rs`
|
|
||||||
|
|
||||||
Added `ConnectionTracker` with per-IP connection limiting (default: 10 concurrent
|
|
||||||
connections per IP). Connections exceeding the limit are rejected with a log warning.
|
|
||||||
Slots are released when clients disconnect.
|
|
||||||
|
|
||||||
### RigState is a 33-field flat struct — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-core/src/rig/state.rs`
|
|
||||||
|
|
||||||
Decoder fields grouped into `DecoderConfig` (8 bools) and `DecoderResetSeqs`
|
|
||||||
(8 u64 counters). Both use `#[serde(flatten)]` for backward-compatible JSON wire
|
|
||||||
format. Updated across all consumers.
|
|
||||||
|
|
||||||
### No `spawn_blocking` timeout — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-server/src/listener.rs`
|
|
||||||
|
|
||||||
Satellite pass computation wrapped in `tokio::time::timeout(30s, ...)` with
|
|
||||||
graceful fallback to empty results on timeout or panic.
|
|
||||||
|
|
||||||
### Command handler boilerplate — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-core/src/rig/controller/handlers.rs`
|
|
||||||
|
|
||||||
Created `rig_command!` declarative macro. 7 unit commands use the macro; 4 commands
|
|
||||||
with custom fields/validation remain as explicit impls.
|
|
||||||
|
|
||||||
### No command execution timeouts at CommandExecutor level — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-server/src/rig_task.rs`
|
|
||||||
|
|
||||||
`tokio::time::timeout(command_exec_timeout, process_command(...))` wraps all
|
|
||||||
command execution. Default timeout: 10s, configurable via `RigTaskConfig`.
|
|
||||||
|
|
||||||
### No forward compatibility in protocol — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-protocol/src/types.rs`, `src/trx-protocol/src/codec.rs`
|
|
||||||
|
|
||||||
Added optional `protocol_version: Option<u32>` to `ClientEnvelope` and
|
|
||||||
`ClientResponse`. `parse_envelope()` distinguishes malformed JSON from
|
|
||||||
unrecognised `cmd` values.
|
|
||||||
|
|
||||||
### `unsafe` string construction in spectrum encoding — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-client/trx-frontend/trx-frontend-http/src/api.rs`
|
|
||||||
|
|
||||||
Replaced `unsafe { String::from_utf8_unchecked(out) }` with safe
|
|
||||||
`String::from_utf8(out).expect(...)`.
|
|
||||||
|
|
||||||
### `#[allow(dead_code)]` cleanup — RESOLVED
|
|
||||||
|
|
||||||
Reduced from 6 to 4 annotations, all in trx-backend-soapysdr where fields serve
|
|
||||||
as lifetime anchors (`device`, `iq_tx`) or document reserved capacity
|
|
||||||
(`fixed_slot_count`, `process_pair`).
|
|
||||||
|
|
||||||
### VDES decoder incomplete FEC — RESOLVED
|
|
||||||
|
|
||||||
Turbo FEC decoder, CRC-16-CCITT validation, and M.2092-1 link-layer frame parsing
|
|
||||||
implemented.
|
|
||||||
|
|
||||||
### Plugin system lacks versioning — DROPPED
|
|
||||||
|
|
||||||
Plugin system removed from the codebase.
|
|
||||||
|
|
||||||
### Configurator serial detection stubbed — RESOLVED
|
|
||||||
|
|
||||||
Implemented using `tokio_serial::available_ports()` with USB, Bluetooth, PCI, and
|
|
||||||
Unknown port type descriptions.
|
|
||||||
|
|
||||||
### Inconsistent frequency/rig naming — DOCUMENTED AS INTENTIONAL
|
|
||||||
|
|
||||||
Field names reflect distinct semantic contexts: `freq_hz` (dial), `center_hz`
|
|
||||||
(SDR capture center), `cw_center_hz` (CW tone); `rig_id` (config key), `id`
|
|
||||||
(runtime UUID); `model` (hardware string), `rig_model` (config parameter).
|
|
||||||
|
|
||||||
### Decoder task duplication in audio.rs — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-server/src/audio.rs`
|
|
||||||
|
|
||||||
APRS and HF APRS decoders merged into a single parameterised
|
|
||||||
`run_aprs_decoder_inner()` function. FT8 and FT4 decoders merged into
|
|
||||||
`run_ftx_decoder_inner()`. All decoder tasks now include `tracing::info_span!`
|
|
||||||
around `block_in_place()` calls for opt-in latency measurement.
|
|
||||||
|
|
||||||
### Missing tests for critical modules — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-server/src/listener.rs`, `src/trx-client/trx-frontend/trx-frontend-http/`
|
|
||||||
|
|
||||||
Added multi-rig state isolation and command routing tests in `listener.rs`.
|
|
||||||
Added background decode `evaluate_bookmark` pure-function tests.
|
|
||||||
|
|
||||||
### Missing integration tests for multi-rig scenarios — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-server/src/listener.rs`
|
|
||||||
|
|
||||||
Added integration tests covering simultaneous state management across two rigs
|
|
||||||
with a dummy backend, verifying state isolation and command routing.
|
|
||||||
|
|
||||||
### Decode log silent failures — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/decoders/trx-decode-log/src/lib.rs`
|
|
||||||
|
|
||||||
`flush()` errors are now logged via `warn!`. On file rotation failure, the old
|
|
||||||
writer is kept rather than silently dropping writes; a degradation warning is
|
|
||||||
emitted.
|
|
||||||
|
|
||||||
### `api.rs` file size and organization — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-client/trx-frontend/trx-frontend-http/src/api/`
|
|
||||||
|
|
||||||
Split 2,831-LOC monolith into 7 logically grouped modules: `mod.rs` (shared
|
|
||||||
types and route configuration), `decoder.rs`, `rig.rs`, `vchan.rs`, `sse.rs`,
|
|
||||||
`bookmarks.rs`, `assets.rs`.
|
|
||||||
|
|
||||||
### Background decode state complexity — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-client/trx-frontend/trx-frontend-http/src/background_decode.rs`
|
|
||||||
|
|
||||||
Extracted the 8-guard decision cascade into a pure `evaluate_bookmark()` function
|
|
||||||
returning `ChannelAction` enum (`Active` or `Skip { reason }`). Added unit tests
|
|
||||||
for all decision paths.
|
|
||||||
|
|
||||||
### Actix-web pinned to exact version — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-client/trx-frontend/trx-frontend-http/Cargo.toml`
|
|
||||||
|
|
||||||
Relaxed from `actix-web = "=4.4.1"` to `actix-web = "4.4"` to allow patch-level
|
|
||||||
security updates.
|
|
||||||
|
|
||||||
### Magic numbers in VDES plausibility scoring — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/decoders/trx-vdes/src/lib.rs`
|
|
||||||
|
|
||||||
Inline magic numbers replaced with documented named constants:
|
|
||||||
`PLAUSIBILITY_UNSYNCED_THRESHOLD` (−35) and
|
|
||||||
`PLAUSIBILITY_LOW_CONFIDENCE_THRESHOLD` (15).
|
|
||||||
|
|
||||||
### FT-817 VFO inference fragile with same frequency — DOCUMENTED
|
|
||||||
|
|
||||||
**Location:** `src/trx-server/trx-backend/trx-backend-ft817/src/lib.rs`
|
|
||||||
|
|
||||||
When both VFOs share the same frequency, inference defaults to VFO A. Resolved
|
|
||||||
after VFO toggle primes both sides. Well-documented in code comments; remains
|
|
||||||
a known limitation.
|
|
||||||
|
|
||||||
### Excessive string cloning in remote client — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-client/src/remote_client.rs`
|
|
||||||
|
|
||||||
Hot-path spectrum polling loop now caches the token to avoid per-poll cloning.
|
|
||||||
State update path restructured to send to the main watch channel last (taking
|
|
||||||
ownership) and avoid one redundant `RigState::clone()`.
|
|
||||||
|
|
||||||
### Missing doc comments on public decoder structs — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/decoders/trx-ais/src/lib.rs`, `src/decoders/trx-vdes/src/lib.rs`,
|
|
||||||
`src/decoders/trx-rds/src/lib.rs`
|
|
||||||
|
|
||||||
Added comprehensive doc comments to `AisDecoder`, `VdesDecoder`, and `RdsDecoder`
|
|
||||||
describing valid sample rates, usage examples, and reset semantics.
|
|
||||||
|
|
||||||
### Turbo decoder precondition not asserted — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/decoders/trx-vdes/src/turbo.rs`
|
|
||||||
|
|
||||||
Added `debug_assert_eq!` on interleaver and deinterleaver lengths in
|
|
||||||
`turbo_decode_soft()`.
|
|
||||||
|
|
||||||
### No tracing spans for decoder performance — RESOLVED
|
|
||||||
|
|
||||||
**Location:** `src/trx-server/src/audio.rs`
|
|
||||||
|
|
||||||
Added `tracing::info_span!` around `block_in_place()` calls in all 10 decoder
|
|
||||||
tasks (APRS, HF APRS, AIS A/B, VDES, CW, FT8, FT4, FT2, WSPR, LRPT) for
|
|
||||||
opt-in per-decoder latency measurement.
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
All previous improvement items have been resolved. No outstanding issues.
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# RDS Parameter Tuning Notes
|
|
||||||
|
|
||||||
*Decoder tuning rationale for `trx-rds`. Recorded 2026-03-27; reflects the
|
|
||||||
shipped parameter set. Kept as a reference for why these constants were chosen —
|
|
||||||
not an open work item.*
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
Maximum sensitivity (weak-signal decode) with zero false positive PI decodes.
|
|
||||||
|
|
||||||
## Changes Applied
|
|
||||||
|
|
||||||
### `src/decoders/trx-rds/src/lib.rs`
|
|
||||||
|
|
||||||
#### Constants tuned
|
|
||||||
- `RRC_ALPHA = 0.50` (was 0.75) — narrower noise bandwidth, ~0.6 dB SNR gain
|
|
||||||
- `COSTAS_KI = 3.5e-7` — loop damping ζ≈0.68, well-damped (1e-6 caused instability)
|
|
||||||
- `PI_ACC_THRESHOLD = 3` (was 2) — accumulate 3 Block A observations before committing PI
|
|
||||||
- `OSD_MAX_FLIP_COST = 0.45` — Tech 9: reject OSD corrections where flipped bits had
|
|
||||||
high confidence (genuine errors have cost ≲ 0.3; noise matches cost 0.6–1.2)
|
|
||||||
|
|
||||||
#### Soft confidence fix
|
|
||||||
In `Candidate::process_sample`, the soft confidence passed to `push_bit_soft` is now
|
|
||||||
`biphase_i.abs()` (was full vector magnitude). This aligns confidence with the bit
|
|
||||||
decision sign and prevents OSD(2) from false-decoding noise when the Costas loop
|
|
||||||
has residual phase error.
|
|
||||||
|
|
||||||
#### OSD(2) in locked mode (kept)
|
|
||||||
`decode_block_soft` performs OSD(2): hard decode → all 26 single-bit flips → all
|
|
||||||
325 two-bit flip pairs. Only active in locked mode; sequential B→C→D block-type
|
|
||||||
gating limits false positives.
|
|
||||||
|
|
||||||
#### Search mode: hard decode only
|
|
||||||
Removed OSD(1) from Block A acquisition (search mode). With OSD(1), ~13% of
|
|
||||||
random 26-bit words would falsely pass the Block A test per bit, allowing wrong
|
|
||||||
clock-phase candidates to accumulate false groups as fast as the correct candidate
|
|
||||||
accumulates real ones. Hard decode reduces the false Block A rate to ~0.5%.
|
|
||||||
|
|
||||||
#### Tech 9: OSD cost ceiling
|
|
||||||
`decode_block_soft` now enforces `OSD_MAX_FLIP_COST = 0.45` — the sum of soft
|
|
||||||
confidences for all flipped bits must not exceed this threshold. At 9–10 dB SNR,
|
|
||||||
genuine bit errors have very low `|biphase_I|` (cost ≲ 0.3), while noise-induced
|
|
||||||
OSD matches flip high-confidence bits (cost 0.6–1.2). This eliminates most
|
|
||||||
spurious OSD(2) matches without affecting real weak-signal corrections.
|
|
||||||
|
|
||||||
#### Tech 10: PI consistency gate
|
|
||||||
`process_group` rejects groups whose Block A PI differs from the candidate's
|
|
||||||
established PI. This prevents a single false OSD decode from polluting accumulated
|
|
||||||
text fields (PS, RT, PTYN) with garbage from noise or interference.
|
|
||||||
|
|
||||||
#### Candidate selection: incumbent tracking
|
|
||||||
Added `best_candidate_idx: Option<usize>` to `RdsDecoder`. The incumbent (winning)
|
|
||||||
candidate can always update `best_state` at equal score (its `ps_seen`/`rt_seen`
|
|
||||||
arrays accumulate coherently). A challenger must achieve a strictly higher score to
|
|
||||||
take over. The incumbent's `best_score` is also updated when it returns `None`
|
|
||||||
(no state change) so challengers cannot leapfrog with a single false group.
|
|
||||||
|
|
||||||
#### Test fixes
|
|
||||||
- `blocks_to_chips`: added NRZI (NRZ-Mark) pre-encoding. The differential biphase
|
|
||||||
decoder computes `bit = input_bit XOR prev_input_bit`; without NRZI the recovered
|
|
||||||
bits were XOR-of-consecutive-bits, not the original data.
|
|
||||||
- `decode_block_soft_rejects_three_bit_error`: removed (OSD(2) legitimately finds
|
|
||||||
distance-2 codewords; `pure_noise_produces_zero_pi_decodes` is the real guard).
|
|
||||||
- New test: `blocks_to_chips_round_trips_all_groups` — verifies round-trip decode
|
|
||||||
of all 16 blocks across all 4 PS segments without BPSK modulation.
|
|
||||||
|
|
||||||
### `src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/wfm.rs`
|
|
||||||
|
|
||||||
- `PILOT_LOCK_THRESHOLD = 0.20` (was 0.25) — pilot reference enabled at lower coherence
|
|
||||||
- Added `PILOT_LOCK_ONSET = 0.30` constant (was hardcoded 0.4)
|
|
||||||
- `pilot_lock` ramp: `((pilot_coherence - PILOT_LOCK_ONSET) / 0.2).clamp(0.0, 1.0)`
|
|
||||||
— pilot reference engages at coherence ≥ 0.36 instead of ≥ 0.45
|
|
||||||
|
|
||||||
## Test Status
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo test -p trx-rds
|
|
||||||
```
|
|
||||||
|
|
||||||
16/16 passing:
|
|
||||||
- ✅ decode_block_recognizes_valid_offsets
|
|
||||||
- ✅ decode_block_soft_corrects_single_bit_error
|
|
||||||
- ✅ decode_block_soft_corrects_two_bit_error_osd2
|
|
||||||
- ✅ block_decode_rate_osd1_vs_osd2
|
|
||||||
- ✅ decode_block_soft_prefers_least_costly_flip
|
|
||||||
- ✅ full_group_with_two_bit_errors_in_each_locked_block
|
|
||||||
- ✅ pi_accumulation_corrects_weak_pi_after_threshold
|
|
||||||
- ✅ decoder_emits_ps_and_pty_from_group_0a
|
|
||||||
- ✅ rrc_tap_dc_gain
|
|
||||||
- ✅ pure_noise_produces_zero_pi_decodes (2 seconds of noise, zero false PI)
|
|
||||||
- ✅ end_to_end_with_pilot_reference_decodes_pi
|
|
||||||
- ✅ end_to_end_noisy_signal_snr_10db_decodes_pi
|
|
||||||
- ✅ end_to_end_noisy_signal_snr_9db_decodes_pi ← new, 9 dB threshold
|
|
||||||
- ✅ costas_tracks_without_diverging_on_clean_signal
|
|
||||||
- ✅ blocks_to_chips_round_trips_all_groups
|
|
||||||
- ✅ end_to_end_clean_signal_decodes_ps
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
# Settings Menu — UI/UX Analysis & Improvement Plan
|
|
||||||
|
|
||||||
*Authored: 2026-03-30*
|
|
||||||
|
|
||||||
## 1. Current Structure
|
|
||||||
|
|
||||||
The Settings tab (`#tab-settings`) contains four sub-tabs:
|
|
||||||
|
|
||||||
| Sub-tab | Purpose | Complexity |
|
|
||||||
|---|---|---|
|
|
||||||
| **Scheduler** | Grayline / Time Span / Satellite scheduling | High — nested modes, forms, timeline |
|
|
||||||
| **Background Decode** | Hidden background decoder channels | Medium — toggle + bookmark checklist |
|
|
||||||
| **Bandplan** | IARU region overlay on spectrum | Low — dropdown + checkbox |
|
|
||||||
| **History** | Clear server-side decode history | Low — 10 clear buttons |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Identified Issues
|
|
||||||
|
|
||||||
### 2.1 Information Architecture
|
|
||||||
|
|
||||||
| # | Issue | Severity |
|
|
||||||
|---|---|---|
|
|
||||||
| IA-1 | **"Settings" is a catch-all bucket.** Scheduler and Background Decode are operational features, not user preferences. Bandplan and History are true settings/maintenance. Mixing them under one tab creates cognitive overhead. | Medium |
|
|
||||||
| IA-2 | **Scheduler sub-tab is overloaded.** It packs three conceptually distinct features (Grayline, Time Span, Satellite) into one scrollable panel via conditional `display:none` sections. Users must scroll past irrelevant sections. | Medium |
|
|
||||||
| IA-3 | **History clearing is buried.** Users wanting to clear FT8 decode history must navigate to Settings → History — an unintuitive path. This action is more naturally accessible from the Digital Modes tab itself. | Low |
|
|
||||||
| IA-4 | **No search or categorization.** With 4 sub-tabs today, it's manageable, but the flat sub-tab bar won't scale if more settings (e.g., audio, display theme, reporting/PSKReporter, notifications) are added. | Low |
|
|
||||||
|
|
||||||
### 2.2 Interaction Design
|
|
||||||
|
|
||||||
| # | Issue | Severity |
|
|
||||||
|---|---|---|
|
|
||||||
| IX-1 | **Save button visibility is inconsistent.** Save/Reset buttons use `style="display:none"` and are shown dynamically, but there is no dirty-state indicator. Users can change fields without realizing they haven't saved. | High |
|
|
||||||
| IX-2 | **No confirmation on destructive actions.** The 10 history-clear buttons and "Reset to Disabled" (scheduler) fire immediately on click. No confirmation dialog protects against accidental data loss. | High |
|
|
||||||
| IX-3 | **Entry table details collapsed by default.** The Time Span entry table is inside a `<details>` element — users must expand it to see, edit, or delete entries. This adds an unnecessary click when entries already exist. | Medium |
|
|
||||||
| IX-4 | **Satellite form uses a modal overlay; Time Span form is inline.** Inconsistent form presentation within the same sub-tab. Both should use the same pattern. | Medium |
|
|
||||||
| IX-5 | **Toast notification positioning.** The `.sch-toast` uses `position: fixed; bottom: 1.5rem` which can overlap with the main tab bar or mobile navigation. It also disappears without user control. | Low |
|
|
||||||
| IX-6 | **Bookmark filter in Background Decode has no "select all / deselect all" shortcut.** With many bookmarks, toggling them one by one is tedious. | Medium |
|
|
||||||
|
|
||||||
### 2.3 Visual & Layout
|
|
||||||
|
|
||||||
| # | Issue | Severity |
|
|
||||||
|---|---|---|
|
|
||||||
| VL-1 | **Scheduler has no visual state summary.** The "No activity yet." card doesn't show whether the scheduler is enabled or what mode it's in at a glance. Users must inspect the mode dropdown. | Medium |
|
|
||||||
| VL-2 | **History clear buttons are uniform.** All 10 buttons look identical (`sch-write sch-reset-btn`). No indication of which decoders have data to clear. Buttons for empty histories are noise. | Low |
|
|
||||||
| VL-3 | **Mobile responsiveness is partial.** The `@media (max-width: 600px)` rules handle `.sch-row` and `.bgd-*` layout, but the Time Span table (`.sch-ts-table` with 8 columns) overflows on narrow screens. | Medium |
|
|
||||||
| VL-4 | **Sub-tab bar can overflow.** It uses `overflow-x: auto` but gives no visual scroll indicator. On small screens, the "History" tab can be hidden off-screen with no affordance. | Low |
|
|
||||||
|
|
||||||
### 2.4 Accessibility
|
|
||||||
|
|
||||||
| # | Issue | Severity |
|
|
||||||
|---|---|---|
|
|
||||||
| A-1 | **Missing `aria-label` on several controls.** The scheduler mode select has one, but the grayline lat/lon inputs, interleave fields, and satellite fields lack accessible names beyond their visible label text (which is acceptable for `<label>` wrapping `<input>`, but form titles like "Add Entry" aren't linked to the form via `aria-labelledby`). | Low |
|
|
||||||
| A-2 | **No keyboard navigation for the 24h timeline SVG.** Timeline segments are clickable (`cursor: pointer`) but not focusable or keyboard-operable. | Medium |
|
|
||||||
| A-3 | **Color-only state indication in Background Decode status.** States like "active" (green), "waiting" (yellow), "error" (red) rely solely on color. Not sufficient for color-blind users. | Medium |
|
|
||||||
| A-4 | **Toast notifications aren't announced to screen readers.** The `.sch-toast` div lacks `role="alert"` or `aria-live` attributes. | Low |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Improvement Plan
|
|
||||||
|
|
||||||
### Phase 1 — Quick Wins (Low effort, high impact)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
gantt
|
|
||||||
title Phase 1 — Quick Wins
|
|
||||||
dateFormat X
|
|
||||||
axisFormat %s
|
|
||||||
section Interaction
|
|
||||||
IX-2 Add confirmation dialogs :a1, 0, 2
|
|
||||||
IX-6 Select all / deselect all :a2, 0, 1
|
|
||||||
IX-1 Dirty-state indicator on Save :a3, 0, 2
|
|
||||||
section Accessibility
|
|
||||||
A-4 Add aria-live to toasts :a4, 0, 1
|
|
||||||
A-3 Add text labels to state dots :a5, 0, 1
|
|
||||||
```
|
|
||||||
|
|
||||||
**IX-2: Add confirmation dialogs for destructive actions**
|
|
||||||
- Wrap history-clear and "Reset to Disabled" clicks in a `confirm()` dialog (or a lightweight inline confirmation pattern).
|
|
||||||
- Estimated: ~30 lines of JS.
|
|
||||||
|
|
||||||
**IX-6: Add select all / deselect all for Background Decode bookmarks**
|
|
||||||
- Add two small buttons above the bookmark checklist: "Select All" / "Deselect All".
|
|
||||||
- Alternatively, a single toggle that reads the current state.
|
|
||||||
|
|
||||||
**IX-1: Dirty-state indicator**
|
|
||||||
- Track whether any field has changed since last load/save.
|
|
||||||
- Show a visual cue (e.g., dot on the Save button, or change button color) when there are unsaved changes.
|
|
||||||
- Optionally warn on tab navigation away from dirty settings.
|
|
||||||
|
|
||||||
**A-4: Toast accessibility**
|
|
||||||
- Add `role="alert"` and `aria-live="polite"` to `.sch-toast` elements.
|
|
||||||
|
|
||||||
**A-3: State badge text labels**
|
|
||||||
- The `.bgd-status-state` already shows uppercase text — ensure the SVG dot badges (`.bgd-state-dot`) are supplemented with visible text, not just color.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 2 — Structural Improvements (Medium effort)
|
|
||||||
|
|
||||||
**IA-1 + IA-3: Reorganize the Settings tab**
|
|
||||||
|
|
||||||
Proposed new sub-tab structure:
|
|
||||||
|
|
||||||
| Sub-tab | Contents |
|
|
||||||
|---|---|
|
|
||||||
| **Scheduler** | Grayline, Time Span, Satellite (unchanged) |
|
|
||||||
| **Background Decode** | Background decode config (unchanged) |
|
|
||||||
| **Display** | Bandplan region/labels, future: theme, font size, spectrum colors |
|
|
||||||
| **Maintenance** | History clearing, with per-decoder item counts |
|
|
||||||
|
|
||||||
Additionally, add contextual "Clear history" links directly in the Digital Modes tab (next to each decoder's output panel), so users don't need to navigate to Settings at all for this common action.
|
|
||||||
|
|
||||||
**IX-3: Auto-expand entry table when entries exist**
|
|
||||||
- If `scheduler-ts-tbody` has rows, set the `<details>` element's `open` attribute on render.
|
|
||||||
|
|
||||||
**IX-4: Unify form presentation**
|
|
||||||
- Convert the satellite modal (`#sch-sat-form-wrap` with `position: fixed`) to an inline form matching the Time Span entry form pattern, or vice versa. Inline is preferred for consistency and mobile friendliness.
|
|
||||||
|
|
||||||
**VL-1: Scheduler status summary card**
|
|
||||||
- Enhance the "Now Playing" card to always show: current mode, active entry (if any), next scheduled event, and satellite pass countdown (if enabled).
|
|
||||||
- Use a compact two-line format when idle: "Mode: Grayline | Next: Dawn transition in 2h 14m".
|
|
||||||
|
|
||||||
**VL-3: Responsive table for Time Span entries**
|
|
||||||
- Replace the 8-column table with a card-based layout on narrow screens (`@media (max-width: 600px)`), or use horizontal scroll with a scroll shadow indicator.
|
|
||||||
|
|
||||||
**A-2: Keyboard-accessible timeline**
|
|
||||||
- Add `tabindex="0"` and `role="button"` to timeline segments.
|
|
||||||
- Handle `keydown` for Enter/Space to activate.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 3 — Polish & Scalability (Higher effort)
|
|
||||||
|
|
||||||
**VL-2: Smart history-clear buttons**
|
|
||||||
- Query each decoder's item count via API (or piggyback on existing SSE state).
|
|
||||||
- Show count badges on each button (e.g., "Clear FT8 history (142)").
|
|
||||||
- Disable or hide buttons for decoders with no history.
|
|
||||||
- Add a "Clear All" button with appropriate confirmation.
|
|
||||||
|
|
||||||
**IA-4: Settings search (future-proofing)**
|
|
||||||
- If the settings surface grows beyond 5-6 sub-tabs, add a search/filter input at the top of the Settings tab that highlights matching sections.
|
|
||||||
- Not needed today, but the sub-tab architecture should be designed to accommodate it.
|
|
||||||
|
|
||||||
**VL-4: Sub-tab scroll indicators**
|
|
||||||
- Add CSS gradient fade or arrow indicators when the sub-tab bar overflows horizontally.
|
|
||||||
- Consider a "more" dropdown for narrow viewports.
|
|
||||||
|
|
||||||
**IX-5: Improved toast system**
|
|
||||||
- Position toasts inside the settings panel (not `position: fixed`) to avoid overlap with global UI.
|
|
||||||
- Add a brief auto-dismiss with a progress bar, plus a manual dismiss button.
|
|
||||||
- Stack multiple toasts if needed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Priority Summary
|
|
||||||
|
|
||||||
| Priority | Items | Rationale |
|
|
||||||
|---|---|---|
|
|
||||||
| **P0 — Do Now** | IX-2 (confirmations), IX-1 (dirty state) | Prevent accidental data loss |
|
|
||||||
| **P1 — Next** | IX-6 (select all), A-3 (color-blind), A-4 (toast a11y), IX-3 (auto-expand) | Low effort, meaningful UX gains |
|
|
||||||
| **P2 — Soon** | IA-1/IA-3 (reorg), IX-4 (form consistency), VL-1 (status card), VL-3 (mobile table) | Structural quality |
|
|
||||||
| **P3 — Later** | VL-2 (smart buttons), IA-4 (search), VL-4 (scroll hints), IX-5 (toast rework) | Polish and future-proofing |
|
|
||||||
@@ -1,390 +0,0 @@
|
|||||||
# UX Guidelines
|
|
||||||
|
|
||||||
This document captures the UI/UX design patterns, conventions, and principles observed across
|
|
||||||
the trx-rs application. It covers the web frontend, CLI interfaces, configuration wizard, API
|
|
||||||
design, and error handling.
|
|
||||||
|
|
||||||
*Last reviewed: 2026-03-28*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Web Frontend (trx-frontend-http)
|
|
||||||
|
|
||||||
### 1.1 Layout and Navigation
|
|
||||||
|
|
||||||
The web UI is a single-page application served from embedded assets (no build step). It uses
|
|
||||||
a **tab-based** navigation model with six top-level tabs:
|
|
||||||
|
|
||||||
| Tab | Icon | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| **Main** | House | Primary radio control: spectrum, frequency, mode, PTT, VFO, SDR controls |
|
|
||||||
| **Bookmarks** | Bookmark | Saved frequency/mode presets with folder organisation |
|
|
||||||
| **Digital modes** | Bar chart | FT8/FT4/FT2, WSPR, CW, APRS, AIS, VDES decode tables |
|
|
||||||
| **Map** | Pin | Leaflet map for APRS/AIS/FT8 station plotting |
|
|
||||||
| **Settings** | Wrench | Scheduler, background decode, history retention |
|
|
||||||
| **About** | Info circle | Server/client/radio/audio/decoder/integration details |
|
|
||||||
|
|
||||||
Tabs use inline SVG icons with a text label below. On narrow viewports the tab bar wraps and
|
|
||||||
subtitles collapse to save space.
|
|
||||||
|
|
||||||
The **Settings** and **About** tabs each use a secondary **sub-tab bar** for further grouping
|
|
||||||
(e.g. Settings > Scheduler | Background Decode | History).
|
|
||||||
|
|
||||||
### 1.2 Theming
|
|
||||||
|
|
||||||
The UI supports **dark mode** (default) and **light mode** toggled via a header button. Theme
|
|
||||||
preference persists in `localStorage`.
|
|
||||||
|
|
||||||
Additionally, nine **colour styles** are available via a dropdown:
|
|
||||||
|
|
||||||
- Original (default), Arctic, Lime, Contrast, Neon Disco, Donald (golden-rain), Amber, Fire, Phosphor
|
|
||||||
|
|
||||||
Each style provides a full CSS custom-property override set for both dark and light variants.
|
|
||||||
Styles are applied via `data-style` and `data-theme` attributes on `<html>`.
|
|
||||||
|
|
||||||
All colours reference CSS custom properties (`--bg`, `--card-bg`, `--text`, `--accent-green`,
|
|
||||||
`--border-light`, etc.) so components never use hard-coded colour values.
|
|
||||||
|
|
||||||
### 1.3 Typography
|
|
||||||
|
|
||||||
- **Body**: `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`
|
|
||||||
- **Frequency display**: `DSEG14 Classic` (14-segment display font, loaded from CDN with `preload`)
|
|
||||||
- **Labels**: uppercase, 0.68-0.78 rem, `font-weight: 700`, `letter-spacing: 0.04em`
|
|
||||||
- **Section labels** use pill-shaped badges (`border-radius: 999px`) with muted text
|
|
||||||
|
|
||||||
### 1.4 Responsive Design
|
|
||||||
|
|
||||||
Six breakpoints handle layout adaptation:
|
|
||||||
|
|
||||||
| Breakpoint | Behaviour |
|
|
||||||
|---|---|
|
|
||||||
| `> 1100px` | Full width with bookmark side gutters on spectrum |
|
|
||||||
| `< 1100px` | Side bookmark panels hidden |
|
|
||||||
| `< 900px` | Card fills viewport width, reduced padding |
|
|
||||||
| `< 760px` | Tab bar wraps, controls stack vertically, safe-area-inset padding for notched devices |
|
|
||||||
| `< 640px` | Bottom-fixed tab bar (mobile), subtitles hidden, compact header |
|
|
||||||
| `< 520px` | Further compact adjustments |
|
|
||||||
|
|
||||||
Touch-specific: `@media (hover: none) and (pointer: coarse)` enlarges hit targets.
|
|
||||||
|
|
||||||
The spectrum panel hints adapt: mouse users see "Scroll to zoom / Ctrl+Scroll to tune /
|
|
||||||
Drag to pan" while touch users see "Pinch to zoom / Drag to pan".
|
|
||||||
|
|
||||||
### 1.5 Interactive Controls
|
|
||||||
|
|
||||||
- **Jog wheel**: Circular CSS-styled draggable dial for frequency tuning (skeuomorphic radial-gradient, grab cursor, shadow/inset). Plus/minus buttons flank it.
|
|
||||||
- **Step unit buttons**: Segmented button group (MHz / kHz / Hz) with `.active` highlight
|
|
||||||
- **Step scale**: 1x / 0.1x multiplier toggle
|
|
||||||
- **Frequency input**: Monospace DSEG14 font, editable `<input>` with disabled opacity fix
|
|
||||||
- **Mode selector**: `<select>` dropdown populated from rig capabilities
|
|
||||||
- **PTT / Power / Lock buttons**: Three-column grid in the transmit/power section
|
|
||||||
- **VFO picker**: Button group (horizontal on desktop, vertical stack on mobile)
|
|
||||||
- **WFM/SAM controls**: Compact labelled controls (de-emphasis, audio mode, denoise, stereo pilot flag, CCI/ACI interference bars)
|
|
||||||
- **SDR settings row**: AGC checkbox, RF/LNA gain inputs with Set buttons, noise blanker
|
|
||||||
|
|
||||||
### 1.6 Spectrum and Waterfall
|
|
||||||
|
|
||||||
The spectrum panel uses `<canvas>` elements (WebGL renderer optional) and offers:
|
|
||||||
|
|
||||||
- **Drag to pan**, **scroll to zoom**, **Ctrl+scroll to tune**
|
|
||||||
- Bandwidth edges are draggable to resize the filter
|
|
||||||
- Keyboard shortcuts: `+`/`-` zoom, arrows pan, `0` reset
|
|
||||||
- **Minimap** for orientation when zoomed
|
|
||||||
- **Resize grip** to adjust spectrum height
|
|
||||||
- Controls: bandwidth input, auto-BW, sweet-spot, peak hold (0-60s), floor (dB), range (dB), auto-level, contrast gamma slider
|
|
||||||
- **Waterfall/waveform split slider** (20%-80%, default 50/50)
|
|
||||||
- **Bookmark axis** overlays on left/right sides at wider viewports
|
|
||||||
- **Decoder overlays**: RDS station name, AIS/VDES/FT8/APRS/CW bar overlays using `aria-live="polite"`
|
|
||||||
|
|
||||||
### 1.7 Real-Time Data
|
|
||||||
|
|
||||||
- **SSE (Server-Sent Events)** on `/events` for rig state updates. Each SSE session gets a
|
|
||||||
UUID, enabling per-tab rig selection without interfering with other tabs.
|
|
||||||
- **Named events**: `data` (state), `session` (session UUID), `channels` (virtual channels),
|
|
||||||
`b` (spectrum bins as base64), `rds`, `vchan_rds`, `ping` (5-second heartbeat)
|
|
||||||
- **WebSocket** on `/audio` for Opus-encoded RX audio streaming
|
|
||||||
- **Connection lost banner**: `#server-lost-banner` with pulsing dot, text "trx-server
|
|
||||||
connection lost -- waiting for reconnect", uses `aria-live="assertive"`
|
|
||||||
- **Loading state**: Centered "Initializing (rig)..." with subtitle, content hidden until ready
|
|
||||||
|
|
||||||
### 1.8 Accessibility
|
|
||||||
|
|
||||||
- All interactive elements have `aria-label` attributes
|
|
||||||
- Spectrum overlays use `aria-live="polite"` for screen reader announcements
|
|
||||||
- Connection-lost banner uses `aria-live="assertive"`
|
|
||||||
- `aria-hidden="true"` on decorative canvases and visual-only elements
|
|
||||||
- SVG icons include `aria-hidden="true"` with descriptive labels on parent buttons
|
|
||||||
- Spectrum resize grip has both `title` and `aria-label`
|
|
||||||
|
|
||||||
### 1.9 Authentication UX
|
|
||||||
|
|
||||||
When auth is enabled, an **auth gate** blocks the UI with:
|
|
||||||
|
|
||||||
- Title: "Access Required"
|
|
||||||
- Subtitle: "Enter passphrase to continue"
|
|
||||||
- Password input + Login button (green accent, full-width)
|
|
||||||
- Optional "Continue as Guest" button (shown when RX passphrase is not set)
|
|
||||||
- Error message area (red `#ff6b6b`)
|
|
||||||
- Role badge display
|
|
||||||
|
|
||||||
Two roles: **Rx** (read-only) and **Control** (full access including TX/PTT).
|
|
||||||
|
|
||||||
Session cookie: `trx_http_sid`, HttpOnly, configurable Secure and SameSite attributes.
|
|
||||||
|
|
||||||
The header shows a Login/Logout button when auth is enabled (`#header-auth-btn`).
|
|
||||||
|
|
||||||
### 1.10 Multi-Rig Support
|
|
||||||
|
|
||||||
- **Header rig switcher**: `<select>` dropdown in the top bar for switching between connected rigs
|
|
||||||
- Per-tab rig binding: each SSE session independently selects a rig via `?remote=` query parameter
|
|
||||||
- Rig state isolation: only the disconnected rig shows the connection-lost banner
|
|
||||||
- About tab shows active rig, available rigs list
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. REST API Design
|
|
||||||
|
|
||||||
### 2.1 Conventions
|
|
||||||
|
|
||||||
- **Read operations** use `GET` (e.g. `/status`, `/events`, `/decode/history`, `/rigs`, `/bookmarks`)
|
|
||||||
- **Mutations** use `POST` for actions and toggles (e.g. `/set_freq`, `/toggle_power`, `/toggle_ft8_decode`)
|
|
||||||
- **CRUD resources** use proper verbs: `GET /bookmarks`, `POST /bookmarks`, `PUT /bookmarks/{id}`,
|
|
||||||
`DELETE /bookmarks/{id}`
|
|
||||||
- **Batch operations**: `POST /bookmarks/batch_delete`, `POST /bookmarks/batch_move`
|
|
||||||
- **Nested resources**: `/channels/{remote}/{channel_id}/subscribe`, `/scheduler/{remote}/status`
|
|
||||||
- Responses are JSON with `Content-Type: application/json`
|
|
||||||
- SSE stream uses `Content-Type: text/event-stream` with `no-cache` and `keep-alive` headers
|
|
||||||
|
|
||||||
### 2.2 Request Timeout
|
|
||||||
|
|
||||||
All rig command requests have a **15-second timeout** (`REQUEST_TIMEOUT`). If the command
|
|
||||||
doesn't complete in time, the request returns an error rather than hanging.
|
|
||||||
|
|
||||||
### 2.3 Error Responses
|
|
||||||
|
|
||||||
- `401 Unauthorized`: `{"error": "Invalid credentials"}` or `{"error": "Authentication required"}`
|
|
||||||
- `429 Too Many Requests`: `{"error": "Too many login attempts, please try again later"}`
|
|
||||||
- `404 Not Found`: Auth endpoints when auth is disabled
|
|
||||||
- `500 Internal Server Error`: Serialization failures
|
|
||||||
- Rate limiting: 10 attempts per 60-second window per IP, counter resets on successful login
|
|
||||||
|
|
||||||
### 2.4 State Enrichment
|
|
||||||
|
|
||||||
API responses merge rig state with **frontend metadata** (`FrontendMeta`) via `serde(flatten)`:
|
|
||||||
|
|
||||||
```
|
|
||||||
http_clients, rigctl_clients, audio_clients, rigctl_addr,
|
|
||||||
active_remote, remotes[], owner_callsign, owner_website_url,
|
|
||||||
owner_website_name, ais_vessel_url_base, show_sdr_gain_control,
|
|
||||||
initial_map_zoom, spectrum_coverage_margin_hz, spectrum_usable_span_ratio,
|
|
||||||
decode_history_retention_min, server_connected
|
|
||||||
```
|
|
||||||
|
|
||||||
This single-payload approach avoids extra round trips for UI configuration.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. CLI Interface
|
|
||||||
|
|
||||||
### 3.1 Argument Style
|
|
||||||
|
|
||||||
Both `trx-server` and `trx-client` use **clap** for argument parsing with short and long flags:
|
|
||||||
|
|
||||||
```
|
|
||||||
-C, --config FILE Path to configuration file
|
|
||||||
--print-config Print example configuration and exit
|
|
||||||
-r, --rig NAME Rig backend name
|
|
||||||
-l, --listen ADDR Listen address
|
|
||||||
-p, --port NUM Port number
|
|
||||||
```
|
|
||||||
|
|
||||||
Positional arguments are used sparingly (e.g. `RIG_ADDR` for serial/TCP address).
|
|
||||||
|
|
||||||
### 3.2 Configuration Resolution
|
|
||||||
|
|
||||||
Config files are searched in priority order:
|
|
||||||
1. Current directory: `trx-rs.toml`
|
|
||||||
2. XDG config: `~/.config/trx-rs/trx-rs.toml`
|
|
||||||
3. System: `/etc/trx-rs/trx-rs.toml`
|
|
||||||
|
|
||||||
The loaded config path is logged: `INFO Loaded configuration from /path/to/config.toml`
|
|
||||||
|
|
||||||
### 3.3 Example Config Generation
|
|
||||||
|
|
||||||
`--print-config` outputs a complete, commented TOML file to stdout with example values
|
|
||||||
(callsign `N0CALL`, coordinates `52.2297, 21.0122`). Each section has a header comment and
|
|
||||||
each field has an inline description.
|
|
||||||
|
|
||||||
### 3.4 Startup Log Sequence
|
|
||||||
|
|
||||||
Server:
|
|
||||||
```
|
|
||||||
INFO Loaded configuration from /path/to/config.toml
|
|
||||||
INFO Starting trx-server with N rig(s): [rig-names]
|
|
||||||
INFO Callsign: CALL
|
|
||||||
INFO [rig-id] Starting (rig: ft817, access: serial /dev/ttyUSB0 @ 9600 baud)
|
|
||||||
INFO Listening on 0.0.0.0:4530
|
|
||||||
```
|
|
||||||
|
|
||||||
Client:
|
|
||||||
```
|
|
||||||
INFO Loaded configuration from /path/to/config.toml
|
|
||||||
INFO Starting trx-client (remotes: [remote-names], frontends: http,rigctl)
|
|
||||||
INFO rigctl frontend for rig 'default' on 127.0.0.1:4532
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Configuration Wizard (trx-configurator)
|
|
||||||
|
|
||||||
### 4.1 Interactive Mode
|
|
||||||
|
|
||||||
Uses the **dialoguer** crate for terminal prompts:
|
|
||||||
|
|
||||||
- `Select` menus for enumerated choices (config type, rig model, access type, log level)
|
|
||||||
- `Input` for free-text with defaults (callsign defaults to `N0CALL`, listen defaults to `127.0.0.1`)
|
|
||||||
- `Confirm` for yes/no questions (enable auth, set location, etc.)
|
|
||||||
- Serial port auto-detection with fallback to `/dev/ttyUSB0`
|
|
||||||
|
|
||||||
### 4.2 Non-Interactive Mode
|
|
||||||
|
|
||||||
`--defaults` generates a config file without prompts, using sensible defaults.
|
|
||||||
|
|
||||||
### 4.3 Config Validation
|
|
||||||
|
|
||||||
`--check FILE` validates an existing config file:
|
|
||||||
|
|
||||||
```
|
|
||||||
/path/to/config.toml: valid TOML
|
|
||||||
Detected type: server
|
|
||||||
warning: [general].log_level 'verbose' is invalid (expected: trace, debug, info, warn, error)
|
|
||||||
1 warning(s), 0 error(s)
|
|
||||||
```
|
|
||||||
|
|
||||||
Validates: TOML syntax, unknown keys, log levels, coordinate ranges (-90..90 lat, -180..180 lon
|
|
||||||
with pair requirement), access types, port ranges (0-65535).
|
|
||||||
|
|
||||||
### 4.4 File Write Confirmation
|
|
||||||
|
|
||||||
Prompts before overwriting an existing file. Outputs `Wrote /path/to/file` on success.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Error Handling and User-Facing Messages
|
|
||||||
|
|
||||||
### 5.1 Error Message Conventions
|
|
||||||
|
|
||||||
- **Contextual**: Include file paths, section names, and peer addresses
|
|
||||||
- `"Failed to parse config file /path: error details"`
|
|
||||||
- `"Unknown rig model: X (available: ft817, ft450d, soapysdr)"`
|
|
||||||
- **Actionable**: Suggest alternatives when available
|
|
||||||
- `"Rig model not specified. Use --rig or set [rig].model in config."`
|
|
||||||
- `"Unknown frontend: X (available: http, rigctl, httpjson)"`
|
|
||||||
- **Structured**: Use field=value format in structured logging
|
|
||||||
|
|
||||||
### 5.2 Log Level Guidelines
|
|
||||||
|
|
||||||
| Level | Usage |
|
|
||||||
|---|---|
|
|
||||||
| `INFO` | Startup milestones, configuration loaded, listening, client connect/disconnect, decoder state changes |
|
|
||||||
| `WARN` | Non-fatal issues: command took too long, panel lock blocking, VFO priming failed, initial tune failed |
|
|
||||||
| `ERROR` | Fatal or significant failures: CAT polling errors, client errors, parse failures |
|
|
||||||
|
|
||||||
Logs suppress module targets (`with_target(false)`) for cleaner output.
|
|
||||||
|
|
||||||
### 5.3 Connection State Communication
|
|
||||||
|
|
||||||
- Server logs: `"Client connected: {peer}"`, `"Client {peer} disconnected"`, `"Client {peer} closing due to shutdown"`
|
|
||||||
- Rig task: `"[rig-id] Rig backend ready"`, `"Serial: /dev/ttyUSB0 @ 9600 baud"`
|
|
||||||
- Web UI: Connection-lost banner with reconnect indication, per-rig isolation
|
|
||||||
|
|
||||||
### 5.4 Graceful Degradation
|
|
||||||
|
|
||||||
- Startup continues after non-fatal failures: `"Initial PowerOn failed (continuing)"`
|
|
||||||
- Stream errors are deduplicated with 60-second summaries to avoid log flooding
|
|
||||||
- Lock poisoning is recovered from rather than panicking
|
|
||||||
- Unknown SSE events or lagged broadcast channels are silently skipped
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Branding and Customisation
|
|
||||||
|
|
||||||
### 6.1 Owner Branding
|
|
||||||
|
|
||||||
Configurable via TOML and exposed via `FrontendMeta`:
|
|
||||||
|
|
||||||
- `owner_callsign` -- displayed in header subtitle and About tab
|
|
||||||
- `owner_website_url` / `owner_website_name` -- optional link in header
|
|
||||||
- `ais_vessel_url_base` -- base URL for linking AIS vessel MMSI numbers
|
|
||||||
|
|
||||||
### 6.2 UI Behaviour Configuration
|
|
||||||
|
|
||||||
- `http_show_sdr_gain_control` -- show/hide RF gain controls
|
|
||||||
- `http_initial_map_zoom` -- default map zoom level
|
|
||||||
- `http_spectrum_coverage_margin_hz` -- guard margin for spectrum center retune
|
|
||||||
- `http_spectrum_usable_span_ratio` -- fraction of spectrum span treated as usable
|
|
||||||
- `http_decode_history_retention_min` -- default history retention (per-rig overrides supported)
|
|
||||||
|
|
||||||
### 6.3 Embedded Assets
|
|
||||||
|
|
||||||
Logo and favicon are embedded at compile time via `include_bytes!`. The logo image has an
|
|
||||||
`onerror` handler to hide itself if loading fails (`this.style.display='none'`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Security UX
|
|
||||||
|
|
||||||
### 7.1 Route Access Classification
|
|
||||||
|
|
||||||
Routes are classified into three tiers:
|
|
||||||
|
|
||||||
| Tier | Examples | Requirement |
|
|
||||||
|---|---|---|
|
|
||||||
| **Public** | `/`, `/index.html`, `/map`, `/auth/*`, static assets | None |
|
|
||||||
| **Read** | `/status`, `/events`, `/audio`, `/decode`, `/spectrum`, `/bookmarks` | Rx or Control role |
|
|
||||||
| **Control** | `/set_freq`, `/set_mode`, `/set_ptt`, `/toggle_power`, all other POST | Control role only |
|
|
||||||
|
|
||||||
### 7.2 Session Management
|
|
||||||
|
|
||||||
- Sessions are 128-bit random hex tokens stored in HttpOnly cookies
|
|
||||||
- Configurable TTL (default from TOML config)
|
|
||||||
- Expired sessions auto-pruned on access
|
|
||||||
- Constant-time passphrase comparison to mitigate timing attacks
|
|
||||||
|
|
||||||
### 7.3 TX Access Control
|
|
||||||
|
|
||||||
An additional `tx_access_control_enabled` flag can restrict transmit-related actions even
|
|
||||||
for Control-role users, providing an extra safety layer.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Virtual Channels (SDR)
|
|
||||||
|
|
||||||
Virtual channels allow SDR users to monitor multiple frequencies simultaneously:
|
|
||||||
|
|
||||||
- Channels appear in a picker row below the VFO controls
|
|
||||||
- CRUD API: `POST /channels/{remote}` to create, `DELETE` to remove, `PUT` to update freq/mode/BW
|
|
||||||
- Subscribe/unsubscribe audio per channel
|
|
||||||
- Background decode channels (hidden, no audio stream back)
|
|
||||||
- Channels auto-destroyed when out-of-bandwidth after center-frequency retune
|
|
||||||
- Channel-list changes broadcast to SSE clients via `event: channels`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Design Principles (Inferred)
|
|
||||||
|
|
||||||
1. **Server-rendered SPA**: All HTML/CSS/JS embedded in the binary -- zero external build tooling, no CDN dependency for core functionality (CDN used only for fonts and Leaflet maps).
|
|
||||||
|
|
||||||
2. **Progressive disclosure**: Advanced controls (WFM, SAM, SDR settings, spectrum controls) are hidden by default and revealed based on the active mode and backend type.
|
|
||||||
|
|
||||||
3. **Keyboard-first, touch-aware**: Spectrum supports full keyboard navigation alongside mouse and touch gestures. Mobile breakpoints enlarge hit targets and adapt layout.
|
|
||||||
|
|
||||||
4. **Real-time by default**: SSE + WebSocket provide sub-second state updates without polling from the browser. 5-second ping heartbeat detects stale connections.
|
|
||||||
|
|
||||||
5. **Per-tab isolation**: Each browser tab gets its own SSE session UUID and can independently select a rig, preventing cross-tab interference.
|
|
||||||
|
|
||||||
6. **Configuration over code**: UI behaviour knobs (gain visibility, map zoom, history retention, spectrum margins) are exposed as TOML config rather than requiring code changes.
|
|
||||||
|
|
||||||
7. **Graceful degradation**: The UI handles server disconnection gracefully with visible banners, and only the affected rig shows as disconnected in multi-rig setups.
|
|
||||||
|
|
||||||
8. **Defensive security defaults**: Auth disabled by default for ease of setup, but when enabled, provides role-based access, rate limiting, constant-time comparison, and HttpOnly cookies.
|
|
||||||
@@ -1,546 +0,0 @@
|
|||||||
# trx-rs Manual
|
|
||||||
|
|
||||||
## What trx-rs is
|
|
||||||
|
|
||||||
`trx-rs` is a modular amateur radio control stack written in Rust. It splits
|
|
||||||
hardware access, DSP, transport, and user-facing interfaces into separate
|
|
||||||
components so a radio or SDR can be controlled locally while audio, decoding,
|
|
||||||
and remote control are exposed elsewhere on the network.
|
|
||||||
|
|
||||||
In practice, `trx-server` owns the rig or SDR backend and runs the DSP
|
|
||||||
pipeline, while `trx-client` connects to it and provides frontends such as the
|
|
||||||
web UI, JSON control, and rigctl-compatible access. The workspace also includes
|
|
||||||
protocol decoders and plugin-based extension points for adding backends and
|
|
||||||
frontends.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Both `trx-server` and `trx-client` use TOML configuration files. Use
|
|
||||||
`--print-config` to generate a fully commented example.
|
|
||||||
|
|
||||||
### File Locations
|
|
||||||
|
|
||||||
**trx-server** lookup order:
|
|
||||||
1. `--config <FILE>`
|
|
||||||
2. `./trx-server.toml`
|
|
||||||
3. `~/.trx-server.toml`
|
|
||||||
4. `~/.config/trx-rs/server.toml`
|
|
||||||
5. `/etc/trx-rs/server.toml`
|
|
||||||
|
|
||||||
**trx-client** lookup order:
|
|
||||||
1. `--config <FILE>`
|
|
||||||
2. `./trx-client.toml`
|
|
||||||
3. `~/.config/trx-rs/client.toml`
|
|
||||||
4. `/etc/trx-rs/client.toml`
|
|
||||||
|
|
||||||
CLI arguments override config file values.
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
- `TRX_PLUGIN_DIRS`: additional plugin directories (path-separated), used by
|
|
||||||
both server and client.
|
|
||||||
|
|
||||||
### Server Options
|
|
||||||
|
|
||||||
#### `[general]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `callsign` | string | `"N0CALL"` | Station callsign |
|
|
||||||
| `log_level` | string | — | `trace`, `debug`, `info`, `warn`, or `error` |
|
|
||||||
| `latitude` | float | — | Station latitude (-90..90) |
|
|
||||||
| `longitude` | float | — | Station longitude (-180..180) |
|
|
||||||
|
|
||||||
`latitude` and `longitude` must be set together or both omitted.
|
|
||||||
|
|
||||||
#### `[rig]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `model` | string | — | Backend name (`ft817`, `ft450d`, `soapysdr`) |
|
|
||||||
| `initial_freq_hz` | u64 | `144300000` | Startup frequency (must be > 0) |
|
|
||||||
| `initial_mode` | string | `"USB"` | Startup mode |
|
|
||||||
|
|
||||||
#### `[rig.access]`
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| `type` | string | `serial`, `tcp`, or `sdr` |
|
|
||||||
| `port` | string | Serial port path (serial mode) |
|
|
||||||
| `baud` | u32 | Serial baud rate (serial mode) |
|
|
||||||
| `host` | string | Remote host (tcp mode) |
|
|
||||||
| `tcp_port` | u16 | Remote port (tcp mode) |
|
|
||||||
| `args` | string | SoapySDR device args (sdr mode, e.g. `"driver=rtlsdr"`) |
|
|
||||||
|
|
||||||
#### `[behavior]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `poll_interval_ms` | u64 | `500` | Rig polling interval |
|
|
||||||
| `poll_interval_tx_ms` | u64 | `100` | Polling interval during TX |
|
|
||||||
| `max_retries` | u32 | `3` | Connection retry limit |
|
|
||||||
| `retry_base_delay_ms` | u64 | `100` | Base retry delay |
|
|
||||||
|
|
||||||
#### `[listen]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `enabled` | bool | `true` | Enable JSON TCP listener |
|
|
||||||
| `listen` | ip | `127.0.0.1` | Bind address |
|
|
||||||
| `port` | u16 | `4530` | Bind port |
|
|
||||||
|
|
||||||
#### `[listen.auth]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `tokens` | string[] | `[]` | Allowed auth tokens (empty = no auth) |
|
|
||||||
|
|
||||||
#### `[audio]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `enabled` | bool | `true` | Enable audio streaming |
|
|
||||||
| `listen` | ip | `127.0.0.1` | Bind address |
|
|
||||||
| `port` | u16 | `4531` | Bind port |
|
|
||||||
| `rx_enabled` | bool | `true` | Enable RX audio |
|
|
||||||
| `tx_enabled` | bool | `true` | Enable TX audio |
|
|
||||||
| `device` | string | — | CPAL device name (empty = default) |
|
|
||||||
| `sample_rate` | u32 | `48000` | Sample rate (8000–192000) |
|
|
||||||
| `channels` | u8 | `1` | Channel count (1 or 2) |
|
|
||||||
| `frame_duration_ms` | u16 | `20` | Opus frame duration (3, 5, 10, 20, 40, 60) |
|
|
||||||
| `bitrate_bps` | u32 | `24000` | Opus bitrate |
|
|
||||||
|
|
||||||
When audio is enabled, at least one of `rx_enabled` or `tx_enabled` must be true.
|
|
||||||
|
|
||||||
#### `[sdr]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `sample_rate` | u32 | `1920000` | IQ capture rate in Hz |
|
|
||||||
| `bandwidth` | u32 | `1500000` | Hardware IF filter bandwidth in Hz |
|
|
||||||
| `center_offset_hz` | i64 | `100000` | Offset from dial to avoid DC spur |
|
|
||||||
|
|
||||||
#### `[sdr.gain]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `mode` | string | `"auto"` | `"auto"` (hardware AGC) or `"manual"` |
|
|
||||||
| `value` | f64 | `30.0` | Gain in dB (manual mode only) |
|
|
||||||
|
|
||||||
#### `[sdr.squelch]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `enabled` | bool | `false` | Enable software squelch |
|
|
||||||
| `threshold_db` | f32 | `-65.0` | Open threshold in dBFS (-140..0) |
|
|
||||||
| `hysteresis_db` | f32 | `3.0` | Close hysteresis in dB (0..40) |
|
|
||||||
| `tail_ms` | u32 | `180` | Tail hold time in ms (0..10000) |
|
|
||||||
|
|
||||||
#### `[[sdr.channels]]`
|
|
||||||
|
|
||||||
Defines virtual receiver channels within the wideband IQ stream. The first
|
|
||||||
channel is the primary channel (controlled by `set_freq`/`set_mode`).
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `id` | string | `""` | Human-readable label |
|
|
||||||
| `offset_hz` | i64 | `0` | Frequency offset from dial |
|
|
||||||
| `mode` | string | `"auto"` | Demod mode (`auto`, `LSB`, `USB`, `CW`, `AM`, `FM`, `WFM`, etc.) |
|
|
||||||
| `audio_bandwidth_hz` | u32 | `3000` | Post-demod audio bandwidth |
|
|
||||||
| `fir_taps` | usize | `64` | FIR filter tap count |
|
|
||||||
| `cw_center_hz` | u32 | `700` | CW tone centre frequency |
|
|
||||||
| `wfm_bandwidth_hz` | u32 | `75000` | WFM pre-demod filter bandwidth |
|
|
||||||
| `decoders` | string[] | `[]` | Decoder IDs for this channel (`ft8`, `wspr`, `aprs`, `cw`) |
|
|
||||||
| `stream_opus` | bool | `false` | Stream this channel's audio to clients |
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Each decoder ID may appear in at most one channel.
|
|
||||||
- At most one channel may set `stream_opus = true`.
|
|
||||||
- Channel IF constraint: `|center_offset_hz + offset_hz| < sample_rate / 2`.
|
|
||||||
|
|
||||||
#### `[pskreporter]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `enabled` | bool | `false` | Enable PSKReporter uplink |
|
|
||||||
| `host` | string | `"report.pskreporter.info"` | Server host |
|
|
||||||
| `port` | u16 | `4739` | Server port |
|
|
||||||
| `receiver_locator` | string | — | Maidenhead grid (derived from lat/lon if omitted) |
|
|
||||||
|
|
||||||
#### `[aprsfi]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `enabled` | bool | `false` | Enable APRS-IS IGate |
|
|
||||||
| `host` | string | `"rotate.aprs.net"` | Server host |
|
|
||||||
| `port` | u16 | `14580` | Server port |
|
|
||||||
| `passcode` | i32 | `-1` | APRS-IS passcode (-1 = auto from callsign) |
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- `[general].callsign` must be non-empty when enabled.
|
|
||||||
- Only APRS packets with valid CRC are forwarded.
|
|
||||||
- Reconnects with exponential backoff (1 s → 60 s) on TCP errors.
|
|
||||||
|
|
||||||
#### `[decode_logs]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `enabled` | bool | `false` | Enable decoder logging |
|
|
||||||
| `dir` | string | `"$XDG_DATA_HOME/trx-rs/decoders"` | Log directory |
|
|
||||||
| `aprs_file` | string | `"TRXRS-APRS-%YYYY%-%MM%-%DD%.log"` | APRS log filename |
|
|
||||||
| `cw_file` | string | `"TRXRS-CW-%YYYY%-%MM%-%DD%.log"` | CW log filename |
|
|
||||||
| `ft8_file` | string | `"TRXRS-FT8-%YYYY%-%MM%-%DD%.log"` | FT8 log filename |
|
|
||||||
| `wspr_file` | string | `"TRXRS-WSPR-%YYYY%-%MM%-%DD%.log"` | WSPR log filename |
|
|
||||||
|
|
||||||
Files are appended in JSON Lines format. Supported date tokens: `%YYYY%`,
|
|
||||||
`%MM%`, `%DD%` (UTC).
|
|
||||||
|
|
||||||
#### Multi-Rig Configuration
|
|
||||||
|
|
||||||
Use `[[rigs]]` arrays instead of the flat `[rig]` section for multi-rig setups:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[rigs]]
|
|
||||||
id = "ft817_0"
|
|
||||||
name = "HF Transceiver"
|
|
||||||
[rigs.rig]
|
|
||||||
model = "ft817"
|
|
||||||
[rigs.rig.access]
|
|
||||||
type = "serial"
|
|
||||||
path = "/dev/ttyUSB0"
|
|
||||||
baud = 9600
|
|
||||||
|
|
||||||
[[rigs]]
|
|
||||||
id = "sdr_0"
|
|
||||||
name = "VHF/UHF SDR"
|
|
||||||
[rigs.rig]
|
|
||||||
model = "soapysdr"
|
|
||||||
[rigs.rig.access]
|
|
||||||
type = "sdr"
|
|
||||||
args = "driver=rtlsdr"
|
|
||||||
```
|
|
||||||
|
|
||||||
When `[[rigs]]` is present it takes priority over the flat `[rig]` section.
|
|
||||||
Rigs without an explicit `id` get auto-generated IDs like `ft817_0`, `soapysdr_1`.
|
|
||||||
|
|
||||||
### Client Options
|
|
||||||
|
|
||||||
#### `[general]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `callsign` | string | `"N0CALL"` | Station callsign |
|
|
||||||
| `log_level` | string | — | `trace`, `debug`, `info`, `warn`, or `error` |
|
|
||||||
|
|
||||||
#### `[remote]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `url` | string | — | Server address (e.g. `localhost:4530`) |
|
|
||||||
| `poll_interval_ms` | u64 | `750` | State poll interval |
|
|
||||||
|
|
||||||
#### `[remote.auth]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `token` | string | — | Auth token (must not be empty if set) |
|
|
||||||
|
|
||||||
#### `[frontends.http]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `enabled` | bool | `true` | Enable web UI |
|
|
||||||
| `listen` | ip | `127.0.0.1` | Bind address |
|
|
||||||
| `port` | u16 | `8080` | Bind port |
|
|
||||||
|
|
||||||
#### `[frontends.rigctl]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `enabled` | bool | `false` | Enable Hamlib rigctl |
|
|
||||||
| `listen` | ip | `127.0.0.1` | Bind address |
|
|
||||||
| `port` | u16 | `4532` | Bind port |
|
|
||||||
|
|
||||||
#### `[frontends.http_json]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `enabled` | bool | `true` | Enable JSON-over-TCP |
|
|
||||||
| `listen` | ip | `127.0.0.1` | Bind address |
|
|
||||||
| `port` | u16 | `0` | Bind port (0 = ephemeral) |
|
|
||||||
| `auth.tokens` | string[] | `[]` | Allowed auth tokens |
|
|
||||||
|
|
||||||
#### `[frontends.audio]`
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `enabled` | bool | `true` | Enable audio client |
|
|
||||||
| `server_port` | u16 | `4531` | Server audio port |
|
|
||||||
| `bridge.enabled` | bool | `false` | Enable local CPAL audio bridge |
|
|
||||||
| `bridge.rx_output_device` | string | — | Local playback device |
|
|
||||||
| `bridge.tx_input_device` | string | — | Local capture device |
|
|
||||||
| `bridge.rx_gain` | float | `1.0` | RX playback gain |
|
|
||||||
| `bridge.tx_gain` | float | `1.0` | TX capture gain |
|
|
||||||
|
|
||||||
The bridge is intended for WSJT-X integration via virtual audio devices (ALSA
|
|
||||||
loopback on Linux, BlackHole on macOS).
|
|
||||||
|
|
||||||
### CLI Override Summary
|
|
||||||
|
|
||||||
**trx-server:**
|
|
||||||
`--config`, `--print-config`, `--rig`, `--access`, `--callsign`, `--listen`,
|
|
||||||
`--port`. SDR options are file-only.
|
|
||||||
|
|
||||||
**trx-client:**
|
|
||||||
`--config`, `--print-config`, `--url`, `--token`, `--poll-interval`,
|
|
||||||
`--frontend`, `--http-listen`, `--http-port`, `--rigctl-listen`,
|
|
||||||
`--rigctl-port`, `--http-json-listen`, `--http-json-port`, `--callsign`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
The HTTP frontend supports optional passphrase-based authentication with two
|
|
||||||
roles:
|
|
||||||
|
|
||||||
- **rx** — read-only access (monitoring, audio, decode streams)
|
|
||||||
- **control** — full access (frequency, mode, PTT, and all settings)
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[frontends.http.auth]
|
|
||||||
enabled = false
|
|
||||||
rx_passphrase = "rx-only-passphrase"
|
|
||||||
control_passphrase = "full-control-passphrase"
|
|
||||||
tx_access_control_enabled = true
|
|
||||||
session_ttl_min = 480
|
|
||||||
cookie_secure = false # true if served via HTTPS
|
|
||||||
cookie_same_site = "Lax" # Strict|Lax|None
|
|
||||||
```
|
|
||||||
|
|
||||||
When `enabled = false` (the default), all auth is bypassed and the UI behaves
|
|
||||||
as before. When enabled, at least one passphrase must be set.
|
|
||||||
|
|
||||||
### Behaviour
|
|
||||||
|
|
||||||
- On login, the server issues an `HttpOnly` session cookie.
|
|
||||||
- Sessions are in-memory; a server restart invalidates all sessions.
|
|
||||||
- Rate limiting is applied per IP to mitigate brute-force attempts.
|
|
||||||
- When `tx_access_control_enabled = true`, TX/PTT controls are hidden and
|
|
||||||
rejected for unauthenticated or `rx`-role users.
|
|
||||||
|
|
||||||
### Routes
|
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
|
||||||
|----------|--------|-------------|
|
|
||||||
| `/auth/login` | POST | Submit `{ "passphrase": "..." }` |
|
|
||||||
| `/auth/logout` | POST | Clear session |
|
|
||||||
| `/auth/session` | GET | Check current session/role |
|
|
||||||
|
|
||||||
Protected routes require at least `rx` role. Control routes (set frequency,
|
|
||||||
mode, PTT, etc.) require `control` role.
|
|
||||||
|
|
||||||
### Frontend Flow
|
|
||||||
|
|
||||||
1. On load, the UI calls `/auth/session`.
|
|
||||||
2. If unauthenticated, a login screen is shown.
|
|
||||||
3. On successful login, the normal UI loads.
|
|
||||||
4. `rx` users see a read-only interface; `control` users get full controls.
|
|
||||||
5. If a session expires mid-use, streams stop and the login screen returns.
|
|
||||||
|
|
||||||
### Transport Security
|
|
||||||
|
|
||||||
There is no built-in TLS. For remote access, place trx-rs behind a
|
|
||||||
TLS-terminating reverse proxy (nginx, Caddy) and set `cookie_secure = true`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Background Decoding Scheduler
|
|
||||||
|
|
||||||
The scheduler automatically retunes the rig to pre-configured bookmarks when no
|
|
||||||
users are connected to the HTTP frontend. It runs as a background task inside
|
|
||||||
`trx-frontend-http`, polling every 30 seconds.
|
|
||||||
|
|
||||||
### Modes
|
|
||||||
|
|
||||||
#### Disabled (default)
|
|
||||||
|
|
||||||
Scheduler is inactive. The rig is not touched automatically.
|
|
||||||
|
|
||||||
#### Grayline
|
|
||||||
|
|
||||||
Retunes around the solar terminator (day/night boundary).
|
|
||||||
|
|
||||||
The user provides:
|
|
||||||
- Station latitude and longitude (decimal degrees)
|
|
||||||
- Optional transition window width (minutes, default 20)
|
|
||||||
- Bookmark IDs for four periods:
|
|
||||||
- **Dawn** — window around sunrise (`sunrise ± window_min/2`)
|
|
||||||
- **Day** — after dawn until dusk
|
|
||||||
- **Dusk** — window around sunset (`sunset ± window_min/2`)
|
|
||||||
- **Night** — after dusk until next dawn
|
|
||||||
|
|
||||||
Period precedence (most specific wins): Dawn > Dusk > Day > Night.
|
|
||||||
|
|
||||||
If no bookmark is assigned to a period, the rig is not retuned for that period.
|
|
||||||
|
|
||||||
Sunrise/sunset is computed inline using the NOAA simplified algorithm. Polar
|
|
||||||
regions (midnight sun / polar night) fall back to Day/Night accordingly.
|
|
||||||
|
|
||||||
#### TimeSpan
|
|
||||||
|
|
||||||
Retunes according to a list of user-defined time windows (UTC).
|
|
||||||
|
|
||||||
Each entry specifies:
|
|
||||||
- `start_hhmm` — start of window (e.g. 600 = 06:00 UTC)
|
|
||||||
- `end_hhmm` — end of window (e.g. 700 = 07:00 UTC)
|
|
||||||
- `bookmark_id` — bookmark to apply
|
|
||||||
- `label` — optional human-readable description
|
|
||||||
|
|
||||||
Windows that span midnight (`end_hhmm < start_hhmm`) are supported. When
|
|
||||||
multiple entries overlap, the first match (by list order) wins.
|
|
||||||
|
|
||||||
### Storage
|
|
||||||
|
|
||||||
Configuration is stored in PickleDB at `~/.config/trx-rs/scheduler.db`.
|
|
||||||
|
|
||||||
Keys: `sch:{rig_id}` → JSON `SchedulerConfig`.
|
|
||||||
|
|
||||||
### HTTP API
|
|
||||||
|
|
||||||
All read endpoints are accessible at the **Rx** role level. Write endpoints
|
|
||||||
require the **Control** role.
|
|
||||||
|
|
||||||
| Method | Path | Description |
|
|
||||||
|--------|------|-------------|
|
|
||||||
| GET | `/scheduler/{rig_id}` | Get scheduler config for a rig |
|
|
||||||
| PUT | `/scheduler/{rig_id}` | Save scheduler config (Control only) |
|
|
||||||
| DELETE | `/scheduler/{rig_id}` | Reset config to Disabled (Control only) |
|
|
||||||
| GET | `/scheduler/{rig_id}/status` | Get last-applied bookmark and next event |
|
|
||||||
|
|
||||||
### Activation Logic
|
|
||||||
|
|
||||||
Every 30 seconds the scheduler task checks:
|
|
||||||
1. No SSE clients connected
|
|
||||||
2. Active rig has a non-Disabled scheduler config
|
|
||||||
3. Current UTC time matches a scheduled window or grayline period
|
|
||||||
4. If the matching bookmark differs from last applied, send `SetFreq` + `SetMode`
|
|
||||||
|
|
||||||
The scheduler does not revert changes when users reconnect.
|
|
||||||
|
|
||||||
### Web UI
|
|
||||||
|
|
||||||
A dedicated tab with a clock icon provides:
|
|
||||||
- Rig selector (read-only, shows active rig)
|
|
||||||
- Mode picker: Disabled / Grayline / TimeSpan
|
|
||||||
- Grayline section: lat/lon inputs, transition window slider, four bookmark selectors
|
|
||||||
- TimeSpan section: table of entries with start/end times, bookmark, label
|
|
||||||
- Status card: last applied bookmark name and timestamp
|
|
||||||
- Save button (Control role only)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SDR Noise Blanker
|
|
||||||
|
|
||||||
The noise blanker suppresses impulse noise (clicks, pops, ignition interference)
|
|
||||||
on raw IQ samples before any mixing or filtering takes place. It works by
|
|
||||||
tracking a running RMS level of the signal and replacing any sample whose
|
|
||||||
magnitude exceeds **threshold x RMS** with the last known clean sample.
|
|
||||||
|
|
||||||
### Configuration (server-side)
|
|
||||||
|
|
||||||
The noise blanker is configured per rig. In a multi-rig setup each
|
|
||||||
`[[rigs]]` entry has its own `[rigs.sdr.noise_blanker]` section:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[rigs]]
|
|
||||||
id = "hf"
|
|
||||||
|
|
||||||
[rigs.rig]
|
|
||||||
type = "sdr"
|
|
||||||
|
|
||||||
[rigs.sdr.noise_blanker]
|
|
||||||
enabled = true
|
|
||||||
threshold = 10.0 # 1 – 100; lower = more aggressive blanking
|
|
||||||
```
|
|
||||||
|
|
||||||
For the legacy single-rig (flat) config the path is `[sdr.noise_blanker]`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[sdr.noise_blanker]
|
|
||||||
enabled = true
|
|
||||||
threshold = 10.0
|
|
||||||
```
|
|
||||||
|
|
||||||
| Field | Type | Default | Range | Description |
|
|
||||||
|-------------|-------|---------|---------|-------------|
|
|
||||||
| `enabled` | bool | false | — | Turn the noise blanker on or off. |
|
|
||||||
| `threshold` | float | 10.0 | 1 – 100 | Multiplier applied to the running RMS. A sample whose magnitude exceeds this multiple is replaced. Lower values blank more aggressively; higher values only catch strong impulses. |
|
|
||||||
|
|
||||||
The noise blanker is off by default.
|
|
||||||
|
|
||||||
### Choosing a threshold
|
|
||||||
|
|
||||||
The threshold controls how aggressively the blanker suppresses impulses.
|
|
||||||
A value of **N** means: blank any sample whose magnitude exceeds **N times**
|
|
||||||
the running average signal level.
|
|
||||||
|
|
||||||
| Threshold | Behavior | Use case |
|
|
||||||
|-----------|----------|----------|
|
|
||||||
| 3 – 5 | Very aggressive — blanks frequently | Dense impulse noise (motors, power lines, LED drivers nearby) |
|
|
||||||
| 8 – 12 | Moderate — catches clear spikes without touching normal signals | Typical HF conditions with occasional ignition or switching noise |
|
|
||||||
| 15 – 25 | Conservative — only blanks strong impulses well above the noise floor | Light interference, or when you want minimal artifacts on weak signals |
|
|
||||||
| 30 – 100 | Very light — rarely triggers | Faint, infrequent clicks; mostly a safety net |
|
|
||||||
|
|
||||||
**Start at 10** (the default) and adjust while listening:
|
|
||||||
|
|
||||||
- If impulse noise is still audible, lower the threshold.
|
|
||||||
- If weak signals sound choppy or distorted, raise it — the blanker may be
|
|
||||||
mistaking signal peaks for noise.
|
|
||||||
- On bands with steady atmospheric noise (e.g. 160 m / 80 m), a threshold of
|
|
||||||
**5 – 8** usually works well.
|
|
||||||
- On quieter VHF/UHF bands where the noise floor is low, values of **15 – 25**
|
|
||||||
avoid false triggers from strong signals.
|
|
||||||
|
|
||||||
### Web UI
|
|
||||||
|
|
||||||
When the server reports noise-blanker support, two controls appear in the
|
|
||||||
**SDR Settings** row of the web interface:
|
|
||||||
|
|
||||||
- **Noise Blanker** checkbox — enables or disables the blanker in real time.
|
|
||||||
- **NB Threshold** number input (1–100) with a **Set** button — adjusts the
|
|
||||||
detection threshold. Press Enter or click Set to apply.
|
|
||||||
|
|
||||||
Both controls stay hidden until the server sends filter state containing NB
|
|
||||||
fields, so they only appear when connected to an SDR backend.
|
|
||||||
|
|
||||||
### HTTP API
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /set_sdr_noise_blanker?enabled=true&threshold=10
|
|
||||||
```
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-------------|--------|----------|-------------|
|
|
||||||
| `enabled` | bool | yes | `true` or `false` |
|
|
||||||
| `threshold` | float | yes | Value between 1 and 100 |
|
|
||||||
|
|
||||||
### How it works
|
|
||||||
|
|
||||||
The blanker runs on every IQ block (4096 samples) *before* the mixer stage in
|
|
||||||
the DSP pipeline:
|
|
||||||
|
|
||||||
1. For each sample, compute magnitude² (`re² + im²`).
|
|
||||||
2. Compare against `threshold² × mean_sq` (the exponentially-smoothed running
|
|
||||||
mean of magnitude²).
|
|
||||||
3. If the sample exceeds the threshold, replace it with the previous clean
|
|
||||||
sample.
|
|
||||||
4. Otherwise, update the running mean with smoothing factor α = 1/128 and store
|
|
||||||
the sample as the last clean value.
|
|
||||||
|
|
||||||
Because the blanker operates on raw IQ before frequency translation, it removes
|
|
||||||
impulse noise across the entire captured bandwidth regardless of the tuned
|
|
||||||
channel offset.
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
# Weather Satellite Map Overlay Integration
|
|
||||||
|
|
||||||
Overlay decoded NOAA APT and Meteor-M LRPT satellite images on the Leaflet
|
|
||||||
map module, with ground track visualisation and source filtering.
|
|
||||||
|
|
||||||
*Created: 2026-03-28*
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
| Step | Description | Status |
|
|
||||||
|------|-------------|--------|
|
|
||||||
| 1 | Add `sgp4` crate, create `trx-core/src/geo.rs` | Done |
|
|
||||||
| 2 | Extend `WxsatImage`/`LrptImage` with geo fields | Done |
|
|
||||||
| 3 | Compute geo-bounds in `finalize_wxsat_pass` / `finalize_lrpt_pass` | Done |
|
|
||||||
| 4 | Add `wxsat` to map source filter + image overlay rendering | Done |
|
|
||||||
| 5 | Add ground track polyline + filter toggle UI | Done |
|
|
||||||
| 6 | Build, test, verify | Done |
|
|
||||||
|
|
||||||
## Motivation
|
|
||||||
|
|
||||||
The wxsat plugin currently shows a history table with download links but has
|
|
||||||
no geographic context. Since the Map module already renders APRS, AIS, VDES,
|
|
||||||
and FTx/WSPR positions, weather satellite images are a natural addition — they
|
|
||||||
can be projected as semi-transparent overlays on the same Leaflet map.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Data flow
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A["Pass decoded (APT / LRPT)"] --> B["finalize_wxsat_pass / finalize_lrpt_pass<br/>(trx-server/audio.rs)"]
|
|
||||||
B --> C["SGP4 propagation using satellite TLE + pass timestamps"]
|
|
||||||
C --> D["Compute geo_bounds<br/>[[south, west], [north, east]]"]
|
|
||||||
D --> E["Compute ground_track<br/>[[lat, lon], ...]"]
|
|
||||||
E --> F["Attach to WxsatImage / LrptImage"]
|
|
||||||
F --> G["Broadcast via DecodedMessage"]
|
|
||||||
G --> H["SSE → browser"]
|
|
||||||
H --> I["wxsat.js: L.imageOverlay() + L.polyline() on aprsMap"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Geo-referencing strategy
|
|
||||||
|
|
||||||
Weather satellites (NOAA POES, Meteor-M) fly sun-synchronous polar orbits at
|
|
||||||
~850 km altitude with known TLE parameters. Given:
|
|
||||||
|
|
||||||
- **Satellite identity** (from telemetry: NOAA-15/18/19, Meteor-M N2-3/N2-4)
|
|
||||||
- **Pass start/end timestamps** (`pass_start_ms`, `pass_end_ms`)
|
|
||||||
- **Receiver station lat/lon** (from `RigState.server_latitude/longitude`)
|
|
||||||
|
|
||||||
We can use **SGP4 propagation** (via the `sgp4` crate) to compute the
|
|
||||||
sub-satellite ground track during the pass, then derive image bounds from the
|
|
||||||
known swath geometry:
|
|
||||||
|
|
||||||
| Parameter | NOAA APT | Meteor LRPT |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| Altitude | ~850 km | ~825 km |
|
|
||||||
| Swath width | ~2800 km | ~2800 km |
|
|
||||||
| Ground speed | ~6.9 km/s | ~6.9 km/s |
|
|
||||||
| Scan rate | 2 lines/sec (0.5s/line) | variable MCU rate |
|
|
||||||
| Image width | 909 px/channel | 1568 px |
|
|
||||||
|
|
||||||
**Bounds computation:**
|
|
||||||
1. Propagate satellite position at `pass_start_ms` and `pass_end_ms`
|
|
||||||
2. Sub-satellite points define the ground track center line
|
|
||||||
3. Swath half-width (~1400 km) gives east/west extent
|
|
||||||
4. Image is projected as a simple lat/lon rectangle (acceptable distortion
|
|
||||||
for the typical ~15° latitude span of a single pass)
|
|
||||||
|
|
||||||
**TLE source:** Hardcoded recent TLEs for the 5 active satellites, with an
|
|
||||||
optional HTTP refresh from CelesTrak. Stale TLEs (weeks old) still give
|
|
||||||
sub-degree accuracy for image overlay purposes.
|
|
||||||
|
|
||||||
### Crate changes
|
|
||||||
|
|
||||||
#### `trx-core` (src/trx-core/)
|
|
||||||
|
|
||||||
New module `src/trx-core/src/geo.rs`:
|
|
||||||
- `SatelliteGeo` struct: holds hardcoded TLEs, provides `compute_pass_bounds()`
|
|
||||||
- `PassGeoBounds { south: f64, west: f64, north: f64, east: f64 }`
|
|
||||||
- `ground_track(sat, start_ms, end_ms) -> Vec<[f64; 2]>`
|
|
||||||
- Uses `sgp4` crate for orbital propagation
|
|
||||||
- Falls back to station-centered approximation when TLE unavailable
|
|
||||||
|
|
||||||
`src/trx-core/src/decode.rs` — extend structs:
|
|
||||||
```rust
|
|
||||||
pub struct WxsatImage {
|
|
||||||
// ... existing fields ...
|
|
||||||
pub geo_bounds: Option<[f64; 4]>, // [south, west, north, east]
|
|
||||||
pub ground_track: Option<Vec<[f64; 2]>>, // [[lat, lon], ...]
|
|
||||||
}
|
|
||||||
// Same for LrptImage
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `trx-server` (src/trx-server/)
|
|
||||||
|
|
||||||
`src/trx-server/src/audio.rs`:
|
|
||||||
- In `finalize_wxsat_pass`: after PNG write, call `SatelliteGeo::compute_pass_bounds()`
|
|
||||||
using satellite name, pass timestamps, and station lat/lon (threaded through
|
|
||||||
from config). Attach result to `WxsatImage`.
|
|
||||||
- Same for `finalize_lrpt_pass`.
|
|
||||||
|
|
||||||
#### Frontend (trx-frontend-http/assets/web/)
|
|
||||||
|
|
||||||
`plugins/wxsat.js`:
|
|
||||||
- On `onServerWxsatImage` / `onServerLrptImage`: if `geo_bounds` present,
|
|
||||||
call `window.addWxsatMapOverlay(msg)`.
|
|
||||||
- Manage overlay list, allow removal.
|
|
||||||
|
|
||||||
`app.js`:
|
|
||||||
- Add `wxsat: false` to `DEFAULT_MAP_SOURCE_FILTER` (off by default to avoid
|
|
||||||
visual clutter; users opt-in).
|
|
||||||
- `window.addWxsatMapOverlay(msg)`: creates `L.imageOverlay(msg.path, bounds)`
|
|
||||||
with opacity 0.6, adds to `mapMarkers` set with `__trxType = "wxsat"`.
|
|
||||||
- `window.addWxsatGroundTrack(msg)`: creates `L.polyline(msg.ground_track)`
|
|
||||||
with dashed style.
|
|
||||||
- Overlay list in wxsat panel with per-image show/hide toggle.
|
|
||||||
|
|
||||||
`index.html`:
|
|
||||||
- No structural changes needed; the map filter chip system auto-generates
|
|
||||||
from `DEFAULT_MAP_SOURCE_FILTER`.
|
|
||||||
|
|
||||||
`style.css`:
|
|
||||||
- Styling for wxsat overlay opacity slider (future enhancement).
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
| Crate | Version | Purpose |
|
|
||||||
|-------|---------|---------|
|
|
||||||
| `sgp4` | 2.4 | Pure Rust SGP4 orbital propagation |
|
|
||||||
|
|
||||||
Added to `trx-core/Cargo.toml` (used by `geo.rs`).
|
|
||||||
|
|
||||||
## Risk / Limitations
|
|
||||||
|
|
||||||
- **Rectangular projection approximation**: The actual scan geometry is curved
|
|
||||||
(satellite moves along a great circle), but for a single pass spanning
|
|
||||||
~15-20° of latitude, a lat/lon rectangle is a reasonable first approximation.
|
|
||||||
More accurate warping could use `L.imageOverlay` with a canvas transform
|
|
||||||
in a future iteration.
|
|
||||||
|
|
||||||
- **TLE staleness**: Hardcoded TLEs drift ~0.1°/week. For overlay purposes
|
|
||||||
this is acceptable. A periodic CelesTrak fetch would keep them fresh.
|
|
||||||
|
|
||||||
- **Image rotation**: Ascending vs descending passes produce different
|
|
||||||
orientations. The initial implementation uses axis-aligned bounds
|
|
||||||
(no rotation). A rotated overlay would need `leaflet-imageoverlay-rotated`
|
|
||||||
or a canvas-based approach — deferred to a follow-up.
|
|
||||||
|
|
||||||
- **Image serving**: The `path` field is a filesystem path. On co-located
|
|
||||||
server/client setups this works directly. Remote setups may need an
|
|
||||||
image-serving endpoint (out of scope for this change).
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
# Frontend Styling & Performance Improvements
|
|
||||||
|
|
||||||
*Analysis date: 2026-04-01*
|
|
||||||
|
|
||||||
This document captures observations and improvement recommendations for the
|
|
||||||
trx-rs web frontend (`trx-frontend-http`). The frontend is a single-page
|
|
||||||
application served as embedded static assets (gzip-compressed with ETag
|
|
||||||
caching) from the Actix-Web server.
|
|
||||||
|
|
||||||
## Current asset inventory
|
|
||||||
|
|
||||||
| File | Lines | Size |
|
|
||||||
|------|------:|-----:|
|
|
||||||
| `style.css` | 5,318 | 144 KB |
|
|
||||||
| `app.js` | 8,427 | 306 KB |
|
|
||||||
| `map-core.js` | 3,483 | 127 KB |
|
|
||||||
| `screenshot.js` | 261 | 10 KB |
|
|
||||||
| `index.html` | 1,564 | 96 KB |
|
|
||||||
| `webgl-renderer.js` | 526 | 20 KB |
|
|
||||||
| `decode-history-worker.js` | 176 | 8 KB |
|
|
||||||
| `leaflet-ais-tracksymbol.js` | 120 | 8 KB |
|
|
||||||
| 15 plugin scripts | 7,360 | 304 KB |
|
|
||||||
| **Total** | **~27,000** | **~1 MB** |
|
|
||||||
|
|
||||||
All assets are pre-compressed with `flate2` (gzip, `Compression::best()`) and
|
|
||||||
served with `ETag` + `If-None-Match` support for conditional requests. The
|
|
||||||
Actix `Compress` middleware handles dynamic responses.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. CSS observations
|
|
||||||
|
|
||||||
### 1.1 Monolithic stylesheet (P1)
|
|
||||||
|
|
||||||
`style.css` is a single 5,318-line file covering every tab, theme, responsive
|
|
||||||
breakpoint, map overlay, decoder UI, scheduler, recorder, and settings panel.
|
|
||||||
Browsers must parse the entire stylesheet before first paint even though most
|
|
||||||
users only interact with 1-2 tabs at a time.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Split into logical partitions: `base.css` (variables, reset, layout), `tabs/*.css` (per-tab styles), `themes/*.css`. The server can concatenate and compress at build time.
|
|
||||||
- At minimum, move the theme colour blocks (lines 3770-5318, ~1,550 lines / 29% of the file) into a separate `themes.css` loaded asynchronously after initial paint, since the default theme is already in `:root`.
|
|
||||||
- Consider using `@layer` (CSS Cascade Layers) to manage specificity between base, component, and theme styles, eliminating the need for `!important` (currently 21 occurrences).
|
|
||||||
|
|
||||||
### 1.2 `backdrop-filter` overuse (P1)
|
|
||||||
|
|
||||||
There are 26 `backdrop-filter` declarations (13 pairs with `-webkit-` prefix).
|
|
||||||
`backdrop-filter: blur()` is one of the most expensive CSS properties -- it
|
|
||||||
forces the browser to composite, rasterize, and blur everything behind the
|
|
||||||
element on every frame.
|
|
||||||
|
|
||||||
Affected areas: tab bar, controls tray, frequency overlay, modals, connection
|
|
||||||
banner, bottom nav, neon-disco theme overlay.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Remove `backdrop-filter` from elements that are always opaque or rarely overlap dynamic content (e.g. bottom tab bar over static background).
|
|
||||||
- For the spectrum/waterfall overlay controls, use a solid semi-transparent `background` instead of blur -- the visual difference is negligible on a dark spectrogram.
|
|
||||||
- Where blur is desired (modals), use `will-change: backdrop-filter` and keep blur radius low (4-6px instead of 12-18px). Larger radii are proportionally more expensive.
|
|
||||||
- Gate expensive blur behind a `@media (prefers-reduced-motion: no-preference)` query or a `[data-effects="full"]` attribute so low-end devices can opt out.
|
|
||||||
|
|
||||||
### 1.3 `color-mix()` usage (P2)
|
|
||||||
|
|
||||||
184 occurrences of `color-mix(in srgb, ...)` throughout the stylesheet. While
|
|
||||||
`color-mix` is well-supported in modern browsers, each call is resolved at
|
|
||||||
computed-value time. Repeated identical mixes (e.g. button hover states
|
|
||||||
repeated across themes) add unnecessary style recalculation cost.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Pre-compute frequently used mixes as CSS custom properties in the theme blocks (e.g. `--btn-hover-bg`, `--btn-active-bg`).
|
|
||||||
- This reduces computed-value work and also makes the palette more explicit and maintainable.
|
|
||||||
|
|
||||||
### 1.4 Theme system duplication (P2)
|
|
||||||
|
|
||||||
Each of the 10 colour themes repeats ~28 variable declarations for both dark
|
|
||||||
and light mode (560 variable declarations total). The theme blocks span lines
|
|
||||||
3770-5318 (29% of the entire stylesheet).
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Move themes to a separate file loaded after first paint (the default `:root` theme is always available).
|
|
||||||
- Consider generating theme CSS from a data source (JSON/TOML) at build time to reduce manual duplication.
|
|
||||||
- Use `color-scheme` and `light-dark()` (CSS Color Level 5) to collapse the dark/light pairs where values differ only in lightness.
|
|
||||||
|
|
||||||
### 1.5 Transitions on non-essential properties (P3)
|
|
||||||
|
|
||||||
25 `transition` declarations, several targeting `background`, `border-color`,
|
|
||||||
and `box-shadow` simultaneously. Multi-property transitions on buttons and
|
|
||||||
inputs cause style recalculation on hover/focus for every such element.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Prefer transitioning only `opacity` and `transform` (GPU-composited).
|
|
||||||
- For colour changes, use `transition: background-color 100ms` rather than the shorthand `background` which also transitions `background-image` and other sub-properties.
|
|
||||||
- Add `will-change: transform` only to elements that are actively animating (currently only 2 occurrences, which is good).
|
|
||||||
|
|
||||||
### 1.6 Missing `contain` declarations (P2)
|
|
||||||
|
|
||||||
Tab content panels, decode history tables, map containers, and spectrum
|
|
||||||
canvases do not use CSS `contain` or `content-visibility`. When a large decode
|
|
||||||
history table updates, the browser recalculates layout for the entire page.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Add `contain: content` to inactive tab panels (`[data-tab]:not(.active)`).
|
|
||||||
- Add `content-visibility: auto` with `contain-intrinsic-size` to off-screen panels (decode history, map, statistics). This lets the browser skip rendering for hidden content entirely.
|
|
||||||
- Add `contain: strict` to the spectrum/waterfall canvas containers since their size is fixed and they don't affect sibling layout.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. JavaScript observations
|
|
||||||
|
|
||||||
### 2.1 Monolithic `app.js` (P1)
|
|
||||||
|
|
||||||
The main application script is 11,928 lines (428 KB uncompressed). It is loaded
|
|
||||||
synchronously in the HTML `<head>` (via embedded asset), blocking first paint
|
|
||||||
until fully parsed and executed. The 15 plugin scripts add another 7,360 lines.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Mark the script tag `defer` or move it to end of `<body>` so HTML parsing completes before script execution.
|
|
||||||
- Split `app.js` into logical modules: `core.js` (SSE, auth, render loop), `spectrum.js`, `map.js`, `decoder.js`, `recorder.js`, `settings.js`. Load non-critical modules lazily when the user navigates to the corresponding tab.
|
|
||||||
- Use ES modules (`type="module"`) for clean dependency management and tree-shaking potential.
|
|
||||||
|
|
||||||
### 2.2 DOM query overhead (P2)
|
|
||||||
|
|
||||||
The codebase contains ~359 `querySelector`/`getElementById` calls, many of
|
|
||||||
which execute on every SSE event (inside `render()`). DOM lookups are not free,
|
|
||||||
especially `querySelector` with compound selectors.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Cache DOM references at initialization time (many already are, but the render path still re-queries elements like `document.getElementById("tab-main")`).
|
|
||||||
- Move repeated lookups (e.g. line 3575 `document.getElementById("tab-main")` inside `es.onmessage`) to module-level constants.
|
|
||||||
|
|
||||||
### 2.3 `innerHTML` usage (P2)
|
|
||||||
|
|
||||||
33 `innerHTML` assignments in `app.js` and 72 across plugin scripts. Each
|
|
||||||
`innerHTML` write forces the browser to:
|
|
||||||
1. Serialize the old DOM subtree for GC
|
|
||||||
2. Parse the HTML string
|
|
||||||
3. Build and insert a new DOM subtree
|
|
||||||
|
|
||||||
This is both a performance concern (layout thrashing) and a security concern
|
|
||||||
(XSS if any user-controlled data is interpolated without escaping).
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Replace `innerHTML` with DOM APIs (`createElement`/`appendChild`) or `DocumentFragment` for bulk updates (only 4 `createDocumentFragment` uses currently).
|
|
||||||
- For large lists (decode history, bookmarks, recorder file lists), use a virtualised list pattern that only renders visible rows.
|
|
||||||
- Where `innerHTML` is used to clear a container, prefer `replaceChildren()` (clears children without HTML parsing).
|
|
||||||
|
|
||||||
### 2.4 SSE render path efficiency (P2)
|
|
||||||
|
|
||||||
Every SSE state event triggers `render(update)` which is a ~300-line function
|
|
||||||
touching dozens of DOM elements. The function does not diff -- it
|
|
||||||
unconditionally sets properties even when values have not changed.
|
|
||||||
|
|
||||||
The string-equality guard (`if (evt.data === lastRendered) return`) is a good
|
|
||||||
optimisation for identical payloads, but when any field changes (e.g. S-meter
|
|
||||||
value), the entire render function runs.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Implement field-level diffing: compare individual fields against previous values and only update DOM elements whose backing data changed.
|
|
||||||
- Group updates by tab: if the user is on the "Map" tab, skip render work for "Main" tab elements (meters, frequency display, controls).
|
|
||||||
- Use `scheduleUiFrameJob()` (already exists at line 3685) more aggressively to batch DOM writes into animation frames.
|
|
||||||
|
|
||||||
### 2.5 Spectrum/waterfall rendering (P2)
|
|
||||||
|
|
||||||
The WebGL renderer (`webgl-renderer.js`) is well-implemented with proper
|
|
||||||
shader programs and batched draws. However:
|
|
||||||
- The CSS colour parsing (`parseCssColor`) uses a DOM probe element (appended to
|
|
||||||
body) and `getComputedStyle` as a fallback, which triggers layout.
|
|
||||||
- The colour cache is a simple `Map` with no eviction policy.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Parse theme colours once when the theme changes, not on every frame.
|
|
||||||
- Invalidate the `cssColorCache` on theme switch events.
|
|
||||||
|
|
||||||
### 2.6 Plugin script loading (P3)
|
|
||||||
|
|
||||||
All 15 plugin scripts are loaded eagerly in `index.html` regardless of which
|
|
||||||
decoders are active. Plugins like `ais.js`, `vdes.js`, `sat.js`,
|
|
||||||
`sat-scheduler.js`, and `hf-aprs.js` are only relevant for specific use cases.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Load plugin scripts on demand when the corresponding decoder or feature is activated.
|
|
||||||
- Use dynamic `import()` if migrated to ES modules, or lazy `<script>` injection.
|
|
||||||
|
|
||||||
### 2.7 Web Worker utilisation (P3)
|
|
||||||
|
|
||||||
Only one Web Worker exists (`decode-history-worker.js`, 176 lines) for CBOR
|
|
||||||
decode-history parsing. All other heavy work (SSE parsing, DOM updates, spectrum
|
|
||||||
rendering, map marker management) runs on the main thread.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Move SSE JSON parsing to a shared worker so the main thread only receives pre-parsed objects.
|
|
||||||
- Offload spectrum FFT data processing / colour mapping to a worker, posting the resulting `ImageData` to the main thread for canvas rendering.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. HTML observations
|
|
||||||
|
|
||||||
### 3.1 CDN dependencies (P2)
|
|
||||||
|
|
||||||
The page loads one external resource at startup:
|
|
||||||
- `@fontsource/dseg14-classic/400.css` from `cdn.jsdelivr.net`
|
|
||||||
|
|
||||||
~~`leaflet@1.9.4` was previously loaded from `unpkg.com` but is now bundled
|
|
||||||
as a vendored asset (`/vendor/leaflet.{js,css}` + marker/layer images),
|
|
||||||
eliminating the CDN dependency.~~
|
|
||||||
|
|
||||||
The font uses `rel="preload" as="style"` with an `onload` trick to make it
|
|
||||||
non-blocking, which is good. However:
|
|
||||||
- If CDN is unreachable (offline/firewalled deployments common in ham radio),
|
|
||||||
the font never loads and the frequency display falls back to the system font.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Self-host the DSEG14 font as an embedded asset (it is small, ~30 KB woff2). This eliminates the CDN dependency entirely and ensures the frequency display always renders correctly.
|
|
||||||
|
|
||||||
### 3.2 Inline SVG icons (P3)
|
|
||||||
|
|
||||||
Tab bar icons are inline SVGs in the HTML (lines 35-63). Each icon is ~150-250
|
|
||||||
bytes of markup. This is acceptable for a small number of icons and avoids
|
|
||||||
extra HTTP requests, but the tab bar HTML is dense and hard to maintain.
|
|
||||||
|
|
||||||
**Recommendation:**
|
|
||||||
- Consider an SVG sprite sheet or moving icons to a small icon font to improve readability without extra requests.
|
|
||||||
|
|
||||||
### 3.3 HTML size (P2)
|
|
||||||
|
|
||||||
`index.html` is 1,564 lines (96 KB uncompressed). All tab content panels are
|
|
||||||
present in the initial HTML regardless of which tab is active.
|
|
||||||
|
|
||||||
**Recommendations:**
|
|
||||||
- Use `<template>` elements for tab panels that are not initially visible. Clone and insert them when the tab is first activated. This reduces initial DOM node count and speeds up first paint.
|
|
||||||
- The server already does template substitution (`{ver}` placeholders). Extend this to strip unused tab content for deployments that don't use certain features.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Responsive design observations
|
|
||||||
|
|
||||||
### 4.1 Breakpoints (P3)
|
|
||||||
|
|
||||||
Six responsive breakpoints are defined:
|
|
||||||
- `>1100px`: side bookmark panels
|
|
||||||
- `<1099px`: hide side bookmarks
|
|
||||||
- `<900px`: full-width card
|
|
||||||
- `<760px`: mobile layout (touch targets, stacked controls)
|
|
||||||
- `<640px`: bottom tab bar, mobile nav
|
|
||||||
- `<520px`: compact mobile
|
|
||||||
- `(hover: none) and (pointer: coarse)`: touch-specific
|
|
||||||
|
|
||||||
This is a well-structured responsive system. Minor improvements:
|
|
||||||
- Use `min-width` mobile-first instead of `max-width` desktop-first to reduce CSS specificity conflicts.
|
|
||||||
- Consider `container queries` for components like the controls tray and decode history table, so they respond to their container size rather than the viewport.
|
|
||||||
|
|
||||||
### 4.2 Touch target sizing (P3)
|
|
||||||
|
|
||||||
Mobile buttons get `min-height: 2.8rem` at `<760px`. The
|
|
||||||
`(hover: none) and (pointer: coarse)` media query adds additional touch
|
|
||||||
accommodations. This meets the 44px minimum recommended by WCAG.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Accessibility observations
|
|
||||||
|
|
||||||
### 5.1 `aria-live` regions (P1)
|
|
||||||
|
|
||||||
The connection-lost banner and power hint text update dynamically but were
|
|
||||||
flagged in the Settings-Menu-UX-Analysis as missing `aria-live` on toast
|
|
||||||
notifications. Ensuring all dynamic status text has `aria-live="polite"` or
|
|
||||||
`aria-live="assertive"` (for errors) is critical for screen reader users.
|
|
||||||
|
|
||||||
### 5.2 Keyboard navigation (P2)
|
|
||||||
|
|
||||||
The tab bar uses `<button>` elements (good, natively focusable). However, the
|
|
||||||
spectrum canvas, jog wheel, and map are mouse/touch-only without keyboard
|
|
||||||
equivalents. The Settings-Menu-UX-Analysis noted the timeline SVG is not
|
|
||||||
keyboard-operable.
|
|
||||||
|
|
||||||
### 5.3 Colour contrast (P2)
|
|
||||||
|
|
||||||
`--text-muted` values (`#91a3bd` on `#0f172a` for dark, `#4a5568` on `#ffffff`
|
|
||||||
for light) should be verified against WCAG AA (4.5:1 for normal text). The
|
|
||||||
dark theme muted text calculates to approximately 4.8:1 (passes), but some
|
|
||||||
theme variants (e.g. Neon Disco) may not meet contrast requirements.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Server-side delivery observations
|
|
||||||
|
|
||||||
### 6.1 Asset compression (already good)
|
|
||||||
|
|
||||||
Static assets are pre-compressed with `gzip` at `Compression::best()` level
|
|
||||||
and served with ETag headers. Conditional `304 Not Modified` responses avoid
|
|
||||||
re-transferring unchanged assets.
|
|
||||||
|
|
||||||
### 6.2 Missing `Cache-Control` headers (P2)
|
|
||||||
|
|
||||||
While ETags are present, the analysis did not find explicit `Cache-Control`
|
|
||||||
headers on static assets. Adding `Cache-Control: public, max-age=31536000,
|
|
||||||
immutable` for versioned assets (with cache-busting query strings) would
|
|
||||||
eliminate conditional requests entirely for repeat visits.
|
|
||||||
|
|
||||||
### 6.3 Consider Brotli compression (P3)
|
|
||||||
|
|
||||||
Brotli (`br`) typically achieves 15-25% better compression than gzip for text
|
|
||||||
assets. For a 428 KB `app.js`, this could save ~60-100 KB of transfer. Actix
|
|
||||||
supports Brotli via the `Compress` middleware.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Priority summary
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
quadrantChart
|
|
||||||
title Impact vs Effort
|
|
||||||
x-axis Low Effort --> High Effort
|
|
||||||
y-axis Low Impact --> High Impact
|
|
||||||
quadrant-1 Do next
|
|
||||||
quadrant-2 Plan carefully
|
|
||||||
quadrant-3 Low priority
|
|
||||||
quadrant-4 Quick wins
|
|
||||||
"backdrop-filter reduction": [0.25, 0.80]
|
|
||||||
"Cache-Control headers": [0.15, 0.55]
|
|
||||||
"CSS contain/content-visibility": [0.30, 0.70]
|
|
||||||
"Cache DOM refs in render": [0.20, 0.50]
|
|
||||||
"Theme CSS split": [0.35, 0.45]
|
|
||||||
"Self-host DSEG14 font": [0.20, 0.40]
|
|
||||||
"Field-level render diffing": [0.60, 0.75]
|
|
||||||
"Split app.js into modules": [0.80, 0.70]
|
|
||||||
"Lazy plugin loading": [0.50, 0.40]
|
|
||||||
"innerHTML to DOM APIs": [0.65, 0.55]
|
|
||||||
"Brotli compression": [0.30, 0.25]
|
|
||||||
"Template-based tab panels": [0.70, 0.60]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quick wins (low effort, high impact)
|
|
||||||
1. ~~Reduce `backdrop-filter` usage (13 blur instances)~~ **DONE** -- replaced with solid backgrounds, blur preserved for modals only, `prefers-reduced-motion` gate added
|
|
||||||
2. ~~Add `contain: content` / `content-visibility: auto` to inactive tabs~~ **DONE** -- containment added for inactive tabs, spectrum/waterfall containers, map, statistics
|
|
||||||
3. ~~Add `Cache-Control` headers to static assets~~ **DONE** -- upgraded to `public, max-age=31536000, immutable`
|
|
||||||
4. ~~Cache remaining DOM references in the render path~~ **DONE** -- `tabMainEl` and other hot-path refs cached at module level
|
|
||||||
|
|
||||||
### Next phase (moderate effort)
|
|
||||||
5. ~~Split theme CSS into a separate lazy-loaded file~~ **DONE** -- theme blocks extracted to `/themes.css`, lazy-loaded via `<link rel="preload">`
|
|
||||||
6. ~~Self-host DSEG14 font~~ **DONE** -- `@font-face` with `font-display: swap` added to `style.css`, CDN preconnect/preload removed from HTML
|
|
||||||
7. ~~Pre-compute `color-mix` results as CSS variables~~ **DONE** -- common mixes pre-computed as `--btn-hover-bg`, `--btn-active-bg`, etc.
|
|
||||||
8. ~~Field-level diffing in the SSE render function~~ **DONE** -- `prevRenderData` tracks freq/mode/ptt/meter, active-tab-aware skip logic added
|
|
||||||
9. ~~Replace `innerHTML` with DOM APIs in hot paths~~ **DONE** -- 15+ `innerHTML = ""` replaced with `replaceChildren()`
|
|
||||||
|
|
||||||
### Longer-term
|
|
||||||
10. ~~Split `app.js` into modules with lazy loading~~ **DONE** -- `map-core.js` (3,480 lines, map/stats/geo) and `screenshot.js` (260 lines) extracted as IIFE modules communicating via `window.trx` namespace; lazy-loaded on tab activation and on-demand respectively; `app.js` reduced from 11,967 to 8,420 lines (30% reduction)
|
|
||||||
11. ~~Lazy-load plugin scripts and Leaflet on demand~~ **DONE** -- plugin scripts loaded on tab activation, core plugins loaded immediately
|
|
||||||
12. ~~Use `<template>` elements for deferred tab content~~ **DONE** -- map, statistics, about tabs wrapped in `<template>`, cloned on first activation
|
|
||||||
13. ~~Migrate to Brotli compression~~ **DONE** -- Brotli added alongside gzip, preferred when `Accept-Encoding: br` present
|
|
||||||
14. Move SSE parsing and spectrum processing to Web Workers -- **DEFERRED** (requires SharedWorker + MessagePort plumbing, tracked separately)
|
|
||||||
|
|
||||||
### Additional improvements implemented
|
|
||||||
15. ~~Optimize CSS transitions~~ **DONE** -- `background` shorthand → `background-color` for GPU compositing
|
|
||||||
16. ~~Add `defer` to script tags~~ **DONE** -- all external script tags use `defer`
|
|
||||||
17. ~~SVG sprite sheet~~ **DONE** -- inline SVGs moved to `<symbol>` defs, referenced via `<use>`
|
|
||||||
18. ~~aria-live regions~~ **DONE** -- `aria-live` added to power hint, loading indicator
|
|
||||||
19. ~~Keyboard navigation~~ **DONE** -- `tabindex`/`role`/`aria-label` on spectrum/waterfall canvases
|
|
||||||
20. ~~Colour contrast~~ **DONE** -- dark theme `--text-muted` improved to `#9bb0ca`
|
|
||||||
21. ~~WebGL colour cache invalidation~~ **DONE** -- `trxClearCssColorCache()` called on theme switch
|
|
||||||
22. ~~Container queries~~ **DONE** -- controls tray and decode history table respond to container size
|
|
||||||
23. ~~Cache-Control immutable~~ **DONE** -- versioned assets use `immutable` directive
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
# Scheduler UI Improvement Plan
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
|
|
||||||
The scheduler UI lives in Settings → Scheduler and provides three operational modes:
|
|
||||||
|
|
||||||
- **Grayline** — auto-switches bookmarks based on solar dawn/day/dusk/night
|
|
||||||
- **Time Span** — UTC time windows with interleaved cycling
|
|
||||||
- **Satellite Pass** — priority overlay that retunes for satellite passes
|
|
||||||
|
|
||||||
Main-view controls include a release button, prev/next step buttons, and a
|
|
||||||
progress ring showing the active interleave entry and countdown.
|
|
||||||
|
|
||||||
Key files:
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `assets/web/plugins/scheduler.js` | UI logic, rendering, API calls (~1,060 LOC) |
|
|
||||||
| `assets/web/plugins/sat-scheduler.js` | Satellite config overlay (~310 LOC) |
|
|
||||||
| `assets/web/index.html` (L1109–1289) | Scheduler settings HTML |
|
|
||||||
| `assets/web/style.css` (`.sch-*`) | Scheduler styling |
|
|
||||||
| `src/scheduler.rs` | Backend task, API handlers (~1,435 LOC) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P0 — Usability Fixes
|
|
||||||
|
|
||||||
### 1. Highlight active entry in time-span table
|
|
||||||
|
|
||||||
**Problem:** The entry table under "Entry details" has no indication of which
|
|
||||||
entry the scheduler is currently operating on. Users must cross-reference the
|
|
||||||
interleave ring label with the table manually.
|
|
||||||
|
|
||||||
**Fix:** In `renderScheduler()`, after receiving status, add/remove an
|
|
||||||
`sch-active` class on the `<tr>` whose entry id matches
|
|
||||||
`currentSchedulerStatus.last_entry_id`. Style with a left border accent
|
|
||||||
(`border-left: 3px solid var(--accent)`).
|
|
||||||
|
|
||||||
### 2. Bookmark existence validation on save
|
|
||||||
|
|
||||||
**Problem:** If a bookmark is deleted after being assigned to a scheduler entry,
|
|
||||||
the scheduler fails silently at runtime — it tries to apply a non-existent
|
|
||||||
bookmark and does nothing.
|
|
||||||
|
|
||||||
**Fix:** In `saveScheduler()`, cross-check every `bookmark_id` /
|
|
||||||
`bookmark_ids[]` against `bookmarkList`. Show a toast error listing the
|
|
||||||
broken entries and refuse to save until corrected.
|
|
||||||
|
|
||||||
### 3. Dirty-state indicator for satellite section
|
|
||||||
|
|
||||||
**Problem:** Changes in the satellite section (add/edit/remove satellites,
|
|
||||||
toggle enable) don't reliably set `schedulerDirty`, so the Save button may
|
|
||||||
not appear.
|
|
||||||
|
|
||||||
**Fix:** Audit all satellite mutation paths in `sat-scheduler.js` and ensure
|
|
||||||
they call `window.schedulerBridge.markDirty()`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P1 — Information Density & Clarity
|
|
||||||
|
|
||||||
### 4. Show local time alongside UTC
|
|
||||||
|
|
||||||
**Problem:** All times are UTC-only. Operators in non-UTC timezones must
|
|
||||||
mentally convert, especially when editing time-span entries.
|
|
||||||
|
|
||||||
**Fix:** Add a `(local)` annotation next to each UTC time display:
|
|
||||||
- In the entry table, append a dimmed local-time column
|
|
||||||
- In the timeline SVG, add a secondary tick row with local hours
|
|
||||||
- Use `Intl.DateTimeFormat` to derive the offset; no config needed
|
|
||||||
|
|
||||||
### 5. Expand entry details by default
|
|
||||||
|
|
||||||
**Problem:** The entry list is hidden behind a `<details>` collapse. New
|
|
||||||
users don't discover it, and experienced users click it open every time.
|
|
||||||
|
|
||||||
**Fix:** Default the `<details>` element to `open`. Persist the
|
|
||||||
open/collapsed preference in `localStorage`.
|
|
||||||
|
|
||||||
### 6. Richer "Now Playing" status card
|
|
||||||
|
|
||||||
**Problem:** The status card shows only `"Last applied: {name} at {time}"` —
|
|
||||||
no frequency, mode, or decoder info.
|
|
||||||
|
|
||||||
**Fix:** Extend `SchedulerStatus` (backend) to include `freq_hz`, `mode`,
|
|
||||||
and `active_decoders[]`. Render them in the status card as
|
|
||||||
`"14.074 MHz · FT8 · FT8 decoder active"`. Adds immediate visibility
|
|
||||||
without opening the bookmark manager.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P2 — Interaction Improvements
|
|
||||||
|
|
||||||
### 7. Inline entry editing
|
|
||||||
|
|
||||||
**Problem:** Editing an entry requires clicking Edit, which opens an overlay
|
|
||||||
form that obscures the table. Users lose context of adjacent entries.
|
|
||||||
|
|
||||||
**Fix:** Replace the overlay with inline editing directly in the table row.
|
|
||||||
Clicking Edit on a row transforms its cells into input fields (time pickers,
|
|
||||||
selects) in-place. Save/Cancel buttons appear in the last column. This
|
|
||||||
keeps sibling entries visible and reduces clicks.
|
|
||||||
|
|
||||||
### 8. Drag-to-reorder entries
|
|
||||||
|
|
||||||
**Problem:** Entry order matters for interleave cycling, but there is no way
|
|
||||||
to reorder entries. Users must delete and re-add.
|
|
||||||
|
|
||||||
**Fix:** Add drag handles (`⠿`) to each table row. Implement HTML5 drag-and-drop
|
|
||||||
on the `<tbody>`. On drop, splice the `currentConfig.entries` array and
|
|
||||||
re-render. Mark dirty.
|
|
||||||
|
|
||||||
### 9. Timeline click-to-add
|
|
||||||
|
|
||||||
**Problem:** Adding an entry requires clicking "+ Add Entry" and manually
|
|
||||||
typing start/end times, even though the timeline is a visual 24-hour bar.
|
|
||||||
|
|
||||||
**Fix:** Make the timeline SVG interactive. Clicking on an empty region
|
|
||||||
opens the entry form pre-filled with the clicked hour as start and start+1h
|
|
||||||
as end. Dragging across a region sets start/end from the drag span. Use
|
|
||||||
`pointer-events` and `getBoundingClientRect()` to map pixel → minute.
|
|
||||||
|
|
||||||
### 10. Improved extra-channels management
|
|
||||||
|
|
||||||
**Problem:** Virtual channels use tiny `+`/`−` buttons with no indication
|
|
||||||
of which bookmarks are already added. Removing a channel requires clicking
|
|
||||||
`−` on the right one in a compact list.
|
|
||||||
|
|
||||||
**Fix:** Replace with a multi-select chip list: each added channel is a
|
|
||||||
removable chip (`× 40m FT8`). The `+` button opens the select dropdown.
|
|
||||||
Already-added bookmarks are disabled in the dropdown to prevent duplicates.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P3 — Feature Enhancements
|
|
||||||
|
|
||||||
### 11. Grayline location lookup by grid square
|
|
||||||
|
|
||||||
**Problem:** Users must manually enter latitude/longitude. Ham operators
|
|
||||||
typically know their Maidenhead grid square (e.g. `JO94`) but not their
|
|
||||||
coordinates to three decimals.
|
|
||||||
|
|
||||||
**Fix:** Add a text input for grid square next to the lat/lon fields. On
|
|
||||||
input, convert the grid square to lat/lon using the standard Maidenhead
|
|
||||||
algorithm (simple arithmetic, no external API). Populate lat/lon fields
|
|
||||||
automatically. Also support reverse: when lat/lon changes, show the
|
|
||||||
derived grid square.
|
|
||||||
|
|
||||||
### 12. Expanded satellite preset library
|
|
||||||
|
|
||||||
**Problem:** Only two satellite presets (Meteor-M2 3 and M2-4). Adding
|
|
||||||
NOAA, ISS, or amateur satellites requires looking up NORAD IDs externally.
|
|
||||||
|
|
||||||
**Fix:** Expand the preset `<option>` list to include common amateur /
|
|
||||||
weather satellites:
|
|
||||||
|
|
||||||
```
|
|
||||||
ISS (145.825 MHz APRS) — 25544
|
|
||||||
SO-50 (436.795 MHz FM) — 27607
|
|
||||||
```
|
|
||||||
|
|
||||||
Low-effort, high-value change — just HTML `<option>` additions plus
|
|
||||||
corresponding default bookmark templates.
|
|
||||||
|
|
||||||
### 13. Scheduler activity log
|
|
||||||
|
|
||||||
**Problem:** No way to see what the scheduler did historically — when it
|
|
||||||
switched, which bookmark it applied, whether any entry was skipped.
|
|
||||||
|
|
||||||
**Fix:**
|
|
||||||
- Backend: Add a ring buffer (last 100 events) to `SchedulerState`.
|
|
||||||
Each event: `{ utc, action: "applied"|"skipped"|"satellite_aos"|"satellite_los", entry_label, bookmark_name }`.
|
|
||||||
- API: `GET /scheduler/{rig_id}/log` returns the buffer.
|
|
||||||
- UI: Add a collapsible "Activity Log" section below the status card.
|
|
||||||
Render as a reverse-chronological compact list with timestamps.
|
|
||||||
|
|
||||||
### 14. Timeline interleave visualization
|
|
||||||
|
|
||||||
**Problem:** When multiple entries overlap, the timeline shows overlapping
|
|
||||||
colored bars but gives no indication of how interleaving splits time between
|
|
||||||
them.
|
|
||||||
|
|
||||||
**Fix:** When interleave is enabled and entries overlap, render alternating
|
|
||||||
color stripes within the overlap region (e.g., 5-minute tick marks colored
|
|
||||||
per-entry). Add a legend showing entry label → color mapping.
|
|
||||||
|
|
||||||
### 15. Keyboard shortcuts for scheduler control
|
|
||||||
|
|
||||||
**Problem:** Release/step controls require mouse clicks on the main view.
|
|
||||||
During operation, keyboard shortcuts would be faster.
|
|
||||||
|
|
||||||
**Fix:** Register global keybindings (configurable in settings):
|
|
||||||
- `Shift+R` — toggle release to scheduler
|
|
||||||
- `Shift+N` / `Shift+P` — step to next/previous entry
|
|
||||||
|
|
||||||
Guard with `!isInputFocused()` to avoid conflicts with text fields.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Order
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
gantt
|
|
||||||
title Scheduler UI Improvements
|
|
||||||
dateFormat X
|
|
||||||
axisFormat %s
|
|
||||||
|
|
||||||
section P0
|
|
||||||
Active entry highlight :1, 2
|
|
||||||
Bookmark validation :1, 2
|
|
||||||
Satellite dirty-state fix :1, 2
|
|
||||||
|
|
||||||
section P1
|
|
||||||
Local time display :3, 5
|
|
||||||
Expand details by default :3, 4
|
|
||||||
Richer status card :3, 5
|
|
||||||
|
|
||||||
section P2
|
|
||||||
Inline entry editing :6, 9
|
|
||||||
Drag-to-reorder :6, 8
|
|
||||||
Timeline click-to-add :6, 9
|
|
||||||
Extra-channels chips :6, 8
|
|
||||||
|
|
||||||
section P3
|
|
||||||
Grid square lookup :10, 11
|
|
||||||
Satellite presets :10, 11
|
|
||||||
Activity log :10, 13
|
|
||||||
Interleave visualization :10, 13
|
|
||||||
Keyboard shortcuts :10, 11
|
|
||||||
```
|
|
||||||
|
|
||||||
P0 items are small, targeted fixes (< 1 hour each). P1 items improve daily
|
|
||||||
usability. P2 items modernize interactions. P3 items add new capabilities.
|
|
||||||
Each item is independently shippable.
|
|
||||||
@@ -1,837 +0,0 @@
|
|||||||
# WEFAX / Radiofax Decoder Implementation Plan
|
|
||||||
|
|
||||||
> **Crate**: `trx-wefax` — `src/decoders/trx-wefax/`
|
|
||||||
> **Status**: Implemented (Phases 1–3b) — 2026-04-02
|
|
||||||
|
|
||||||
## 1. Overview
|
|
||||||
|
|
||||||
WEFAX (Weather Facsimile, ITU-T T.4 / WMO) is an analog image transmission
|
|
||||||
mode used by meteorological agencies worldwide (NOAA, DWD, JMH, etc.) on HF
|
|
||||||
and satellite downlinks. The decoder converts FM-modulated audio tones into
|
|
||||||
greyscale (or colour-composited) image lines.
|
|
||||||
|
|
||||||
### Goals
|
|
||||||
|
|
||||||
- Pure Rust, zero C FFI dependencies (matching project conventions).
|
|
||||||
- Multi-speed support: **60, 90, 120, 240 LPM** (lines per minute).
|
|
||||||
- Multi-IOC support: **288 and 576** (Index of Cooperation — defines
|
|
||||||
line pixel width).
|
|
||||||
- Automatic start/stop detection via APT tones.
|
|
||||||
- Phase-aligned line assembly from phasing signal.
|
|
||||||
- Incremental image output (line-by-line progress + final PNG).
|
|
||||||
- Follow existing decoder patterns (`process_block` / `decode_if_ready`).
|
|
||||||
|
|
||||||
## 2. WEFAX Signal Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
Carrier (1900 Hz center, ±400 Hz deviation)
|
|
||||||
Black = 1500 Hz
|
|
||||||
White = 2300 Hz
|
|
||||||
(linear mapping between frequency and luminance)
|
|
||||||
|
|
||||||
Transmission sequence:
|
|
||||||
┌─────────────┐
|
|
||||||
│ Start tone │ 300 Hz (5s) or 675 Hz (3s) — selects IOC 576 / 288
|
|
||||||
├─────────────┤
|
|
||||||
│ Phasing │ >95% white line + narrow black pulse — phase alignment
|
|
||||||
│ (30 lines) │
|
|
||||||
├─────────────┤
|
|
||||||
│ Image lines │ N lines at configured LPM
|
|
||||||
├─────────────┤
|
|
||||||
│ Stop tone │ 450 Hz (5s) — signals end of transmission
|
|
||||||
└─────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key parameters
|
|
||||||
|
|
||||||
| Parameter | IOC 576 | IOC 288 |
|
|
||||||
|-----------|---------|---------|
|
|
||||||
| Pixels per line | 1809 | 904 |
|
|
||||||
| Line duration (120 LPM) | 500 ms | 500 ms |
|
|
||||||
| Line duration (60 LPM) | 1000 ms | 1000 ms |
|
|
||||||
| Pixel clock | ~3618 px/s (120 LPM) | ~1808 px/s (120 LPM) |
|
|
||||||
|
|
||||||
Pixel count per line = `IOC × π` (rounded: 576×π ≈ 1809, 288×π ≈ 904).
|
|
||||||
|
|
||||||
## 3. Architecture
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
PCM["PCM audio (f32, 48 kHz)"] --> RS["Resampler (to internal rate)"]
|
|
||||||
RS --> FM["FM Discriminator"]
|
|
||||||
FM --> LPF["Low-pass filter (anti-alias)"]
|
|
||||||
LPF --> TD["Tone Detector (APT start/stop)"]
|
|
||||||
LPF --> PA["Phase Aligner"]
|
|
||||||
PA --> LS["Line Slicer"]
|
|
||||||
LS --> IMG["Image Assembler"]
|
|
||||||
IMG --> OUT["WefaxMessage (line / image)"]
|
|
||||||
TD --> SM["State Machine"]
|
|
||||||
SM -->|controls| PA
|
|
||||||
SM -->|controls| LS
|
|
||||||
```
|
|
||||||
|
|
||||||
### Internal sample rate
|
|
||||||
|
|
||||||
Resample input to **11,025 Hz** (sufficient for 2300 Hz max tone with
|
|
||||||
comfortable margin; matches common WEFAX decoder practice and keeps DSP
|
|
||||||
cost low).
|
|
||||||
|
|
||||||
## 4. Module Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
src/decoders/trx-wefax/
|
|
||||||
Cargo.toml
|
|
||||||
src/
|
|
||||||
lib.rs # Public API: WefaxDecoder, WefaxConfig, WefaxEvent
|
|
||||||
decoder.rs # Top-level decoder state machine + process_block/decode_if_ready
|
|
||||||
demod.rs # FM discriminator (instantaneous frequency from analytic signal)
|
|
||||||
tone_detect.rs # Goertzel-based APT tone detector (300/450/675 Hz)
|
|
||||||
phase.rs # Phasing signal detector and line-start alignment
|
|
||||||
line_slicer.rs # Pixel clock recovery, line buffer assembly
|
|
||||||
resampler.rs # Polyphase rational resampler (48k → 11025)
|
|
||||||
image.rs # Image buffer, PNG encoding, optional colour compositing
|
|
||||||
config.rs # WefaxConfig: speed, IOC, auto-detect, output path
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Core Types
|
|
||||||
|
|
||||||
### 5.1 Configuration
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct WefaxConfig {
|
|
||||||
/// Lines per minute: 60, 90, 120, 240. `None` = auto-detect from APT.
|
|
||||||
pub lpm: Option<u16>,
|
|
||||||
/// Index of Cooperation: 288 or 576. `None` = auto-detect from start tone.
|
|
||||||
pub ioc: Option<u16>,
|
|
||||||
/// Centre frequency of the FM subcarrier (default 1900 Hz).
|
|
||||||
pub center_freq_hz: f32,
|
|
||||||
/// Deviation (default ±400 Hz, so black=1500, white=2300).
|
|
||||||
pub deviation_hz: f32,
|
|
||||||
/// Directory for saving decoded images.
|
|
||||||
pub output_dir: Option<String>,
|
|
||||||
/// Whether to emit line-by-line progress events.
|
|
||||||
pub emit_progress: bool,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 Decoder state machine
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub enum WefaxState {
|
|
||||||
/// Listening for APT start tone.
|
|
||||||
Idle,
|
|
||||||
/// Start tone detected; waiting for phasing signal.
|
|
||||||
StartDetected { ioc: u16, tone_start_sample: u64 },
|
|
||||||
/// Receiving phasing lines; aligning line-start phase.
|
|
||||||
Phasing { ioc: u16, lpm: u16, phase_offset: Option<usize> },
|
|
||||||
/// Actively decoding image lines.
|
|
||||||
Receiving { ioc: u16, lpm: u16, line_number: u32 },
|
|
||||||
/// Stop tone detected; finalising image.
|
|
||||||
Stopping,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 Output messages (for `trx-core::DecodedMessage`)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// A complete or in-progress WEFAX image.
|
|
||||||
pub struct WefaxMessage {
|
|
||||||
pub rig_id: Option<String>,
|
|
||||||
pub ts_ms: Option<i64>,
|
|
||||||
/// Number of image lines decoded so far.
|
|
||||||
pub line_count: u32,
|
|
||||||
/// Detected or configured LPM.
|
|
||||||
pub lpm: u16,
|
|
||||||
/// Detected or configured IOC.
|
|
||||||
pub ioc: u16,
|
|
||||||
/// Pixels per line (IOC × π, rounded).
|
|
||||||
pub pixels_per_line: u16,
|
|
||||||
/// Filesystem path to saved PNG (set on completion).
|
|
||||||
pub path: Option<String>,
|
|
||||||
/// True when image is complete (stop tone received).
|
|
||||||
pub complete: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Progress update emitted every N lines during active reception.
|
|
||||||
pub struct WefaxProgress {
|
|
||||||
pub rig_id: Option<String>,
|
|
||||||
pub line_count: u32,
|
|
||||||
pub lpm: u16,
|
|
||||||
pub ioc: u16,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. DSP Pipeline Detail
|
|
||||||
|
|
||||||
### 6.1 Resampling
|
|
||||||
|
|
||||||
Rational polyphase resampler: 48000 → 11025 Hz (ratio 441/1920, simplified
|
|
||||||
from 11025/48000). Follow `docs/Optimization-Guidelines.md` polyphase
|
|
||||||
resampler guidance. Same pattern as FT8 decoder's 48k→12k resampler.
|
|
||||||
|
|
||||||
### 6.2 FM Discriminator
|
|
||||||
|
|
||||||
Compute instantaneous frequency from the analytic signal:
|
|
||||||
|
|
||||||
1. **Hilbert transform** (FIR, 65-tap) to produce analytic signal `z[n]`.
|
|
||||||
2. **Instantaneous frequency**: `f[n] = arg(z[n] · conj(z[n-1])) / (2π·Ts)`
|
|
||||||
3. Map frequency to luminance: `pixel = clamp((f - 1500) / 800, 0, 1)`.
|
|
||||||
|
|
||||||
The Hilbert + frequency discriminator approach avoids PLL complexity and works
|
|
||||||
well for the relatively low data rate of WEFAX.
|
|
||||||
|
|
||||||
### 6.3 APT Tone Detection
|
|
||||||
|
|
||||||
Use **Goertzel filters** at three frequencies (matching `trx-cw` pattern):
|
|
||||||
|
|
||||||
| Tone | Frequency | Meaning |
|
|
||||||
|------|-----------|---------|
|
|
||||||
| Start (IOC 576) | 300 Hz | Begin reception, IOC=576 |
|
|
||||||
| Start (IOC 288) | 675 Hz | Begin reception, IOC=288 |
|
|
||||||
| Stop | 450 Hz | End of transmission |
|
|
||||||
|
|
||||||
Detection window: ~200 ms (2205 samples at 11025 Hz). Require sustained
|
|
||||||
detection for ≥1.5 s to confirm (debounce against noise). Energy ratio
|
|
||||||
vs broadband noise for reliability.
|
|
||||||
|
|
||||||
### 6.4 Phasing Signal Detection
|
|
||||||
|
|
||||||
During phasing, each line is >95% white (2300 Hz) with a narrow black pulse
|
|
||||||
(~5% of line width) at the line-start position.
|
|
||||||
|
|
||||||
1. After start tone, begin accumulating demodulated samples.
|
|
||||||
2. Slice into line-duration windows (e.g., 500 ms for 120 LPM).
|
|
||||||
3. Cross-correlate against expected phasing template (short black pulse).
|
|
||||||
4. Average pulse position over 10+ phasing lines → line-start phase offset.
|
|
||||||
5. Transition to `Receiving` once phase is stable (variance < 2 samples).
|
|
||||||
|
|
||||||
### 6.5 Line Slicing and Pixel Clock
|
|
||||||
|
|
||||||
Once phased:
|
|
||||||
|
|
||||||
1. Accumulate demodulated (frequency → luminance) samples.
|
|
||||||
2. At each line boundary (determined by LPM and phase offset), extract
|
|
||||||
one line of `pixels_per_line` values via linear interpolation from
|
|
||||||
the sample buffer.
|
|
||||||
3. Push completed line into the image assembler.
|
|
||||||
4. Emit `WefaxProgress` every 50 lines (configurable).
|
|
||||||
|
|
||||||
### 6.6 Image Assembly
|
|
||||||
|
|
||||||
- Maintain a `Vec<Vec<u8>>` of greyscale lines (0–255).
|
|
||||||
- On stop tone or manual stop: encode to 8-bit greyscale PNG.
|
|
||||||
- Save to `output_dir` with filename pattern:
|
|
||||||
`WEFAX-{YYYY}-{MM}-{DD}T{HH}{mm}{ss}-IOC{ioc}-{lpm}lpm.png`
|
|
||||||
- Return `WefaxMessage` with `complete: true` and `path` set.
|
|
||||||
|
|
||||||
## 7. Integration with trx-rs
|
|
||||||
|
|
||||||
### 7.1 Workspace registration
|
|
||||||
|
|
||||||
Add to root `Cargo.toml` workspace members:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
"src/decoders/trx-wefax"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 `trx-core` changes
|
|
||||||
|
|
||||||
Add variants to `DecodedMessage`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[serde(rename = "wefax")]
|
|
||||||
Wefax(WefaxMessage),
|
|
||||||
#[serde(rename = "wefax_progress")]
|
|
||||||
WefaxProgress(WefaxProgress),
|
|
||||||
```
|
|
||||||
|
|
||||||
Update `set_rig_id()` / `rig_id()` match arms.
|
|
||||||
|
|
||||||
### 7.3 `trx-server` integration
|
|
||||||
|
|
||||||
Add `run_wefax_decoder()` in `audio.rs` following the existing pattern:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub async fn run_wefax_decoder(
|
|
||||||
sample_rate: u32,
|
|
||||||
channels: u16,
|
|
||||||
mut pcm_rx: broadcast::Receiver<Vec<f32>>,
|
|
||||||
state_rx: watch::Receiver<RigState>,
|
|
||||||
decode_tx: broadcast::Sender<DecodedMessage>,
|
|
||||||
logs: Option<Arc<DecoderLoggers>>,
|
|
||||||
histories: Arc<DecoderHistories>,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Spawn in `main.rs` alongside other decoders, gated by mode (USB/LSB on
|
|
||||||
HF WEFAX frequencies).
|
|
||||||
|
|
||||||
### 7.4 History and logging
|
|
||||||
|
|
||||||
- Add `wefax: Arc<Mutex<VecDeque<WefaxMessage>>>` to `DecoderHistories`.
|
|
||||||
- Add optional `wefax` logger to `DecoderLoggers` (JSON Lines).
|
|
||||||
|
|
||||||
### 7.5 Frontend exposure
|
|
||||||
|
|
||||||
The web frontend follows the existing decoder plugin pattern used by WSPR,
|
|
||||||
FT8, AIS, etc. WEFAX is unique among decoders because it produces **images**
|
|
||||||
rather than text rows, so the UI uses a `<canvas>` for live line-by-line
|
|
||||||
rendering instead of the tabular layout used by other decoders.
|
|
||||||
|
|
||||||
#### 7.5.1 Rust backend wiring (`trx-frontend-http`)
|
|
||||||
|
|
||||||
**`src/status.rs`** — embed the plugin script:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub const WEFAX_JS: &str = include_str!("../assets/web/plugins/wefax.js");
|
|
||||||
```
|
|
||||||
|
|
||||||
**`src/api/assets.rs`** — define the gzip-cached route:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
define_gz_cache!(gz_wefax_js, status::WEFAX_JS, "wefax.js");
|
|
||||||
|
|
||||||
#[get("/wefax.js")]
|
|
||||||
pub(crate) async fn wefax_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_wefax_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`src/api/decoder.rs`** — add endpoints:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[post("/toggle_wefax_decode")]
|
|
||||||
pub async fn toggle_wefax_decode(
|
|
||||||
query: web::Query<RemoteQuery>,
|
|
||||||
state: web::Data<watch::Receiver<RigState>>,
|
|
||||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let enabled = state.get_ref().borrow().decoders.wefax_decode_enabled;
|
|
||||||
send_command(
|
|
||||||
&rig_tx,
|
|
||||||
RigCommand::SetWefaxDecodeEnabled(!enabled),
|
|
||||||
query.into_inner().remote,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/clear_wefax_decode")]
|
|
||||||
pub async fn clear_wefax_decode(
|
|
||||||
query: web::Query<RemoteQuery>,
|
|
||||||
context: web::Data<Arc<FrontendRuntimeContext>>,
|
|
||||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
crate::server::audio::clear_wefax_history(context.get_ref());
|
|
||||||
send_command(
|
|
||||||
&rig_tx,
|
|
||||||
RigCommand::ResetWefaxDecoder,
|
|
||||||
query.into_inner().remote,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`src/api/mod.rs`** — register in `configure()`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
.service(decoder::toggle_wefax_decode)
|
|
||||||
.service(decoder::clear_wefax_decode)
|
|
||||||
.service(assets::wefax_js)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Decode history** — add `"wefax"` key to the CBOR payload returned
|
|
||||||
by `GET /decode/history`, containing `Vec<WefaxMessage>` (completed images
|
|
||||||
only; in-progress images are streamed via SSE).
|
|
||||||
|
|
||||||
**SSE `/decode` stream** — broadcast two event shapes:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"wefax_progress": {"line_count": 142, "lpm": 120, "ioc": 576, "pixels_per_line": 1809,
|
|
||||||
"line_data": "<base64-encoded u8 greyscale row>"}}
|
|
||||||
|
|
||||||
{"wefax": {"ts_ms": 1712000000000, "line_count": 800, "lpm": 120, "ioc": 576,
|
|
||||||
"pixels_per_line": 1809, "complete": true,
|
|
||||||
"path": "/images/WEFAX-2026-04-02T1430-IOC576-120lpm.png"}}
|
|
||||||
```
|
|
||||||
|
|
||||||
`wefax_progress` events carry a base64 `line_data` field (one image row of
|
|
||||||
greyscale bytes) so the browser can paint each line as it arrives without
|
|
||||||
needing a separate WebSocket channel.
|
|
||||||
|
|
||||||
**Decoder registry** — add entry to `DECODER_REGISTRY` in
|
|
||||||
`trx-protocol`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
DecoderRegistryEntry {
|
|
||||||
id: "wefax",
|
|
||||||
label: "WEFAX",
|
|
||||||
activation: "toggle", // enable/disable button
|
|
||||||
active_modes: &["usb", "lsb", "am"],
|
|
||||||
background_decode: false,
|
|
||||||
bookmark_selectable: true,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.5.2 HTML additions (`index.html`)
|
|
||||||
|
|
||||||
**Sub-tab button** (inside `.sub-tab-bar`, after the existing decoder
|
|
||||||
buttons):
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button class="sub-tab" data-subtab="wefax" id="subtab-wefax">WEFAX</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Sub-tab panel** (alongside other `sub-tab-panel` divs):
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="subtab-wefax" class="sub-tab-panel" style="display:none;">
|
|
||||||
<div class="ft8-controls">
|
|
||||||
<button id="wefax-decode-toggle-btn" type="button">Enable WEFAX</button>
|
|
||||||
<button id="wefax-clear-btn" type="button"
|
|
||||||
style="margin-left:0.5rem; font-size:0.8rem;">Clear</button>
|
|
||||||
<small id="wefax-status" style="color:var(--text-muted);">Idle</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Live image canvas — painted line-by-line during reception -->
|
|
||||||
<div id="wefax-live-container" style="display:none; margin:0.5rem 0;">
|
|
||||||
<div style="display:flex; align-items:center; gap:0.5rem; margin-bottom:0.3rem;">
|
|
||||||
<strong>Receiving</strong>
|
|
||||||
<small id="wefax-live-info" style="color:var(--text-muted);"></small>
|
|
||||||
</div>
|
|
||||||
<canvas id="wefax-live-canvas" width="1809" height="800"
|
|
||||||
style="width:100%; image-rendering:pixelated; background:#000;"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Gallery of completed images -->
|
|
||||||
<div id="wefax-gallery" style="display:flex; flex-wrap:wrap; gap:0.5rem;"></div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Overview section** (inside the digital-modes overview panel):
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="plugin-item" data-decoder="wefax">
|
|
||||||
<strong>WEFAX Decoder</strong>
|
|
||||||
<div style="color:var(--text-muted); font-size:0.85rem; margin-top:0.2rem;">
|
|
||||||
Weather Facsimile — HF/satellite image reception (60/90/120/240 LPM)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
**About section** (in the About tab decoder list):
|
|
||||||
|
|
||||||
```html
|
|
||||||
<tr id="about-dec-wefax"><td>WEFAX</td><td>Weather Facsimile decoder</td></tr>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.5.3 Plugin script registration
|
|
||||||
|
|
||||||
**`index.html` plugin map** — add `'/wefax.js'` to the
|
|
||||||
`'digital-modes'` array in `pluginScripts`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var pluginScripts = {
|
|
||||||
'digital-modes': ['/ft8.js', ..., '/wefax.js'],
|
|
||||||
// ...
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.5.4 SSE dispatch in `app.js`
|
|
||||||
|
|
||||||
Add WEFAX to the decode event dispatcher (inside `decodeSource.onmessage`):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
if (msg.wefax_progress && window.onServerWefaxProgress) {
|
|
||||||
window.onServerWefaxProgress(msg.wefax_progress);
|
|
||||||
}
|
|
||||||
if (msg.wefax && window.onServerWefax) {
|
|
||||||
window.onServerWefax(msg.wefax);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Add `"wefax"` to the decode history restore loop:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// In loadDecodeHistoryOnMainThread / worker dispatch:
|
|
||||||
const HISTORY_GROUP_KEYS = ["ais", "vdes", "aprs", "hf_aprs",
|
|
||||||
"cw", "ft8", "ft4", "ft2", "wspr", "wefax"];
|
|
||||||
```
|
|
||||||
|
|
||||||
Add WEFAX to `restoreDecodeHistoryGroup()`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
case "wefax":
|
|
||||||
if (window.restoreWefaxHistory) window.restoreWefaxHistory(messages);
|
|
||||||
break;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.5.5 Plugin file (`assets/web/plugins/wefax.js`)
|
|
||||||
|
|
||||||
Full plugin structure following the project's vanilla-JS decoder plugin
|
|
||||||
pattern:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// wefax.js — WEFAX decoder plugin for trx-frontend-http
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// --- DOM refs ---
|
|
||||||
const wefaxStatus = document.getElementById('wefax-status');
|
|
||||||
const wefaxLiveContainer= document.getElementById('wefax-live-container');
|
|
||||||
const wefaxLiveInfo = document.getElementById('wefax-live-info');
|
|
||||||
const wefaxLiveCanvas = document.getElementById('wefax-live-canvas');
|
|
||||||
const wefaxGallery = document.getElementById('wefax-gallery');
|
|
||||||
const wefaxToggleBtn = document.getElementById('wefax-decode-toggle-btn');
|
|
||||||
const wefaxClearBtn = document.getElementById('wefax-clear-btn');
|
|
||||||
|
|
||||||
// --- State ---
|
|
||||||
let wefaxImageHistory = []; // completed WefaxMessage objects
|
|
||||||
let wefaxLiveCtx = null; // canvas 2D context
|
|
||||||
let wefaxLiveLineCount = 0; // lines painted so far
|
|
||||||
let wefaxLivePixelsPerLine = 1809;
|
|
||||||
|
|
||||||
// --- Helpers ---
|
|
||||||
function currentWefaxHistoryRetentionMs() {
|
|
||||||
return window.getDecodeHistoryRetentionMs?.() || 24 * 60 * 60 * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pruneWefaxHistory() {
|
|
||||||
const cutoff = Date.now() - currentWefaxHistoryRetentionMs();
|
|
||||||
wefaxImageHistory = wefaxImageHistory.filter(m => (m._tsMs || 0) > cutoff);
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s)
|
|
||||||
.replaceAll('&', '&')
|
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>')
|
|
||||||
.replaceAll('"', '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Live canvas rendering ---
|
|
||||||
|
|
||||||
/** Reset canvas for a new image reception. */
|
|
||||||
function resetLiveCanvas(pixelsPerLine) {
|
|
||||||
wefaxLivePixelsPerLine = pixelsPerLine;
|
|
||||||
wefaxLiveLineCount = 0;
|
|
||||||
wefaxLiveCanvas.width = pixelsPerLine;
|
|
||||||
wefaxLiveCanvas.height = 800; // grows if needed
|
|
||||||
wefaxLiveCtx = wefaxLiveCanvas.getContext('2d');
|
|
||||||
wefaxLiveCtx.fillStyle = '#000';
|
|
||||||
wefaxLiveCtx.fillRect(0, 0, wefaxLiveCanvas.width, wefaxLiveCanvas.height);
|
|
||||||
wefaxLiveContainer.style.display = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Append one greyscale line (Uint8Array) to the live canvas. */
|
|
||||||
function paintLine(lineBytes) {
|
|
||||||
if (!wefaxLiveCtx) return;
|
|
||||||
const y = wefaxLiveLineCount;
|
|
||||||
|
|
||||||
// Grow canvas vertically if needed (double height strategy).
|
|
||||||
if (y >= wefaxLiveCanvas.height) {
|
|
||||||
const old = wefaxLiveCtx.getImageData(
|
|
||||||
0, 0, wefaxLiveCanvas.width, wefaxLiveCanvas.height);
|
|
||||||
wefaxLiveCanvas.height *= 2;
|
|
||||||
wefaxLiveCtx.putImageData(old, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const w = wefaxLivePixelsPerLine;
|
|
||||||
const imgData = wefaxLiveCtx.createImageData(w, 1);
|
|
||||||
const d = imgData.data;
|
|
||||||
for (let x = 0; x < w; x++) {
|
|
||||||
const v = x < lineBytes.length ? lineBytes[x] : 0;
|
|
||||||
const i = x * 4;
|
|
||||||
d[i] = v; d[i + 1] = v; d[i + 2] = v; d[i + 3] = 255;
|
|
||||||
}
|
|
||||||
wefaxLiveCtx.putImageData(imgData, 0, y);
|
|
||||||
wefaxLiveLineCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Gallery rendering ---
|
|
||||||
|
|
||||||
function renderGalleryThumbnail(msg) {
|
|
||||||
const card = document.createElement('div');
|
|
||||||
card.className = 'wefax-card';
|
|
||||||
card.style.cssText =
|
|
||||||
'border:1px solid var(--border-color); border-radius:4px; ' +
|
|
||||||
'padding:0.4rem; max-width:280px; cursor:pointer;';
|
|
||||||
|
|
||||||
const ts = msg._tsMs
|
|
||||||
? new Date(msg._tsMs).toLocaleString()
|
|
||||||
: '—';
|
|
||||||
const info = `${msg.ioc} IOC · ${msg.lpm} LPM · ${msg.line_count} lines`;
|
|
||||||
|
|
||||||
// If a server path is available, show a thumbnail linking to it.
|
|
||||||
if (msg.path) {
|
|
||||||
card.innerHTML =
|
|
||||||
`<img src="/images/${escapeHtml(msg.path.split('/').pop())}"
|
|
||||||
alt="WEFAX" loading="lazy"
|
|
||||||
style="width:100%; image-rendering:pixelated;" />` +
|
|
||||||
`<div style="font-size:0.8rem; margin-top:0.2rem;">${escapeHtml(ts)}</div>` +
|
|
||||||
`<div style="font-size:0.75rem; color:var(--text-muted);">${info}</div>`;
|
|
||||||
} else {
|
|
||||||
card.innerHTML =
|
|
||||||
`<div style="font-size:0.8rem;">${escapeHtml(ts)}</div>` +
|
|
||||||
`<div style="font-size:0.75rem; color:var(--text-muted);">${info}</div>`;
|
|
||||||
}
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWefaxGallery() {
|
|
||||||
pruneWefaxHistory();
|
|
||||||
const frag = document.createDocumentFragment();
|
|
||||||
for (const msg of wefaxImageHistory) {
|
|
||||||
frag.appendChild(renderGalleryThumbnail(msg));
|
|
||||||
}
|
|
||||||
wefaxGallery.innerHTML = '';
|
|
||||||
wefaxGallery.appendChild(frag);
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleWefaxGalleryRender() {
|
|
||||||
if (window.trxScheduleUiFrameJob) {
|
|
||||||
window.trxScheduleUiFrameJob('wefax-gallery', renderWefaxGallery);
|
|
||||||
} else {
|
|
||||||
requestAnimationFrame(renderWefaxGallery);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- SSE event handlers (public API) ---
|
|
||||||
|
|
||||||
/** Called for each wefax_progress SSE event (one image line). */
|
|
||||||
window.onServerWefaxProgress = function (msg) {
|
|
||||||
// First progress event of a new image → reset canvas.
|
|
||||||
if (msg.line_count <= 1 || !wefaxLiveCtx) {
|
|
||||||
resetLiveCanvas(msg.pixels_per_line || 1809);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode base64 line_data → Uint8Array → paint.
|
|
||||||
if (msg.line_data) {
|
|
||||||
const binary = atob(msg.line_data);
|
|
||||||
const bytes = new Uint8Array(binary.length);
|
|
||||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
||||||
paintLine(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update status text.
|
|
||||||
if (wefaxLiveInfo) {
|
|
||||||
wefaxLiveInfo.textContent =
|
|
||||||
`Line ${msg.line_count} · ${msg.ioc} IOC · ${msg.lpm} LPM`;
|
|
||||||
}
|
|
||||||
if (wefaxStatus) {
|
|
||||||
wefaxStatus.textContent = `Receiving — line ${msg.line_count}`;
|
|
||||||
wefaxStatus.style.color = 'var(--text-accent)';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Called when a complete WEFAX image is received. */
|
|
||||||
window.onServerWefax = function (msg) {
|
|
||||||
msg._tsMs = msg.ts_ms || Date.now();
|
|
||||||
wefaxImageHistory.unshift(msg);
|
|
||||||
pruneWefaxHistory();
|
|
||||||
scheduleWefaxGalleryRender();
|
|
||||||
|
|
||||||
// Finalise live canvas — trim height to actual line count.
|
|
||||||
if (wefaxLiveCtx && wefaxLiveLineCount > 0) {
|
|
||||||
const trimmed = wefaxLiveCtx.getImageData(
|
|
||||||
0, 0, wefaxLiveCanvas.width, wefaxLiveLineCount);
|
|
||||||
wefaxLiveCanvas.height = wefaxLiveLineCount;
|
|
||||||
wefaxLiveCtx.putImageData(trimmed, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wefaxStatus) {
|
|
||||||
wefaxStatus.textContent = `Complete — ${msg.line_count} lines`;
|
|
||||||
wefaxStatus.style.color = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Batch restore from decode history (page load). */
|
|
||||||
window.restoreWefaxHistory = function (messages) {
|
|
||||||
if (!messages || !messages.length) return;
|
|
||||||
for (const m of messages) {
|
|
||||||
m._tsMs = m.ts_ms || Date.now();
|
|
||||||
}
|
|
||||||
wefaxImageHistory = messages.concat(wefaxImageHistory);
|
|
||||||
pruneWefaxHistory();
|
|
||||||
scheduleWefaxGalleryRender();
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Called by history retention pruning cycle. */
|
|
||||||
window.pruneWefaxHistoryView = function () {
|
|
||||||
pruneWefaxHistory();
|
|
||||||
scheduleWefaxGalleryRender();
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Full reset (rig change, clear). */
|
|
||||||
window.resetWefaxHistoryView = function () {
|
|
||||||
wefaxImageHistory = [];
|
|
||||||
wefaxGallery.innerHTML = '';
|
|
||||||
wefaxLiveContainer.style.display = 'none';
|
|
||||||
wefaxLiveCtx = null;
|
|
||||||
wefaxLiveLineCount = 0;
|
|
||||||
if (wefaxStatus) {
|
|
||||||
wefaxStatus.textContent = 'Idle';
|
|
||||||
wefaxStatus.style.color = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Button handlers ---
|
|
||||||
if (wefaxClearBtn) {
|
|
||||||
wefaxClearBtn.addEventListener('click', function () {
|
|
||||||
fetch('/clear_wefax_decode', { method: 'POST' });
|
|
||||||
window.resetWefaxHistoryView();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.5.6 Data flow summary
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Server as trx-server (wefax decoder)
|
|
||||||
participant SSE as SSE /decode
|
|
||||||
participant Plugin as wefax.js
|
|
||||||
participant Canvas as <canvas>
|
|
||||||
participant Gallery as Gallery div
|
|
||||||
|
|
||||||
Server->>SSE: wefax_progress (line_data base64)
|
|
||||||
SSE->>Plugin: onServerWefaxProgress()
|
|
||||||
Plugin->>Canvas: paintLine() — one greyscale row
|
|
||||||
|
|
||||||
Note over Server: ...repeats per line...
|
|
||||||
|
|
||||||
Server->>SSE: wefax (complete=true, path)
|
|
||||||
SSE->>Plugin: onServerWefax()
|
|
||||||
Plugin->>Canvas: trim canvas to final height
|
|
||||||
Plugin->>Gallery: renderGalleryThumbnail()
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.5.7 Image serving
|
|
||||||
|
|
||||||
Completed PNG files saved by the decoder need an HTTP route for browser
|
|
||||||
access. Add a static-file route in `assets.rs`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[get("/images/{filename}")]
|
|
||||||
pub(crate) async fn wefax_image(
|
|
||||||
req: HttpRequest,
|
|
||||||
path: web::Path<String>,
|
|
||||||
) -> impl Responder {
|
|
||||||
// Serve from WefaxConfig::output_dir, validate filename (no path traversal).
|
|
||||||
// Content-Type: image/png, Cache-Control: public, max-age=86400.
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Register in `api/mod.rs`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
.service(assets::wefax_image)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.5.8 Decode history worker update
|
|
||||||
|
|
||||||
Add `"wefax"` to `HISTORY_GROUP_KEYS` in `decode-history-worker.js`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const HISTORY_GROUP_KEYS = [
|
|
||||||
"ais", "vdes", "aprs", "hf_aprs", "cw",
|
|
||||||
"ft8", "ft4", "ft2", "wspr", "wefax"
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
## 8. Implementation Phases
|
|
||||||
|
|
||||||
### Phase 1: Core DSP (MVP) ✅
|
|
||||||
|
|
||||||
1. ✅ **Resampler** — 48k→11025 polyphase resampler with tests.
|
|
||||||
2. ✅ **FM discriminator** — Hilbert FIR + instantaneous freq, verify
|
|
||||||
against synthetic 1500–2300 Hz sweeps.
|
|
||||||
3. ✅ **Tone detector** — Goertzel at 300/450/675 Hz with debounce.
|
|
||||||
4. ✅ **Line slicer** — Fixed-config (manual LPM+IOC) line extraction.
|
|
||||||
5. ✅ **Image buffer + PNG** — Greyscale line accumulation, `png`
|
|
||||||
crate for encoding.
|
|
||||||
|
|
||||||
Deliverable: decode a known WEFAX WAV recording at a single speed/IOC.
|
|
||||||
|
|
||||||
### Phase 2: Automatic Detection ✅
|
|
||||||
|
|
||||||
6. ✅ **State machine** — Full `Idle→StartDetected→Phasing→Receiving→Stopping`
|
|
||||||
transitions driven by tone detector.
|
|
||||||
7. ✅ **Phase alignment** — Cross-correlation phasing detector.
|
|
||||||
8. ✅ **Auto IOC/LPM** — IOC from start tone frequency; LPM from phasing
|
|
||||||
line duration measurement.
|
|
||||||
|
|
||||||
Deliverable: fully automatic reception of a single image without manual config.
|
|
||||||
|
|
||||||
### Phase 3: Server Integration ✅
|
|
||||||
|
|
||||||
9. ✅ **`trx-core` message types** — `WefaxMessage`, `WefaxProgress` in
|
|
||||||
`DecodedMessage`.
|
|
||||||
10. ✅ **`trx-server` task** — `run_wefax_decoder()`, history, logging.
|
|
||||||
11. ✅ **Protocol registry** — `DECODER_REGISTRY` entry for `"wefax"`.
|
|
||||||
|
|
||||||
Deliverable: backend wefax decoding with SSE event broadcast.
|
|
||||||
|
|
||||||
### Phase 3b: Frontend Wiring ✅
|
|
||||||
|
|
||||||
12. ✅ **Rust asset pipeline** — `status.rs` embed, `assets.rs` gzip
|
|
||||||
cache + route, `decoder.rs` toggle/clear endpoints, `api/mod.rs`
|
|
||||||
registration (§7.5.1).
|
|
||||||
13. ✅ **HTML scaffold** — sub-tab button, sub-tab panel with canvas +
|
|
||||||
gallery, overview entry, about row (§7.5.2).
|
|
||||||
14. ✅ **Plugin loading** — add `/wefax.js` to `pluginScripts`
|
|
||||||
`'digital-modes'` array (§7.5.3).
|
|
||||||
15. ✅ **SSE dispatch** — `wefax` / `wefax_progress` handlers in
|
|
||||||
`app.js` decode event dispatcher (§7.5.4).
|
|
||||||
16. ✅ **`wefax.js` plugin** — live canvas rendering, gallery
|
|
||||||
thumbnails, history restore, toggle/clear wiring (§7.5.5).
|
|
||||||
17. **Image serving** — `/images/{filename}` static route for
|
|
||||||
completed PNGs (§7.5.7). *(deferred: images served from output_dir)*
|
|
||||||
18. ✅ **History worker** — add `"wefax"` to `HISTORY_GROUP_KEYS`
|
|
||||||
(§7.5.8).
|
|
||||||
|
|
||||||
Deliverable: end-to-end live WEFAX decoding with in-browser image preview.
|
|
||||||
|
|
||||||
### Phase 4: Polish
|
|
||||||
|
|
||||||
19. **Multi-speed runtime switching** — handle back-to-back
|
|
||||||
transmissions at different LPM within one session.
|
|
||||||
20. **Slant correction** — fine-tune sample clock drift compensation
|
|
||||||
using phasing pulse tracking.
|
|
||||||
21. **Colour compositing** — optional IR + visible overlay for
|
|
||||||
satellite WEFAX (future).
|
|
||||||
22. **Test suite** — synthetic signal generation, round-trip tests,
|
|
||||||
edge cases (partial images, noise, frequency offset).
|
|
||||||
|
|
||||||
## 9. Dependencies
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
trx-core = { path = "../../trx-core" }
|
|
||||||
rustfft = "6" # Hilbert transform FIR via FFT overlap-save (optional)
|
|
||||||
png = "0.17" # PNG encoding (lightweight, no image full dep)
|
|
||||||
```
|
|
||||||
|
|
||||||
No additional heavy dependencies required. The DSP components (Goertzel,
|
|
||||||
polyphase resampler, Hilbert FIR) are small enough to implement inline,
|
|
||||||
consistent with the pure-Rust approach of `trx-rds`, `trx-cw`, and
|
|
||||||
`trx-ftx`.
|
|
||||||
|
|
||||||
## 10. Testing Strategy
|
|
||||||
|
|
||||||
| Test | Method |
|
|
||||||
|------|--------|
|
|
||||||
| FM discriminator accuracy | Synthesise known-frequency tones, verify ±1 Hz |
|
|
||||||
| Tone detection | Inject 300/450/675 Hz bursts, verify timing |
|
|
||||||
| Phase alignment | Synthetic phasing signal with known pulse position |
|
|
||||||
| Line pixel accuracy | Known gradient pattern → verify pixel values |
|
|
||||||
| Full decode round-trip | Reference WEFAX WAV → compare output PNG against known-good |
|
|
||||||
| Multi-speed switching | Sequential 120 LPM + 60 LPM images in one stream |
|
|
||||||
| Noise resilience | Add white noise at various SNR, verify graceful degradation |
|
|
||||||
|
|
||||||
## 11. References
|
|
||||||
|
|
||||||
- ITU-R BT.601 (facsimile signal characteristics)
|
|
||||||
- WMO Manual on the GTS, Attachment II-13 (HF radiofax schedule/format)
|
|
||||||
- NOAA Radiofax Charts: frequency schedules and IOC/LPM per product
|
|
||||||
- Existing open-source implementations: `fldigi` WEFAX module, `multimon-ng`
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "trx-plugin-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
trx-backend = { path = "../../src/trx-server/trx-backend" }
|
||||||
|
trx-core = { path = "../../src/trx-core" }
|
||||||
|
trx-frontend = { path = "../../src/trx-client/trx-frontend" }
|
||||||
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
tracing = { workspace = true }
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# trx-plugin-example
|
||||||
|
|
||||||
|
This is a minimal shared-library plugin that registers a backend and frontend.
|
||||||
|
The backend is a stub that returns an error; the frontend is a no-op spawner.
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build -p trx-plugin-example --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Install (example):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p plugins
|
||||||
|
cp target/release/libtrx_plugin_example.* plugins/
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `trx-server` or `trx-client` with `TRX_PLUGIN_DIRS=./plugins` to discover the plugin.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use tokio::sync::{mpsc, watch};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use trx_backend::{RegistrationContext, RigAccess};
|
||||||
|
use trx_core::{DynResult, RigRequest, RigState};
|
||||||
|
use trx_frontend::{FrontendRuntimeContext, FrontendSpawner, FrontendRegistrationContext};
|
||||||
|
|
||||||
|
const BACKEND_NAME: &str = "example";
|
||||||
|
const FRONTEND_NAME: &str = "example-frontend";
|
||||||
|
|
||||||
|
/// Entry point called by trx-server when the plugin is loaded.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn trx_register_backend(context: *mut std::ffi::c_void) {
|
||||||
|
let context = unsafe { &mut *(context as *mut RegistrationContext) };
|
||||||
|
context.register_backend(BACKEND_NAME, example_backend_factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entry point called by trx-client when the plugin is loaded.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn trx_register_frontend(context: *mut std::ffi::c_void) {
|
||||||
|
let context = unsafe { &mut *(context as *mut FrontendRegistrationContext) };
|
||||||
|
context.register_frontend(FRONTEND_NAME, ExampleFrontend::spawn_frontend);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn example_backend_factory(_access: RigAccess) -> DynResult<Box<dyn trx_core::rig::RigCat>> {
|
||||||
|
Err("example plugin backend not implemented".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExampleFrontend;
|
||||||
|
|
||||||
|
impl FrontendSpawner for ExampleFrontend {
|
||||||
|
fn spawn_frontend(
|
||||||
|
_state_rx: watch::Receiver<RigState>,
|
||||||
|
_rig_tx: mpsc::Sender<RigRequest>,
|
||||||
|
_callsign: Option<String>,
|
||||||
|
listen_addr: SocketAddr,
|
||||||
|
_context: std::sync::Arc<FrontendRuntimeContext>,
|
||||||
|
) -> JoinHandle<()> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
info!("example frontend loaded at {} (no-op)", listen_addr);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
BasedOnStyle: WebKit
|
||||||
|
# Cpp11BracedListStyle: false
|
||||||
|
# ColumnLimit: 120
|
||||||
|
IndentCaseLabels: false
|
||||||
|
IndentExternBlock: false
|
||||||
|
IndentWidth: 4
|
||||||
|
TabWidth: 8
|
||||||
|
UseTab: Never
|
||||||
|
PointerAlignment: Left
|
||||||
|
SortIncludes: false
|
||||||
|
AlignConsecutiveMacros: true
|
||||||
|
AllowShortBlocksOnASingleLine: false
|
||||||
|
AllowShortCaseLabelsOnASingleLine: false
|
||||||
|
AllowShortIfStatementsOnASingleLine: false
|
||||||
|
AllowShortLoopsOnASingleLine: false
|
||||||
|
AllowShortFunctionsOnASingleLine: false
|
||||||
|
AlignTrailingComments: true
|
||||||
|
BreakConstructorInitializers: BeforeColon
|
||||||
|
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||||
|
ConstructorInitializerIndentWidth: 0
|
||||||
|
BreakBeforeBraces: Custom
|
||||||
|
BreakBeforeBinaryOperators: All
|
||||||
|
BraceWrapping:
|
||||||
|
AfterControlStatement: true
|
||||||
|
AfterClass: true
|
||||||
|
AfterEnum: true
|
||||||
|
AfterFunction: true
|
||||||
|
AfterNamespace: true
|
||||||
|
AfterStruct: true
|
||||||
|
AfterUnion: true
|
||||||
|
AfterExternBlock: true
|
||||||
|
BeforeElse: true
|
||||||
|
BeforeCatch: true
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
gen_ft8
|
||||||
|
decode_ft8
|
||||||
|
test_ft8
|
||||||
|
libft8.a
|
||||||
|
wsjtx2/
|
||||||
|
.build/
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
__pycache__/
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Kārlis Goba
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
Vendored
+58
@@ -0,0 +1,58 @@
|
|||||||
|
BUILD_DIR = .build
|
||||||
|
|
||||||
|
FT8_SRC = $(wildcard ft8/*.c)
|
||||||
|
FT8_OBJ = $(patsubst %.c,$(BUILD_DIR)/%.o,$(FT8_SRC))
|
||||||
|
|
||||||
|
COMMON_SRC = $(wildcard common/*.c)
|
||||||
|
COMMON_OBJ = $(patsubst %.c,$(BUILD_DIR)/%.o,$(COMMON_SRC))
|
||||||
|
|
||||||
|
FFT_SRC = $(wildcard fft/*.c)
|
||||||
|
FFT_OBJ = $(patsubst %.c,$(BUILD_DIR)/%.o,$(FFT_SRC))
|
||||||
|
|
||||||
|
TARGETS = libft8.a gen_ft8 decode_ft8 test_ft8
|
||||||
|
|
||||||
|
ifdef FT8_DEBUG
|
||||||
|
CFLAGS = -fsanitize=address -ggdb3 -DHAVE_STPCPY -I. -DFTX_DEBUG_PRINT
|
||||||
|
LDFLAGS = -fsanitize=address -lm
|
||||||
|
else
|
||||||
|
CFLAGS = -O3 -DHAVE_STPCPY -I.
|
||||||
|
LDFLAGS = -lm
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Optionally, use Portaudio for live audio input
|
||||||
|
# Portaudio is a C++ library, so then you need to set CC=clang++ or CC=g++
|
||||||
|
ifdef PORTAUDIO_PREFIX
|
||||||
|
CFLAGS += -DUSE_PORTAUDIO -I$(PORTAUDIO_PREFIX)/include
|
||||||
|
LDFLAGS += -lportaudio -L$(PORTAUDIO_PREFIX)/lib
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: all clean run_tests install
|
||||||
|
|
||||||
|
all: $(TARGETS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILD_DIR) $(TARGETS)
|
||||||
|
|
||||||
|
run_tests: test_ft8
|
||||||
|
@./test_ft8
|
||||||
|
|
||||||
|
install: libft8.a
|
||||||
|
install libft8.a /usr/lib/libft8.a
|
||||||
|
|
||||||
|
gen_ft8: $(BUILD_DIR)/demo/gen_ft8.o libft8.a
|
||||||
|
$(CC) $(CFLAGS) -o $@ .build/demo/gen_ft8.o -lft8 -L. -lm
|
||||||
|
|
||||||
|
decode_ft8: $(BUILD_DIR)/demo/decode_ft8.o libft8.a $(FFT_OBJ)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $(BUILD_DIR)/demo/decode_ft8.o $(FFT_OBJ) -lft8 -L. -lm
|
||||||
|
|
||||||
|
test_ft8: $(BUILD_DIR)/test/test.o libft8.a
|
||||||
|
$(CC) $(CFLAGS) -o $@ .build/test/test.o -lft8 -L. -lm
|
||||||
|
|
||||||
|
$(BUILD_DIR)/%.o: %.c
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
$(CC) $(CFLAGS) -o $@ -c $^
|
||||||
|
|
||||||
|
lib: libft8.a
|
||||||
|
|
||||||
|
libft8.a: $(FT8_OBJ) $(COMMON_OBJ)
|
||||||
|
$(AR) rc libft8.a $(FT8_OBJ) $(COMMON_OBJ)
|
||||||
Vendored
+54
@@ -0,0 +1,54 @@
|
|||||||
|
# FT8 (and now FT4) library
|
||||||
|
|
||||||
|
C implementation of a lightweight FT8/FT4 decoder and encoder, mostly intended for experimental use on microcontrollers.
|
||||||
|
|
||||||
|
The intent of this library is to allow FT8/FT4 encoding and decoding in standalone environments (i.e. without a PC or RPi), e.g. automated beacons or SDR transceivers. It's also my learning process, optimization problem and source of fun.
|
||||||
|
|
||||||
|
The encoding process is relatively light on resources, and an Arduino should be perfectly capable of running this code.
|
||||||
|
|
||||||
|
The decoder is designed with memory and computing efficiency in mind, in order to be usable with a fast enough microcontroller. It is shown to be working on STM32F7 boards fast enough for real work, but the embedded application itself is beyond this repository. This repository provides an example decoder which can decode a 15-second WAV file on a desktop machine or SBC. The decoder needs to access the whole 15-second window in spectral magnitude representation (the window can be also shorter, and messages can have varying starting time within the window). The example FT8 decoder can work with slightly less than 200 KB of RAM.
|
||||||
|
|
||||||
|
# Current state
|
||||||
|
|
||||||
|
Currently the basic message set for establishing QSOs, as well as telemetry and free-text message modes are supported:
|
||||||
|
* CQ {call} {grid}, e.g. CQ CA0LL GG77
|
||||||
|
* CQ {xy} {call} {grid}, e.g. CQ JA CA0LL GG77
|
||||||
|
* {call} {call} {report}, e.g. CA0LL OT7ER R-07
|
||||||
|
* {call} {call} 73/RRR/RR73, e.g. OT7ER CA0LL 73
|
||||||
|
* Free-text messages (up to 13 characters from a limited alphabet) (decoding only, untested)
|
||||||
|
* Telemetry data (71 bits as 18 hex symbols)
|
||||||
|
|
||||||
|
Encoding and decoding works for both FT8 and FT4. For encoding and decoding, there is a console application provided for each, which serves mostly as test code, and could be a starting point for your potential application on an MCU. The console apps should run perfectly well on a RPi or a PC/Mac. I don't provide a concrete example for a particular MCU hardware here, since it would be very specific.
|
||||||
|
|
||||||
|
The code is not yet really a library, rather a collection of routines and example code.
|
||||||
|
|
||||||
|
# Future ideas
|
||||||
|
|
||||||
|
Incremental decoding (processing during the 15 second window) is something that I would like to explore, but haven't started.
|
||||||
|
|
||||||
|
These features are low on my priority list:
|
||||||
|
* Contest modes
|
||||||
|
* Compound callsigns with country prefixes and special callsigns
|
||||||
|
|
||||||
|
# What to do with it
|
||||||
|
|
||||||
|
You can generate 15-second WAV files with your own messages as a proof of concept or for testing purposes. They can either be played back or opened directly from WSJT-X. To do that, run ```make```. Then run ```gen_ft8``` (run it without parameters to check what parameters are supported). Currently messages are modulated at 1000-1050 Hz.
|
||||||
|
|
||||||
|
You can decode 15-second (or shorter) WAV files with ```decode_ft8```. This is only an example application and does not support live processing/recording. For that you could use third party code (PortAudio, for example).
|
||||||
|
|
||||||
|
# References and credits
|
||||||
|
|
||||||
|
Thanks goes out to:
|
||||||
|
* my contributors who have provided me with various improvements which have often been beyond my skill set.
|
||||||
|
* Robert Morris, AB1HL, whose Python code (https://github.com/rtmrtmrtmrtm/weakmon) inspired this and helped to test various parts of the code.
|
||||||
|
* Mark Borgerding for his FFT implementation (https://github.com/mborgerding/kissfft). I have included a portion of his code.
|
||||||
|
* WSJT-X authors, who developed a very interesting and novel communications protocol
|
||||||
|
|
||||||
|
The details of FT4 and FT8 procotols and decoding/encoding are described here: https://physics.princeton.edu/pulsar/k1jt/FT4_FT8_QEX.pdf
|
||||||
|
|
||||||
|
The public part of FT4/FT8 implementation is included in this repository under ft4_ft8_public.
|
||||||
|
|
||||||
|
Of course in moments of frustration I have looked up the original WSJT-X code, which is mostly written in Fortran (http://physics.princeton.edu/pulsar/K1JT/wsjtx.html). However, this library contains my own original DSP routines and a different implementation of the decoder which is suitable for resource-constrained embedded environments.
|
||||||
|
|
||||||
|
Karlis Goba,
|
||||||
|
YL3JG
|
||||||
Vendored
+191
@@ -0,0 +1,191 @@
|
|||||||
|
#include "audio.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifdef USE_PORTAUDIO
|
||||||
|
#include <portaudio.h>
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
PaStream* instream;
|
||||||
|
} audio_context_t;
|
||||||
|
|
||||||
|
static audio_context_t audio_context;
|
||||||
|
|
||||||
|
static int audio_cb(void* inputBuffer, void* outputBuffer, unsigned long framesPerBuffer,
|
||||||
|
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData)
|
||||||
|
{
|
||||||
|
audio_context_t* context = (audio_context_t*)userData;
|
||||||
|
float* samples_in = (float*)inputBuffer;
|
||||||
|
|
||||||
|
// PaTime time = data->startTime + timeInfo->inputBufferAdcTime;
|
||||||
|
printf("Callback with %ld samples\n", framesPerBuffer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio_list(void)
|
||||||
|
{
|
||||||
|
PaError pa_rc;
|
||||||
|
|
||||||
|
pa_rc = Pa_Initialize(); // Initialize PortAudio
|
||||||
|
if (pa_rc != paNoError)
|
||||||
|
{
|
||||||
|
printf("Error initializing PortAudio.\n");
|
||||||
|
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int numDevices;
|
||||||
|
numDevices = Pa_GetDeviceCount();
|
||||||
|
if (numDevices < 0)
|
||||||
|
{
|
||||||
|
printf("ERROR: Pa_CountDevices returned 0x%x\n", numDevices);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("%d audio devices found:\n", numDevices);
|
||||||
|
for (int i = 0; i < numDevices; i++)
|
||||||
|
{
|
||||||
|
const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i);
|
||||||
|
|
||||||
|
PaStreamParameters inputParameters = {
|
||||||
|
.device = i,
|
||||||
|
.channelCount = 1, // 1 = mono, 2 = stereo
|
||||||
|
.sampleFormat = paFloat32,
|
||||||
|
.suggestedLatency = 0.2,
|
||||||
|
.hostApiSpecificStreamInfo = NULL
|
||||||
|
};
|
||||||
|
double sample_rate = 12000; // sample rate (frames per second)
|
||||||
|
pa_rc = Pa_IsFormatSupported(&inputParameters, NULL, sample_rate);
|
||||||
|
|
||||||
|
printf("%d: [%s] [%s]\n", (i + 1), deviceInfo->name, (pa_rc == paNoError) ? "OK" : "NOT SUPPORTED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int audio_init(void)
|
||||||
|
{
|
||||||
|
PaError pa_rc;
|
||||||
|
|
||||||
|
pa_rc = Pa_Initialize(); // Initialize PortAudio
|
||||||
|
if (pa_rc != paNoError)
|
||||||
|
{
|
||||||
|
printf("Error initializing PortAudio.\n");
|
||||||
|
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
|
||||||
|
Pa_Terminate(); // I don't think we need this but...
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int audio_open(const char* name)
|
||||||
|
{
|
||||||
|
PaError pa_rc;
|
||||||
|
audio_context.instream = NULL;
|
||||||
|
|
||||||
|
PaDeviceIndex ndevice_in = -1;
|
||||||
|
int numDevices = Pa_GetDeviceCount();
|
||||||
|
for (int i = 0; i < numDevices; i++)
|
||||||
|
{
|
||||||
|
const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i);
|
||||||
|
if (0 == strcmp(deviceInfo->name, name))
|
||||||
|
{
|
||||||
|
ndevice_in = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ndevice_in < 0)
|
||||||
|
{
|
||||||
|
printf("Could not find device [%s].\n", name);
|
||||||
|
audio_list();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long nfpb = 1920 / 4; // frames per buffer
|
||||||
|
double sample_rate = 12000; // sample rate (frames per second)
|
||||||
|
|
||||||
|
PaStreamParameters inputParameters = {
|
||||||
|
.device = ndevice_in,
|
||||||
|
.channelCount = 1, // 1 = mono, 2 = stereo
|
||||||
|
.sampleFormat = paFloat32,
|
||||||
|
.suggestedLatency = 0.2,
|
||||||
|
.hostApiSpecificStreamInfo = NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test if this configuration actually works, so we do not run into an ugly assertion
|
||||||
|
pa_rc = Pa_IsFormatSupported(&inputParameters, NULL, sample_rate);
|
||||||
|
if (pa_rc != paNoError)
|
||||||
|
{
|
||||||
|
printf("Error opening input audio stream.\n");
|
||||||
|
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaStream* instream;
|
||||||
|
pa_rc = Pa_OpenStream(
|
||||||
|
&instream, // address of stream
|
||||||
|
&inputParameters,
|
||||||
|
NULL,
|
||||||
|
sample_rate, // Sample rate
|
||||||
|
nfpb, // Frames per buffer
|
||||||
|
paNoFlag,
|
||||||
|
NULL /*(PaStreamCallback*)audio_cb*/, // Callback routine
|
||||||
|
NULL /*(void*)&audio_context*/); // address of data structure
|
||||||
|
if (pa_rc != paNoError)
|
||||||
|
{ // We should have no error here usually
|
||||||
|
printf("Error opening input audio stream:\n");
|
||||||
|
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
// printf("Successfully opened audio input.\n");
|
||||||
|
|
||||||
|
pa_rc = Pa_StartStream(instream); // Start input stream
|
||||||
|
if (pa_rc != paNoError)
|
||||||
|
{
|
||||||
|
printf("Error starting input audio stream!\n");
|
||||||
|
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
|
||||||
|
return -4;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio_context.instream = instream;
|
||||||
|
|
||||||
|
// while (Pa_IsStreamActive(instream))
|
||||||
|
// {
|
||||||
|
// Pa_Sleep(100);
|
||||||
|
// }
|
||||||
|
// Pa_AbortStream(instream); // Abort stream
|
||||||
|
// Pa_CloseStream(instream); // Close stream, we're done.
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int audio_read(float* buffer, int num_samples)
|
||||||
|
{
|
||||||
|
PaError pa_rc;
|
||||||
|
pa_rc = Pa_ReadStream(audio_context.instream, (void*)buffer, num_samples);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
int audio_init(void)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio_list(void)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int audio_open(const char* name)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int audio_read(float* buffer, int num_samples)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
Vendored
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#ifndef _INCLUDE_AUDIO_H_
|
||||||
|
#define _INCLUDE_AUDIO_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int audio_init(void);
|
||||||
|
void audio_list(void);
|
||||||
|
int audio_open(const char* name);
|
||||||
|
int audio_read(float* buffer, int num_samples);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_AUDIO_H_
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#ifndef M_PI
|
||||||
|
#define M_PI 3.14159265358979323846
|
||||||
|
#endif
|
||||||
Vendored
+261
@@ -0,0 +1,261 @@
|
|||||||
|
#include "monitor.h"
|
||||||
|
#include <common/common.h>
|
||||||
|
|
||||||
|
#define LOG_LEVEL LOG_INFO
|
||||||
|
#include <ft8/debug.h>
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
static float hann_i(int i, int N)
|
||||||
|
{
|
||||||
|
float x = sinf((float)M_PI * i / N);
|
||||||
|
return x * x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// static float hamming_i(int i, int N)
|
||||||
|
// {
|
||||||
|
// const float a0 = (float)25 / 46;
|
||||||
|
// const float a1 = 1 - a0;
|
||||||
|
|
||||||
|
// float x1 = cosf(2 * (float)M_PI * i / N);
|
||||||
|
// return a0 - a1 * x1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// static float blackman_i(int i, int N)
|
||||||
|
// {
|
||||||
|
// const float alpha = 0.16f; // or 2860/18608
|
||||||
|
// const float a0 = (1 - alpha) / 2;
|
||||||
|
// const float a1 = 1.0f / 2;
|
||||||
|
// const float a2 = alpha / 2;
|
||||||
|
|
||||||
|
// float x1 = cosf(2 * (float)M_PI * i / N);
|
||||||
|
// float x2 = 2 * x1 * x1 - 1; // Use double angle formula
|
||||||
|
|
||||||
|
// return a0 - a1 * x1 + a2 * x2;
|
||||||
|
// }
|
||||||
|
|
||||||
|
static void waterfall_init(ftx_waterfall_t* me, int max_blocks, int num_bins, int time_osr, int freq_osr)
|
||||||
|
{
|
||||||
|
size_t mag_size = max_blocks * time_osr * freq_osr * num_bins * sizeof(me->mag[0]);
|
||||||
|
me->max_blocks = max_blocks;
|
||||||
|
me->num_blocks = 0;
|
||||||
|
me->num_bins = num_bins;
|
||||||
|
me->time_osr = time_osr;
|
||||||
|
me->freq_osr = freq_osr;
|
||||||
|
me->block_stride = (time_osr * freq_osr * num_bins);
|
||||||
|
me->mag = (WF_ELEM_T*)malloc(mag_size);
|
||||||
|
LOG(LOG_DEBUG, "Waterfall size = %zu\n", mag_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void waterfall_free(ftx_waterfall_t* me)
|
||||||
|
{
|
||||||
|
free(me->mag);
|
||||||
|
}
|
||||||
|
|
||||||
|
void monitor_init(monitor_t* me, const monitor_config_t* cfg)
|
||||||
|
{
|
||||||
|
float slot_time = ftx_protocol_slot_time(cfg->protocol);
|
||||||
|
float symbol_period = ftx_protocol_symbol_period(cfg->protocol);
|
||||||
|
// Compute DSP parameters that depend on the sample rate
|
||||||
|
me->block_size = (int)(cfg->sample_rate * symbol_period); // samples corresponding to one FSK symbol
|
||||||
|
me->subblock_size = me->block_size / cfg->time_osr;
|
||||||
|
me->nfft = me->block_size * cfg->freq_osr;
|
||||||
|
me->fft_norm = 2.0f / me->nfft;
|
||||||
|
// const int len_window = 1.8f * me->block_size; // hand-picked and optimized
|
||||||
|
|
||||||
|
me->window = (float*)malloc(me->nfft * sizeof(me->window[0]));
|
||||||
|
for (int i = 0; i < me->nfft; ++i)
|
||||||
|
{
|
||||||
|
// window[i] = 1;
|
||||||
|
me->window[i] = me->fft_norm * hann_i(i, me->nfft);
|
||||||
|
// me->window[i] = blackman_i(i, me->nfft);
|
||||||
|
// me->window[i] = hamming_i(i, me->nfft);
|
||||||
|
// me->window[i] = (i < len_window) ? hann_i(i, len_window) : 0;
|
||||||
|
}
|
||||||
|
me->last_frame = (float*)calloc(me->nfft, sizeof(me->last_frame[0]));
|
||||||
|
|
||||||
|
LOG(LOG_INFO, "Block size = %d\n", me->block_size);
|
||||||
|
LOG(LOG_INFO, "Subblock size = %d\n", me->subblock_size);
|
||||||
|
|
||||||
|
size_t fft_work_size = 0;
|
||||||
|
kiss_fftr_alloc(me->nfft, 0, 0, &fft_work_size);
|
||||||
|
me->fft_work = malloc(fft_work_size);
|
||||||
|
me->fft_cfg = kiss_fftr_alloc(me->nfft, 0, me->fft_work, &fft_work_size);
|
||||||
|
|
||||||
|
LOG(LOG_INFO, "N_FFT = %d\n", me->nfft);
|
||||||
|
LOG(LOG_DEBUG, "FFT work area = %zu\n", fft_work_size);
|
||||||
|
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
me->nifft = 64; // Gives 200 Hz sample rate for FT8 (160ms symbol period)
|
||||||
|
|
||||||
|
size_t ifft_work_size = 0;
|
||||||
|
kiss_fft_alloc(me->nifft, 1, 0, &ifft_work_size);
|
||||||
|
me->ifft_work = malloc(ifft_work_size);
|
||||||
|
me->ifft_cfg = kiss_fft_alloc(me->nifft, 1, me->ifft_work, &ifft_work_size);
|
||||||
|
|
||||||
|
LOG(LOG_INFO, "N_iFFT = %d\n", me->nifft);
|
||||||
|
LOG(LOG_DEBUG, "iFFT work area = %zu\n", ifft_work_size);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Allocate enough blocks to fit the entire FT8/FT4 slot in memory
|
||||||
|
const int max_blocks = (int)(slot_time / symbol_period);
|
||||||
|
// Keep only FFT bins in the specified frequency range (f_min/f_max)
|
||||||
|
me->min_bin = (int)(cfg->f_min * symbol_period);
|
||||||
|
me->max_bin = (int)(cfg->f_max * symbol_period) + 1;
|
||||||
|
const int num_bins = me->max_bin - me->min_bin;
|
||||||
|
|
||||||
|
waterfall_init(&me->wf, max_blocks, num_bins, cfg->time_osr, cfg->freq_osr);
|
||||||
|
me->wf.protocol = cfg->protocol;
|
||||||
|
|
||||||
|
me->symbol_period = symbol_period;
|
||||||
|
|
||||||
|
me->max_mag = -120.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void monitor_free(monitor_t* me)
|
||||||
|
{
|
||||||
|
waterfall_free(&me->wf);
|
||||||
|
free(me->fft_work);
|
||||||
|
free(me->last_frame);
|
||||||
|
free(me->window);
|
||||||
|
}
|
||||||
|
|
||||||
|
void monitor_reset(monitor_t* me)
|
||||||
|
{
|
||||||
|
me->wf.num_blocks = 0;
|
||||||
|
me->max_mag = -120.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute FFT magnitudes (log wf) for a frame in the signal and update waterfall data
|
||||||
|
void monitor_process(monitor_t* me, const float* frame)
|
||||||
|
{
|
||||||
|
// Check if we can still store more waterfall data
|
||||||
|
if (me->wf.num_blocks >= me->wf.max_blocks)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int offset = me->wf.num_blocks * me->wf.block_stride;
|
||||||
|
int frame_pos = 0;
|
||||||
|
|
||||||
|
// Loop over block subdivisions
|
||||||
|
for (int time_sub = 0; time_sub < me->wf.time_osr; ++time_sub)
|
||||||
|
{
|
||||||
|
kiss_fft_scalar timedata[me->nfft];
|
||||||
|
kiss_fft_cpx freqdata[me->nfft / 2 + 1];
|
||||||
|
|
||||||
|
// Shift the new data into analysis frame
|
||||||
|
for (int pos = 0; pos < me->nfft - me->subblock_size; ++pos)
|
||||||
|
{
|
||||||
|
me->last_frame[pos] = me->last_frame[pos + me->subblock_size];
|
||||||
|
}
|
||||||
|
for (int pos = me->nfft - me->subblock_size; pos < me->nfft; ++pos)
|
||||||
|
{
|
||||||
|
me->last_frame[pos] = frame[frame_pos];
|
||||||
|
++frame_pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do DFT of windowed analysis frame
|
||||||
|
for (int pos = 0; pos < me->nfft; ++pos)
|
||||||
|
{
|
||||||
|
timedata[pos] = me->window[pos] * me->last_frame[pos];
|
||||||
|
}
|
||||||
|
kiss_fftr(me->fft_cfg, timedata, freqdata);
|
||||||
|
|
||||||
|
// Loop over possible frequency OSR offsets
|
||||||
|
for (int freq_sub = 0; freq_sub < me->wf.freq_osr; ++freq_sub)
|
||||||
|
{
|
||||||
|
for (int bin = me->min_bin; bin < me->max_bin; ++bin)
|
||||||
|
{
|
||||||
|
int src_bin = (bin * me->wf.freq_osr) + freq_sub;
|
||||||
|
float mag2 = (freqdata[src_bin].i * freqdata[src_bin].i) + (freqdata[src_bin].r * freqdata[src_bin].r);
|
||||||
|
float db = 10.0f * log10f(1E-12f + mag2);
|
||||||
|
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
// Save the magnitude in dB and phase in radians
|
||||||
|
float phase = atan2f(freqdata[src_bin].i, freqdata[src_bin].r);
|
||||||
|
me->wf.mag[offset].mag = db;
|
||||||
|
me->wf.mag[offset].phase = phase;
|
||||||
|
#else
|
||||||
|
// Scale decibels to unsigned 8-bit range and clamp the value
|
||||||
|
// Range 0-240 covers -120..0 dB in 0.5 dB steps
|
||||||
|
int scaled = (int)(2 * db + 240);
|
||||||
|
me->wf.mag[offset] = (scaled < 0) ? 0 : ((scaled > 255) ? 255 : scaled);
|
||||||
|
#endif
|
||||||
|
++offset;
|
||||||
|
|
||||||
|
if (db > me->max_mag)
|
||||||
|
me->max_mag = db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
++me->wf.num_blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
void monitor_resynth(const monitor_t* me, const ftx_candidate_t* candidate, float* signal)
|
||||||
|
{
|
||||||
|
const int num_ifft = me->nifft;
|
||||||
|
const int num_shift = num_ifft / 2;
|
||||||
|
const int taper_width = 4;
|
||||||
|
// Starting offset is 3 subblocks due to analysis buffer loading
|
||||||
|
int offset = 1; // candidate->time_offset;
|
||||||
|
offset = (offset * me->wf.time_osr) + 1; // + candidate->time_sub;
|
||||||
|
offset = (offset * me->wf.freq_osr); // + candidate->freq_sub;
|
||||||
|
offset = (offset * me->wf.num_bins); // + candidate->freq_offset;
|
||||||
|
|
||||||
|
WF_ELEM_T* el = me->wf.mag + offset;
|
||||||
|
|
||||||
|
// DFT frequency data - initialize to zero
|
||||||
|
kiss_fft_cpx freqdata[num_ifft];
|
||||||
|
for (int i = 0; i < num_ifft; ++i)
|
||||||
|
{
|
||||||
|
freqdata[i].r = 0;
|
||||||
|
freqdata[i].i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pos = 0;
|
||||||
|
for (int num_block = 1; num_block < me->wf.num_blocks; ++num_block)
|
||||||
|
{
|
||||||
|
// Extract frequency data around the selected candidate only
|
||||||
|
for (int i = candidate->freq_offset - taper_width - 1; i < candidate->freq_offset + 8 + taper_width - 1; ++i)
|
||||||
|
{
|
||||||
|
if ((i >= 0) && (i < me->wf.num_bins))
|
||||||
|
{
|
||||||
|
int tgt_bin = (me->wf.freq_osr * (i - candidate->freq_offset) + num_ifft) % num_ifft;
|
||||||
|
float weight = 1.0f;
|
||||||
|
if (i < candidate->freq_offset)
|
||||||
|
{
|
||||||
|
weight = ((i - candidate->freq_offset) + taper_width) / (float)taper_width;
|
||||||
|
}
|
||||||
|
else if (i > candidate->freq_offset + 7)
|
||||||
|
{
|
||||||
|
weight = ((candidate->freq_offset + 7 - i) + taper_width) / (float)taper_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert (dB magnitude, phase) to (real, imaginary)
|
||||||
|
float mag = powf(10.0f, el[i].mag / 20) / 2 * weight;
|
||||||
|
freqdata[tgt_bin].r = mag * cosf(el[i].phase);
|
||||||
|
freqdata[tgt_bin].i = mag * sinf(el[i].phase);
|
||||||
|
|
||||||
|
int i2 = i + me->wf.num_bins;
|
||||||
|
tgt_bin = (tgt_bin + 1) % num_ifft;
|
||||||
|
float mag2 = powf(10.0f, el[i2].mag / 20) / 2 * weight;
|
||||||
|
freqdata[tgt_bin].r = mag2 * cosf(el[i2].phase);
|
||||||
|
freqdata[tgt_bin].i = mag2 * sinf(el[i2].phase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute inverse DFT and overlap-add the waveform
|
||||||
|
kiss_fft_cpx timedata[num_ifft];
|
||||||
|
kiss_fft(me->ifft_cfg, freqdata, timedata);
|
||||||
|
for (int i = 0; i < num_ifft; ++i)
|
||||||
|
{
|
||||||
|
signal[pos + i] += timedata[i].i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the next symbol
|
||||||
|
el += me->wf.block_stride;
|
||||||
|
pos += num_shift;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Vendored
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#ifndef _INCLUDE_MONITOR_H_
|
||||||
|
#define _INCLUDE_MONITOR_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <ft8/decode.h>
|
||||||
|
#include <fft/kiss_fftr.h>
|
||||||
|
|
||||||
|
/// Configuration options for FT4/FT8 monitor
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
float f_min; ///< Lower frequency bound for analysis
|
||||||
|
float f_max; ///< Upper frequency bound for analysis
|
||||||
|
int sample_rate; ///< Sample rate in Hertz
|
||||||
|
int time_osr; ///< Number of time subdivisions
|
||||||
|
int freq_osr; ///< Number of frequency subdivisions
|
||||||
|
ftx_protocol_t protocol; ///< Protocol: FT4 or FT8
|
||||||
|
} monitor_config_t;
|
||||||
|
|
||||||
|
/// FT4/FT8 monitor object that manages DSP processing of incoming audio data
|
||||||
|
/// and prepares a waterfall object
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
float symbol_period; ///< FT4/FT8 symbol period in seconds
|
||||||
|
int min_bin; ///< First FFT bin in the frequency range (begin)
|
||||||
|
int max_bin; ///< First FFT bin outside the frequency range (end)
|
||||||
|
int block_size; ///< Number of samples per symbol (block)
|
||||||
|
int subblock_size; ///< Analysis shift size (number of samples)
|
||||||
|
int nfft; ///< FFT size
|
||||||
|
float fft_norm; ///< FFT normalization factor
|
||||||
|
float* window; ///< Window function for STFT analysis (nfft samples)
|
||||||
|
float* last_frame; ///< Current STFT analysis frame (nfft samples)
|
||||||
|
ftx_waterfall_t wf; ///< Waterfall object
|
||||||
|
float max_mag; ///< Maximum detected magnitude (debug stats)
|
||||||
|
|
||||||
|
// KISS FFT housekeeping variables
|
||||||
|
void* fft_work; ///< Work area required by Kiss FFT
|
||||||
|
kiss_fftr_cfg fft_cfg; ///< Kiss FFT housekeeping object
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
int nifft; ///< iFFT size
|
||||||
|
void* ifft_work; ///< Work area required by inverse Kiss FFT
|
||||||
|
kiss_fft_cfg ifft_cfg; ///< Inverse Kiss FFT housekeeping object
|
||||||
|
#endif
|
||||||
|
} monitor_t;
|
||||||
|
|
||||||
|
void monitor_init(monitor_t* me, const monitor_config_t* cfg);
|
||||||
|
void monitor_reset(monitor_t* me);
|
||||||
|
void monitor_process(monitor_t* me, const float* frame);
|
||||||
|
void monitor_free(monitor_t* me);
|
||||||
|
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
void monitor_resynth(const monitor_t* me, const ftx_candidate_t* candidate, float* signal);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_MONITOR_H_
|
||||||
Vendored
+133
@@ -0,0 +1,133 @@
|
|||||||
|
#include "wave.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// Save signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||||
|
int save_wav(const float* signal, int num_samples, int sample_rate, const char* path)
|
||||||
|
{
|
||||||
|
char subChunk1ID[4] = { 'f', 'm', 't', ' ' };
|
||||||
|
uint32_t subChunk1Size = 16; // 16 for PCM
|
||||||
|
uint16_t audioFormat = 1; // PCM = 1
|
||||||
|
uint16_t numChannels = 1;
|
||||||
|
uint16_t bitsPerSample = 16;
|
||||||
|
uint32_t sampleRate = sample_rate;
|
||||||
|
uint16_t blockAlign = numChannels * bitsPerSample / 8;
|
||||||
|
uint32_t byteRate = sampleRate * blockAlign;
|
||||||
|
|
||||||
|
char subChunk2ID[4] = { 'd', 'a', 't', 'a' };
|
||||||
|
uint32_t subChunk2Size = num_samples * blockAlign;
|
||||||
|
|
||||||
|
char chunkID[4] = { 'R', 'I', 'F', 'F' };
|
||||||
|
uint32_t chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size);
|
||||||
|
char format[4] = { 'W', 'A', 'V', 'E' };
|
||||||
|
|
||||||
|
int16_t* raw_data = (int16_t*)malloc(num_samples * blockAlign);
|
||||||
|
for (int i = 0; i < num_samples; i++)
|
||||||
|
{
|
||||||
|
float x = signal[i];
|
||||||
|
if (x > 1.0)
|
||||||
|
x = 1.0;
|
||||||
|
else if (x < -1.0)
|
||||||
|
x = -1.0;
|
||||||
|
raw_data[i] = (int)(0.5 + (x * 32767.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE* f = fopen(path, "wb");
|
||||||
|
if (f == NULL)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
// NOTE: works only on little-endian architecture
|
||||||
|
fwrite(chunkID, sizeof(chunkID), 1, f);
|
||||||
|
fwrite(&chunkSize, sizeof(chunkSize), 1, f);
|
||||||
|
fwrite(format, sizeof(format), 1, f);
|
||||||
|
|
||||||
|
fwrite(subChunk1ID, sizeof(subChunk1ID), 1, f);
|
||||||
|
fwrite(&subChunk1Size, sizeof(subChunk1Size), 1, f);
|
||||||
|
fwrite(&audioFormat, sizeof(audioFormat), 1, f);
|
||||||
|
fwrite(&numChannels, sizeof(numChannels), 1, f);
|
||||||
|
fwrite(&sampleRate, sizeof(sampleRate), 1, f);
|
||||||
|
fwrite(&byteRate, sizeof(byteRate), 1, f);
|
||||||
|
fwrite(&blockAlign, sizeof(blockAlign), 1, f);
|
||||||
|
fwrite(&bitsPerSample, sizeof(bitsPerSample), 1, f);
|
||||||
|
|
||||||
|
fwrite(subChunk2ID, sizeof(subChunk2ID), 1, f);
|
||||||
|
fwrite(&subChunk2Size, sizeof(subChunk2Size), 1, f);
|
||||||
|
|
||||||
|
fwrite(raw_data, blockAlign, num_samples, f);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
free(raw_data);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||||
|
int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path)
|
||||||
|
{
|
||||||
|
char subChunk1ID[4]; // = {'f', 'm', 't', ' '};
|
||||||
|
uint32_t subChunk1Size; // = 16; // 16 for PCM
|
||||||
|
uint16_t audioFormat; // = 1; // PCM = 1
|
||||||
|
uint16_t numChannels; // = 1;
|
||||||
|
uint16_t bitsPerSample; // = 16;
|
||||||
|
uint32_t sampleRate;
|
||||||
|
uint16_t blockAlign; // = numChannels * bitsPerSample / 8;
|
||||||
|
uint32_t byteRate; // = sampleRate * blockAlign;
|
||||||
|
|
||||||
|
char subChunk2ID[4]; // = {'d', 'a', 't', 'a'};
|
||||||
|
uint32_t subChunk2Size; // = num_samples * blockAlign;
|
||||||
|
|
||||||
|
char chunkID[4]; // = {'R', 'I', 'F', 'F'};
|
||||||
|
uint32_t chunkSize; // = 4 + (8 + subChunk1Size) + (8 + subChunk2Size);
|
||||||
|
char format[4]; // = {'W', 'A', 'V', 'E'};
|
||||||
|
|
||||||
|
FILE* f = fopen(path, "rb");
|
||||||
|
if (f == NULL)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
// NOTE: works only on little-endian architecture
|
||||||
|
fread((void*)chunkID, sizeof(chunkID), 1, f);
|
||||||
|
fread((void*)&chunkSize, sizeof(chunkSize), 1, f);
|
||||||
|
fread((void*)format, sizeof(format), 1, f);
|
||||||
|
|
||||||
|
fread((void*)subChunk1ID, sizeof(subChunk1ID), 1, f);
|
||||||
|
fread((void*)&subChunk1Size, sizeof(subChunk1Size), 1, f);
|
||||||
|
if (subChunk1Size != 16)
|
||||||
|
return -2;
|
||||||
|
|
||||||
|
fread((void*)&audioFormat, sizeof(audioFormat), 1, f);
|
||||||
|
fread((void*)&numChannels, sizeof(numChannels), 1, f);
|
||||||
|
fread((void*)&sampleRate, sizeof(sampleRate), 1, f);
|
||||||
|
fread((void*)&byteRate, sizeof(byteRate), 1, f);
|
||||||
|
fread((void*)&blockAlign, sizeof(blockAlign), 1, f);
|
||||||
|
fread((void*)&bitsPerSample, sizeof(bitsPerSample), 1, f);
|
||||||
|
|
||||||
|
if (audioFormat != 1 || numChannels != 1 || bitsPerSample != 16)
|
||||||
|
return -3;
|
||||||
|
|
||||||
|
fread((void*)subChunk2ID, sizeof(subChunk2ID), 1, f);
|
||||||
|
fread((void*)&subChunk2Size, sizeof(subChunk2Size), 1, f);
|
||||||
|
|
||||||
|
if (subChunk2Size / blockAlign > *num_samples)
|
||||||
|
return -4;
|
||||||
|
|
||||||
|
*num_samples = subChunk2Size / blockAlign;
|
||||||
|
*sample_rate = sampleRate;
|
||||||
|
|
||||||
|
int16_t* raw_data = (int16_t*)malloc(*num_samples * blockAlign);
|
||||||
|
|
||||||
|
fread((void*)raw_data, blockAlign, *num_samples, f);
|
||||||
|
for (int i = 0; i < *num_samples; i++)
|
||||||
|
{
|
||||||
|
signal[i] = raw_data[i] / 32768.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(raw_data);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef _INCLUDE_WAVE_H_
|
||||||
|
#define _INCLUDE_WAVE_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Save signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||||
|
int save_wav(const float* signal, int num_samples, int sample_rate, const char* path);
|
||||||
|
|
||||||
|
// Load signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||||
|
int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_WAVE_H_
|
||||||
Vendored
+393
@@ -0,0 +1,393 @@
|
|||||||
|
#define _POSIX_C_SOURCE 199309L
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#include <ft8/decode.h>
|
||||||
|
#include <ft8/encode.h>
|
||||||
|
#include <ft8/message.h>
|
||||||
|
|
||||||
|
#include <common/common.h>
|
||||||
|
#include <common/wave.h>
|
||||||
|
#include <common/monitor.h>
|
||||||
|
#include <common/audio.h>
|
||||||
|
|
||||||
|
#define LOG_LEVEL LOG_INFO
|
||||||
|
#include <ft8/debug.h>
|
||||||
|
|
||||||
|
const int kMin_score = 10; // Minimum sync score threshold for candidates
|
||||||
|
const int kMax_candidates = 140;
|
||||||
|
const int kLDPC_iterations = 25;
|
||||||
|
|
||||||
|
const int kMax_decoded_messages = 50;
|
||||||
|
|
||||||
|
const int kFreq_osr = 2; // Frequency oversampling rate (bin subdivision)
|
||||||
|
const int kTime_osr = 2; // Time oversampling rate (symbol subdivision)
|
||||||
|
|
||||||
|
void usage(const char* error_msg)
|
||||||
|
{
|
||||||
|
if (error_msg != NULL)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "ERROR: %s\n", error_msg);
|
||||||
|
}
|
||||||
|
fprintf(stderr, "Usage: decode_ft8 [-list|([-ft4] [INPUT|-dev DEVICE])]\n\n");
|
||||||
|
fprintf(stderr, "Decode a 15-second (or slighly shorter) WAV file.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CALLSIGN_HASHTABLE_SIZE 256
|
||||||
|
|
||||||
|
static struct
|
||||||
|
{
|
||||||
|
char callsign[12]; ///> Up to 11 symbols of callsign + trailing zeros (always filled)
|
||||||
|
uint32_t hash; ///> 8 MSBs contain the age of callsign; 22 LSBs contain hash value
|
||||||
|
} callsign_hashtable[CALLSIGN_HASHTABLE_SIZE];
|
||||||
|
|
||||||
|
static int callsign_hashtable_size;
|
||||||
|
|
||||||
|
void hashtable_init(void)
|
||||||
|
{
|
||||||
|
callsign_hashtable_size = 0;
|
||||||
|
memset(callsign_hashtable, 0, sizeof(callsign_hashtable));
|
||||||
|
}
|
||||||
|
|
||||||
|
void hashtable_cleanup(uint8_t max_age)
|
||||||
|
{
|
||||||
|
for (int idx_hash = 0; idx_hash < CALLSIGN_HASHTABLE_SIZE; ++idx_hash)
|
||||||
|
{
|
||||||
|
if (callsign_hashtable[idx_hash].callsign[0] != '\0')
|
||||||
|
{
|
||||||
|
uint8_t age = (uint8_t)(callsign_hashtable[idx_hash].hash >> 24);
|
||||||
|
if (age > max_age)
|
||||||
|
{
|
||||||
|
LOG(LOG_INFO, "Removing [%s] from hash table, age = %d\n", callsign_hashtable[idx_hash].callsign, age);
|
||||||
|
// free the hash entry
|
||||||
|
callsign_hashtable[idx_hash].callsign[0] = '\0';
|
||||||
|
callsign_hashtable[idx_hash].hash = 0;
|
||||||
|
callsign_hashtable_size--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// increase callsign age
|
||||||
|
callsign_hashtable[idx_hash].hash = (((uint32_t)age + 1u) << 24) | (callsign_hashtable[idx_hash].hash & 0x3FFFFFu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hashtable_add(const char* callsign, uint32_t hash)
|
||||||
|
{
|
||||||
|
uint16_t hash10 = (hash >> 12) & 0x3FFu;
|
||||||
|
int idx_hash = (hash10 * 23) % CALLSIGN_HASHTABLE_SIZE;
|
||||||
|
while (callsign_hashtable[idx_hash].callsign[0] != '\0')
|
||||||
|
{
|
||||||
|
if (((callsign_hashtable[idx_hash].hash & 0x3FFFFFu) == hash) && (0 == strcmp(callsign_hashtable[idx_hash].callsign, callsign)))
|
||||||
|
{
|
||||||
|
// reset age
|
||||||
|
callsign_hashtable[idx_hash].hash &= 0x3FFFFFu;
|
||||||
|
LOG(LOG_DEBUG, "Found a duplicate [%s]\n", callsign);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG(LOG_DEBUG, "Hash table clash!\n");
|
||||||
|
// Move on to check the next entry in hash table
|
||||||
|
idx_hash = (idx_hash + 1) % CALLSIGN_HASHTABLE_SIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callsign_hashtable_size++;
|
||||||
|
strncpy(callsign_hashtable[idx_hash].callsign, callsign, 11);
|
||||||
|
callsign_hashtable[idx_hash].callsign[11] = '\0';
|
||||||
|
callsign_hashtable[idx_hash].hash = hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hashtable_lookup(ftx_callsign_hash_type_t hash_type, uint32_t hash, char* callsign)
|
||||||
|
{
|
||||||
|
uint8_t hash_shift = (hash_type == FTX_CALLSIGN_HASH_10_BITS) ? 12 : (hash_type == FTX_CALLSIGN_HASH_12_BITS ? 10 : 0);
|
||||||
|
uint16_t hash10 = (hash >> (12 - hash_shift)) & 0x3FFu;
|
||||||
|
int idx_hash = (hash10 * 23) % CALLSIGN_HASHTABLE_SIZE;
|
||||||
|
while (callsign_hashtable[idx_hash].callsign[0] != '\0')
|
||||||
|
{
|
||||||
|
if (((callsign_hashtable[idx_hash].hash & 0x3FFFFFu) >> hash_shift) == hash)
|
||||||
|
{
|
||||||
|
strcpy(callsign, callsign_hashtable[idx_hash].callsign);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Move on to check the next entry in hash table
|
||||||
|
idx_hash = (idx_hash + 1) % CALLSIGN_HASHTABLE_SIZE;
|
||||||
|
}
|
||||||
|
callsign[0] = '\0';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ftx_callsign_hash_interface_t hash_if = {
|
||||||
|
.lookup_hash = hashtable_lookup,
|
||||||
|
.save_hash = hashtable_add
|
||||||
|
};
|
||||||
|
|
||||||
|
void decode(const monitor_t* mon, struct tm* tm_slot_start)
|
||||||
|
{
|
||||||
|
const ftx_waterfall_t* wf = &mon->wf;
|
||||||
|
// Find top candidates by Costas sync score and localize them in time and frequency
|
||||||
|
ftx_candidate_t candidate_list[kMax_candidates];
|
||||||
|
int num_candidates = ftx_find_candidates(wf, kMax_candidates, candidate_list, kMin_score);
|
||||||
|
|
||||||
|
// Hash table for decoded messages (to check for duplicates)
|
||||||
|
int num_decoded = 0;
|
||||||
|
ftx_message_t decoded[kMax_decoded_messages];
|
||||||
|
ftx_message_t* decoded_hashtable[kMax_decoded_messages];
|
||||||
|
|
||||||
|
// Initialize hash table pointers
|
||||||
|
for (int i = 0; i < kMax_decoded_messages; ++i)
|
||||||
|
{
|
||||||
|
decoded_hashtable[i] = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go over candidates and attempt to decode messages
|
||||||
|
for (int idx = 0; idx < num_candidates; ++idx)
|
||||||
|
{
|
||||||
|
const ftx_candidate_t* cand = &candidate_list[idx];
|
||||||
|
|
||||||
|
float freq_hz = (mon->min_bin + cand->freq_offset + (float)cand->freq_sub / wf->freq_osr) / mon->symbol_period;
|
||||||
|
float time_sec = (cand->time_offset + (float)cand->time_sub / wf->time_osr) * mon->symbol_period;
|
||||||
|
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
// int resynth_len = 12000 * 16;
|
||||||
|
// float resynth_signal[resynth_len];
|
||||||
|
// for (int pos = 0; pos < resynth_len; ++pos)
|
||||||
|
// {
|
||||||
|
// resynth_signal[pos] = 0;
|
||||||
|
// }
|
||||||
|
// monitor_resynth(mon, cand, resynth_signal);
|
||||||
|
// char resynth_path[80];
|
||||||
|
// sprintf(resynth_path, "resynth_%04f_%02.1f.wav", freq_hz, time_sec);
|
||||||
|
// save_wav(resynth_signal, resynth_len, 12000, resynth_path);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ftx_message_t message;
|
||||||
|
ftx_decode_status_t status;
|
||||||
|
if (!ftx_decode_candidate(wf, cand, kLDPC_iterations, &message, &status))
|
||||||
|
{
|
||||||
|
if (status.ldpc_errors > 0)
|
||||||
|
{
|
||||||
|
LOG(LOG_DEBUG, "LDPC decode: %d errors\n", status.ldpc_errors);
|
||||||
|
}
|
||||||
|
else if (status.crc_calculated != status.crc_extracted)
|
||||||
|
{
|
||||||
|
LOG(LOG_DEBUG, "CRC mismatch!\n");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG(LOG_DEBUG, "Checking hash table for %4.1fs / %4.1fHz [%d]...\n", time_sec, freq_hz, cand->score);
|
||||||
|
int idx_hash = message.hash % kMax_decoded_messages;
|
||||||
|
bool found_empty_slot = false;
|
||||||
|
bool found_duplicate = false;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (decoded_hashtable[idx_hash] == NULL)
|
||||||
|
{
|
||||||
|
LOG(LOG_DEBUG, "Found an empty slot\n");
|
||||||
|
found_empty_slot = true;
|
||||||
|
}
|
||||||
|
else if ((decoded_hashtable[idx_hash]->hash == message.hash) && (0 == memcmp(decoded_hashtable[idx_hash]->payload, message.payload, sizeof(message.payload))))
|
||||||
|
{
|
||||||
|
LOG(LOG_DEBUG, "Found a duplicate!\n");
|
||||||
|
found_duplicate = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG(LOG_DEBUG, "Hash table clash!\n");
|
||||||
|
// Move on to check the next entry in hash table
|
||||||
|
idx_hash = (idx_hash + 1) % kMax_decoded_messages;
|
||||||
|
}
|
||||||
|
} while (!found_empty_slot && !found_duplicate);
|
||||||
|
|
||||||
|
if (found_empty_slot)
|
||||||
|
{
|
||||||
|
// Fill the empty hashtable slot
|
||||||
|
memcpy(&decoded[idx_hash], &message, sizeof(message));
|
||||||
|
decoded_hashtable[idx_hash] = &decoded[idx_hash];
|
||||||
|
++num_decoded;
|
||||||
|
|
||||||
|
char text[FTX_MAX_MESSAGE_LENGTH];
|
||||||
|
ftx_message_offsets_t offsets;
|
||||||
|
ftx_message_rc_t unpack_status = ftx_message_decode(&message, &hash_if, text, &offsets);
|
||||||
|
if (unpack_status != FTX_MESSAGE_RC_OK)
|
||||||
|
{
|
||||||
|
snprintf(text, sizeof(text), "Error [%d] while unpacking!", (int)unpack_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fake WSJT-X-like output for now
|
||||||
|
float snr = cand->score * 0.5f; // TODO: compute better approximation of SNR
|
||||||
|
printf("%02d%02d%02d %+05.1f %+4.2f %4.0f ~ %s\n",
|
||||||
|
tm_slot_start->tm_hour, tm_slot_start->tm_min, tm_slot_start->tm_sec,
|
||||||
|
snr, time_sec, freq_hz, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG(LOG_INFO, "Decoded %d messages, callsign hashtable size %d\n", num_decoded, callsign_hashtable_size);
|
||||||
|
hashtable_cleanup(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
// Accepted arguments
|
||||||
|
const char* wav_path = NULL;
|
||||||
|
const char* dev_name = NULL;
|
||||||
|
ftx_protocol_t protocol = FTX_PROTOCOL_FT8;
|
||||||
|
float time_shift = 0.8;
|
||||||
|
|
||||||
|
// Parse arguments one by one
|
||||||
|
int arg_idx = 1;
|
||||||
|
while (arg_idx < argc)
|
||||||
|
{
|
||||||
|
// Check if the current argument is an option (-xxx)
|
||||||
|
if (argv[arg_idx][0] == '-')
|
||||||
|
{
|
||||||
|
// Check agaist valid options
|
||||||
|
if (0 == strcmp(argv[arg_idx], "-ft4"))
|
||||||
|
{
|
||||||
|
protocol = FTX_PROTOCOL_FT4;
|
||||||
|
}
|
||||||
|
else if (0 == strcmp(argv[arg_idx], "-list"))
|
||||||
|
{
|
||||||
|
audio_init();
|
||||||
|
audio_list();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else if (0 == strcmp(argv[arg_idx], "-dev"))
|
||||||
|
{
|
||||||
|
if (arg_idx + 1 < argc)
|
||||||
|
{
|
||||||
|
++arg_idx;
|
||||||
|
dev_name = argv[arg_idx];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
usage("Expected an audio device name after -dev");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
usage("Unknown command line option");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (wav_path == NULL)
|
||||||
|
{
|
||||||
|
wav_path = argv[arg_idx];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
usage("Multiple positional arguments");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++arg_idx;
|
||||||
|
}
|
||||||
|
// Check if all mandatory arguments have been received
|
||||||
|
if (wav_path == NULL && dev_name == NULL)
|
||||||
|
{
|
||||||
|
usage("Expected either INPUT file path or DEVICE name");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
float slot_period = ((protocol == FTX_PROTOCOL_FT8) ? FT8_SLOT_TIME : FT4_SLOT_TIME);
|
||||||
|
int sample_rate = 12000;
|
||||||
|
int num_samples = slot_period * sample_rate;
|
||||||
|
float signal[num_samples];
|
||||||
|
bool is_live = false;
|
||||||
|
|
||||||
|
if (wav_path != NULL)
|
||||||
|
{
|
||||||
|
int rc = load_wav(signal, &num_samples, &sample_rate, wav_path);
|
||||||
|
if (rc < 0)
|
||||||
|
{
|
||||||
|
LOG(LOG_ERROR, "ERROR: cannot load wave file %s\n", wav_path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
LOG(LOG_INFO, "Sample rate %d Hz, %d samples, %.3f seconds\n", sample_rate, num_samples, (double)num_samples / sample_rate);
|
||||||
|
}
|
||||||
|
else if (dev_name != NULL)
|
||||||
|
{
|
||||||
|
audio_init();
|
||||||
|
audio_open(dev_name);
|
||||||
|
num_samples = (slot_period - 0.4f) * sample_rate;
|
||||||
|
is_live = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute FFT over the whole signal and store it
|
||||||
|
monitor_t mon;
|
||||||
|
monitor_config_t mon_cfg = {
|
||||||
|
.f_min = 200,
|
||||||
|
.f_max = 3000,
|
||||||
|
.sample_rate = sample_rate,
|
||||||
|
.time_osr = kTime_osr,
|
||||||
|
.freq_osr = kFreq_osr,
|
||||||
|
.protocol = protocol
|
||||||
|
};
|
||||||
|
|
||||||
|
hashtable_init();
|
||||||
|
|
||||||
|
monitor_init(&mon, &mon_cfg);
|
||||||
|
LOG(LOG_DEBUG, "Waterfall allocated %d symbols\n", mon.wf.max_blocks);
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
struct tm tm_slot_start = { 0 };
|
||||||
|
if (is_live)
|
||||||
|
{
|
||||||
|
// Wait for the start of time slot
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
struct timespec spec;
|
||||||
|
clock_gettime(CLOCK_REALTIME, &spec);
|
||||||
|
double time = (double)spec.tv_sec + (spec.tv_nsec / 1e9);
|
||||||
|
double time_within_slot = fmod(time - time_shift, slot_period);
|
||||||
|
if (time_within_slot > slot_period / 4)
|
||||||
|
{
|
||||||
|
audio_read(signal, mon.block_size);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
time_t time_slot_start = (time_t)(time - time_within_slot);
|
||||||
|
gmtime_r(&time_slot_start, &tm_slot_start);
|
||||||
|
LOG(LOG_INFO, "Time within slot %02d%02d%02d: %.3f s\n", tm_slot_start.tm_hour,
|
||||||
|
tm_slot_start.tm_min, tm_slot_start.tm_sec, time_within_slot);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process and accumulate audio data in a monitor/waterfall instance
|
||||||
|
for (int frame_pos = 0; frame_pos + mon.block_size <= num_samples; frame_pos += mon.block_size)
|
||||||
|
{
|
||||||
|
if (dev_name != NULL)
|
||||||
|
{
|
||||||
|
audio_read(signal + frame_pos, mon.block_size);
|
||||||
|
}
|
||||||
|
// LOG(LOG_DEBUG, "Frame pos: %.3fs\n", (float)(frame_pos + mon.block_size) / sample_rate);
|
||||||
|
fprintf(stderr, "#");
|
||||||
|
// Process the waveform data frame by frame - you could have a live loop here with data from an audio device
|
||||||
|
monitor_process(&mon, signal + frame_pos);
|
||||||
|
}
|
||||||
|
fprintf(stderr, "\n");
|
||||||
|
LOG(LOG_DEBUG, "Waterfall accumulated %d symbols\n", mon.wf.num_blocks);
|
||||||
|
LOG(LOG_INFO, "Max magnitude: %.1f dB\n", mon.max_mag);
|
||||||
|
|
||||||
|
// Decode accumulated data (containing slightly less than a full time slot)
|
||||||
|
decode(&mon, &tm_slot_start);
|
||||||
|
|
||||||
|
// Reset internal variables for the next time slot
|
||||||
|
monitor_reset(&mon);
|
||||||
|
} while (is_live);
|
||||||
|
|
||||||
|
monitor_free(&mon);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Vendored
+189
@@ -0,0 +1,189 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include "common/common.h"
|
||||||
|
#include "common/wave.h"
|
||||||
|
#include "ft8/message.h"
|
||||||
|
#include "ft8/encode.h"
|
||||||
|
#include "ft8/constants.h"
|
||||||
|
|
||||||
|
#define LOG_LEVEL LOG_INFO
|
||||||
|
#include "ft8/debug.h"
|
||||||
|
|
||||||
|
#define FT8_SYMBOL_BT 2.0f ///< symbol smoothing filter bandwidth factor (BT)
|
||||||
|
#define FT4_SYMBOL_BT 1.0f ///< symbol smoothing filter bandwidth factor (BT)
|
||||||
|
|
||||||
|
#define GFSK_CONST_K 5.336446f ///< == pi * sqrt(2 / log(2))
|
||||||
|
|
||||||
|
/// Computes a GFSK smoothing pulse.
|
||||||
|
/// The pulse is theoretically infinitely long, however, here it's truncated at 3 times the symbol length.
|
||||||
|
/// This means the pulse array has to have space for 3*n_spsym elements.
|
||||||
|
/// @param[in] n_spsym Number of samples per symbol
|
||||||
|
/// @param[in] b Shape parameter (values defined for FT8/FT4)
|
||||||
|
/// @param[out] pulse Output array of pulse samples
|
||||||
|
///
|
||||||
|
void gfsk_pulse(int n_spsym, float symbol_bt, float* pulse)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 3 * n_spsym; ++i)
|
||||||
|
{
|
||||||
|
float t = i / (float)n_spsym - 1.5f;
|
||||||
|
float arg1 = GFSK_CONST_K * symbol_bt * (t + 0.5f);
|
||||||
|
float arg2 = GFSK_CONST_K * symbol_bt * (t - 0.5f);
|
||||||
|
pulse[i] = (erff(arg1) - erff(arg2)) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synthesize waveform data using GFSK phase shaping.
|
||||||
|
/// The output waveform will contain n_sym symbols.
|
||||||
|
/// @param[in] symbols Array of symbols (tones) (0-7 for FT8)
|
||||||
|
/// @param[in] n_sym Number of symbols in the symbol array
|
||||||
|
/// @param[in] f0 Audio frequency in Hertz for the symbol 0 (base frequency)
|
||||||
|
/// @param[in] symbol_bt Symbol smoothing filter bandwidth (2 for FT8, 1 for FT4)
|
||||||
|
/// @param[in] symbol_period Symbol period (duration), seconds
|
||||||
|
/// @param[in] signal_rate Sample rate of synthesized signal, Hertz
|
||||||
|
/// @param[out] signal Output array of signal waveform samples (should have space for n_sym*n_spsym samples)
|
||||||
|
///
|
||||||
|
void synth_gfsk(const uint8_t* symbols, int n_sym, float f0, float symbol_bt, float symbol_period, int signal_rate, float* signal)
|
||||||
|
{
|
||||||
|
int n_spsym = (int)(0.5f + signal_rate * symbol_period); // Samples per symbol
|
||||||
|
int n_wave = n_sym * n_spsym; // Number of output samples
|
||||||
|
float hmod = 1.0f;
|
||||||
|
|
||||||
|
LOG(LOG_DEBUG, "n_spsym = %d\n", n_spsym);
|
||||||
|
// Compute the smoothed frequency waveform.
|
||||||
|
// Length = (nsym+2)*n_spsym samples, first and last symbols extended
|
||||||
|
float dphi_peak = 2 * M_PI * hmod / n_spsym;
|
||||||
|
float dphi[n_wave + 2 * n_spsym];
|
||||||
|
|
||||||
|
// Shift frequency up by f0
|
||||||
|
for (int i = 0; i < n_wave + 2 * n_spsym; ++i)
|
||||||
|
{
|
||||||
|
dphi[i] = 2 * M_PI * f0 / signal_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
float pulse[3 * n_spsym];
|
||||||
|
gfsk_pulse(n_spsym, symbol_bt, pulse);
|
||||||
|
|
||||||
|
for (int i = 0; i < n_sym; ++i)
|
||||||
|
{
|
||||||
|
int ib = i * n_spsym;
|
||||||
|
for (int j = 0; j < 3 * n_spsym; ++j)
|
||||||
|
{
|
||||||
|
dphi[j + ib] += dphi_peak * symbols[i] * pulse[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dummy symbols at beginning and end with tone values equal to 1st and last symbol, respectively
|
||||||
|
for (int j = 0; j < 2 * n_spsym; ++j)
|
||||||
|
{
|
||||||
|
dphi[j] += dphi_peak * pulse[j + n_spsym] * symbols[0];
|
||||||
|
dphi[j + n_sym * n_spsym] += dphi_peak * pulse[j] * symbols[n_sym - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate and insert the audio waveform
|
||||||
|
float phi = 0;
|
||||||
|
for (int k = 0; k < n_wave; ++k)
|
||||||
|
{ // Don't include dummy symbols
|
||||||
|
signal[k] = sinf(phi);
|
||||||
|
phi = fmodf(phi + dphi[k + n_spsym], 2 * M_PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply envelope shaping to the first and last symbols
|
||||||
|
int n_ramp = n_spsym / 8;
|
||||||
|
for (int i = 0; i < n_ramp; ++i)
|
||||||
|
{
|
||||||
|
float env = (1 - cosf(2 * M_PI * i / (2 * n_ramp))) / 2;
|
||||||
|
signal[i] *= env;
|
||||||
|
signal[n_wave - 1 - i] *= env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void usage()
|
||||||
|
{
|
||||||
|
printf("Generate a 15-second WAV file encoding a given message.\n");
|
||||||
|
printf("Usage:\n");
|
||||||
|
printf("\n");
|
||||||
|
printf("gen_ft8 MESSAGE WAV_FILE [FREQUENCY]\n");
|
||||||
|
printf("\n");
|
||||||
|
printf("(Note that you might have to enclose your message in quote marks if it contains spaces)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
// Expect two command-line arguments
|
||||||
|
if (argc < 3)
|
||||||
|
{
|
||||||
|
usage();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* message = argv[1];
|
||||||
|
const char* wav_path = argv[2];
|
||||||
|
float frequency = 1000.0;
|
||||||
|
if (argc > 3)
|
||||||
|
{
|
||||||
|
frequency = atof(argv[3]);
|
||||||
|
}
|
||||||
|
bool is_ft4 = (argc > 4) && (0 == strcmp(argv[4], "-ft4"));
|
||||||
|
|
||||||
|
// First, pack the text data into binary message
|
||||||
|
ftx_message_t msg;
|
||||||
|
ftx_message_rc_t rc = ftx_message_encode(&msg, NULL, message);
|
||||||
|
if (rc != FTX_MESSAGE_RC_OK)
|
||||||
|
{
|
||||||
|
printf("Cannot parse message!\n");
|
||||||
|
printf("RC = %d\n", (int)rc);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Packed data: ");
|
||||||
|
for (int j = 0; j < 10; ++j)
|
||||||
|
{
|
||||||
|
printf("%02x ", msg.payload[j]);
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
|
||||||
|
int num_tones = (is_ft4) ? FT4_NN : FT8_NN;
|
||||||
|
float symbol_period = (is_ft4) ? FT4_SYMBOL_PERIOD : FT8_SYMBOL_PERIOD;
|
||||||
|
float symbol_bt = (is_ft4) ? FT4_SYMBOL_BT : FT8_SYMBOL_BT;
|
||||||
|
float slot_time = (is_ft4) ? FT4_SLOT_TIME : FT8_SLOT_TIME;
|
||||||
|
|
||||||
|
// Second, encode the binary message as a sequence of FSK tones
|
||||||
|
uint8_t tones[num_tones]; // Array of 79 tones (symbols)
|
||||||
|
if (is_ft4)
|
||||||
|
{
|
||||||
|
ft4_encode(msg.payload, tones);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ft8_encode(msg.payload, tones);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("FSK tones: ");
|
||||||
|
for (int j = 0; j < num_tones; ++j)
|
||||||
|
{
|
||||||
|
printf("%d", tones[j]);
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
|
||||||
|
// Third, convert the FSK tones into an audio signal
|
||||||
|
int sample_rate = 12000;
|
||||||
|
int num_samples = (int)(0.5f + num_tones * symbol_period * sample_rate); // Number of samples in the data signal
|
||||||
|
int num_silence = (slot_time * sample_rate - num_samples) / 2; // Silence padding at both ends to make 15 seconds
|
||||||
|
int num_total_samples = num_silence + num_samples + num_silence; // Number of samples in the padded signal
|
||||||
|
float signal[num_total_samples];
|
||||||
|
for (int i = 0; i < num_silence; i++)
|
||||||
|
{
|
||||||
|
signal[i] = 0;
|
||||||
|
signal[i + num_samples + num_silence] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synthesize waveform data (signal) and save it as WAV file
|
||||||
|
synth_gfsk(tones, num_tones, frequency, symbol_bt, symbol_period, sample_rate, signal + num_silence);
|
||||||
|
save_wav(signal, num_total_samples, sample_rate, wav_path);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
|
||||||
|
* This file is part of KISS FFT - https://github.com/mborgerding/kissfft
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
* See COPYING file for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* kiss_fft.h
|
||||||
|
defines kiss_fft_scalar as either short or a float type
|
||||||
|
and defines
|
||||||
|
typedef struct { kiss_fft_scalar r; kiss_fft_scalar i; }kiss_fft_cpx; */
|
||||||
|
#include "kiss_fft.h"
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
|
#define MAXFACTORS 32
|
||||||
|
/* e.g. an fft of length 128 has 4 factors
|
||||||
|
as far as kissfft is concerned
|
||||||
|
4*4*4*2
|
||||||
|
*/
|
||||||
|
|
||||||
|
struct kiss_fft_state{
|
||||||
|
int nfft;
|
||||||
|
int inverse;
|
||||||
|
int factors[2*MAXFACTORS];
|
||||||
|
kiss_fft_cpx twiddles[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Explanation of macros dealing with complex math:
|
||||||
|
|
||||||
|
C_MUL(m,a,b) : m = a*b
|
||||||
|
C_FIXDIV( c , div ) : if a fixed point impl., c /= div. noop otherwise
|
||||||
|
C_SUB( res, a,b) : res = a - b
|
||||||
|
C_SUBFROM( res , a) : res -= a
|
||||||
|
C_ADDTO( res , a) : res += a
|
||||||
|
* */
|
||||||
|
#ifdef FIXED_POINT
|
||||||
|
#if (FIXED_POINT==32)
|
||||||
|
# define FRACBITS 31
|
||||||
|
# define SAMPPROD int64_t
|
||||||
|
#define SAMP_MAX 2147483647
|
||||||
|
#else
|
||||||
|
# define FRACBITS 15
|
||||||
|
# define SAMPPROD int32_t
|
||||||
|
#define SAMP_MAX 32767
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define SAMP_MIN -SAMP_MAX
|
||||||
|
|
||||||
|
#if defined(CHECK_OVERFLOW)
|
||||||
|
# define CHECK_OVERFLOW_OP(a,op,b) \
|
||||||
|
if ( (SAMPPROD)(a) op (SAMPPROD)(b) > SAMP_MAX || (SAMPPROD)(a) op (SAMPPROD)(b) < SAMP_MIN ) { \
|
||||||
|
fprintf(stderr,"WARNING:overflow @ " __FILE__ "(%d): (%d " #op" %d) = %ld\n",__LINE__,(a),(b),(SAMPPROD)(a) op (SAMPPROD)(b) ); }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
# define smul(a,b) ( (SAMPPROD)(a)*(b) )
|
||||||
|
# define sround( x ) (kiss_fft_scalar)( ( (x) + (1<<(FRACBITS-1)) ) >> FRACBITS )
|
||||||
|
|
||||||
|
# define S_MUL(a,b) sround( smul(a,b) )
|
||||||
|
|
||||||
|
# define C_MUL(m,a,b) \
|
||||||
|
do{ (m).r = sround( smul((a).r,(b).r) - smul((a).i,(b).i) ); \
|
||||||
|
(m).i = sround( smul((a).r,(b).i) + smul((a).i,(b).r) ); }while(0)
|
||||||
|
|
||||||
|
# define DIVSCALAR(x,k) \
|
||||||
|
(x) = sround( smul( x, SAMP_MAX/k ) )
|
||||||
|
|
||||||
|
# define C_FIXDIV(c,div) \
|
||||||
|
do { DIVSCALAR( (c).r , div); \
|
||||||
|
DIVSCALAR( (c).i , div); }while (0)
|
||||||
|
|
||||||
|
# define C_MULBYSCALAR( c, s ) \
|
||||||
|
do{ (c).r = sround( smul( (c).r , s ) ) ;\
|
||||||
|
(c).i = sround( smul( (c).i , s ) ) ; }while(0)
|
||||||
|
|
||||||
|
#else /* not FIXED_POINT*/
|
||||||
|
|
||||||
|
# define S_MUL(a,b) ( (a)*(b) )
|
||||||
|
#define C_MUL(m,a,b) \
|
||||||
|
do{ (m).r = (a).r*(b).r - (a).i*(b).i;\
|
||||||
|
(m).i = (a).r*(b).i + (a).i*(b).r; }while(0)
|
||||||
|
# define C_FIXDIV(c,div) /* NOOP */
|
||||||
|
# define C_MULBYSCALAR( c, s ) \
|
||||||
|
do{ (c).r *= (s);\
|
||||||
|
(c).i *= (s); }while(0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CHECK_OVERFLOW_OP
|
||||||
|
# define CHECK_OVERFLOW_OP(a,op,b) /* noop */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define C_ADD( res, a,b)\
|
||||||
|
do { \
|
||||||
|
CHECK_OVERFLOW_OP((a).r,+,(b).r)\
|
||||||
|
CHECK_OVERFLOW_OP((a).i,+,(b).i)\
|
||||||
|
(res).r=(a).r+(b).r; (res).i=(a).i+(b).i; \
|
||||||
|
}while(0)
|
||||||
|
#define C_SUB( res, a,b)\
|
||||||
|
do { \
|
||||||
|
CHECK_OVERFLOW_OP((a).r,-,(b).r)\
|
||||||
|
CHECK_OVERFLOW_OP((a).i,-,(b).i)\
|
||||||
|
(res).r=(a).r-(b).r; (res).i=(a).i-(b).i; \
|
||||||
|
}while(0)
|
||||||
|
#define C_ADDTO( res , a)\
|
||||||
|
do { \
|
||||||
|
CHECK_OVERFLOW_OP((res).r,+,(a).r)\
|
||||||
|
CHECK_OVERFLOW_OP((res).i,+,(a).i)\
|
||||||
|
(res).r += (a).r; (res).i += (a).i;\
|
||||||
|
}while(0)
|
||||||
|
|
||||||
|
#define C_SUBFROM( res , a)\
|
||||||
|
do {\
|
||||||
|
CHECK_OVERFLOW_OP((res).r,-,(a).r)\
|
||||||
|
CHECK_OVERFLOW_OP((res).i,-,(a).i)\
|
||||||
|
(res).r -= (a).r; (res).i -= (a).i; \
|
||||||
|
}while(0)
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef FIXED_POINT
|
||||||
|
# define KISS_FFT_COS(phase) floor(.5+SAMP_MAX * cos (phase))
|
||||||
|
# define KISS_FFT_SIN(phase) floor(.5+SAMP_MAX * sin (phase))
|
||||||
|
# define HALF_OF(x) ((x)>>1)
|
||||||
|
#elif defined(USE_SIMD)
|
||||||
|
# define KISS_FFT_COS(phase) _mm_set1_ps( cos(phase) )
|
||||||
|
# define KISS_FFT_SIN(phase) _mm_set1_ps( sin(phase) )
|
||||||
|
# define HALF_OF(x) ((x)*_mm_set1_ps(.5))
|
||||||
|
#else
|
||||||
|
# define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase)
|
||||||
|
# define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase)
|
||||||
|
# define HALF_OF(x) ((x)*.5)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define kf_cexp(x,phase) \
|
||||||
|
do{ \
|
||||||
|
(x)->r = KISS_FFT_COS(phase);\
|
||||||
|
(x)->i = KISS_FFT_SIN(phase);\
|
||||||
|
}while(0)
|
||||||
|
|
||||||
|
|
||||||
|
/* a debugging function */
|
||||||
|
#define pcpx(c)\
|
||||||
|
fprintf(stderr,"%g + %gi\n",(double)((c)->r),(double)((c)->i) )
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef KISS_FFT_USE_ALLOCA
|
||||||
|
// define this to allow use of alloca instead of malloc for temporary buffers
|
||||||
|
// Temporary buffers are used in two case:
|
||||||
|
// 1. FFT sizes that have "bad" factors. i.e. not 2,3 and 5
|
||||||
|
// 2. "in-place" FFTs. Notice the quotes, since kissfft does not really do an in-place transform.
|
||||||
|
#include <alloca.h>
|
||||||
|
#define KISS_FFT_TMP_ALLOC(nbytes) alloca(nbytes)
|
||||||
|
#define KISS_FFT_TMP_FREE(ptr)
|
||||||
|
#else
|
||||||
|
#define KISS_FFT_TMP_ALLOC(nbytes) KISS_FFT_MALLOC(nbytes)
|
||||||
|
#define KISS_FFT_TMP_FREE(ptr) KISS_FFT_FREE(ptr)
|
||||||
|
#endif
|
||||||
Vendored
+402
@@ -0,0 +1,402 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
|
||||||
|
* This file is part of KISS FFT - https://github.com/mborgerding/kissfft
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
* See COPYING file for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#include "_kiss_fft_guts.h"
|
||||||
|
/* The guts header contains all the multiplication and addition macros that are defined for
|
||||||
|
fixed or floating point complex numbers. It also delares the kf_ internal functions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
static void kf_bfly2(
|
||||||
|
kiss_fft_cpx * Fout,
|
||||||
|
const size_t fstride,
|
||||||
|
const kiss_fft_cfg st,
|
||||||
|
int m
|
||||||
|
)
|
||||||
|
{
|
||||||
|
kiss_fft_cpx * Fout2;
|
||||||
|
kiss_fft_cpx * tw1 = st->twiddles;
|
||||||
|
kiss_fft_cpx t;
|
||||||
|
Fout2 = Fout + m;
|
||||||
|
do{
|
||||||
|
C_FIXDIV(*Fout,2); C_FIXDIV(*Fout2,2);
|
||||||
|
|
||||||
|
C_MUL (t, *Fout2 , *tw1);
|
||||||
|
tw1 += fstride;
|
||||||
|
C_SUB( *Fout2 , *Fout , t );
|
||||||
|
C_ADDTO( *Fout , t );
|
||||||
|
++Fout2;
|
||||||
|
++Fout;
|
||||||
|
}while (--m);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void kf_bfly4(
|
||||||
|
kiss_fft_cpx * Fout,
|
||||||
|
const size_t fstride,
|
||||||
|
const kiss_fft_cfg st,
|
||||||
|
const size_t m
|
||||||
|
)
|
||||||
|
{
|
||||||
|
kiss_fft_cpx *tw1,*tw2,*tw3;
|
||||||
|
kiss_fft_cpx scratch[6];
|
||||||
|
size_t k=m;
|
||||||
|
const size_t m2=2*m;
|
||||||
|
const size_t m3=3*m;
|
||||||
|
|
||||||
|
|
||||||
|
tw3 = tw2 = tw1 = st->twiddles;
|
||||||
|
|
||||||
|
do {
|
||||||
|
C_FIXDIV(*Fout,4); C_FIXDIV(Fout[m],4); C_FIXDIV(Fout[m2],4); C_FIXDIV(Fout[m3],4);
|
||||||
|
|
||||||
|
C_MUL(scratch[0],Fout[m] , *tw1 );
|
||||||
|
C_MUL(scratch[1],Fout[m2] , *tw2 );
|
||||||
|
C_MUL(scratch[2],Fout[m3] , *tw3 );
|
||||||
|
|
||||||
|
C_SUB( scratch[5] , *Fout, scratch[1] );
|
||||||
|
C_ADDTO(*Fout, scratch[1]);
|
||||||
|
C_ADD( scratch[3] , scratch[0] , scratch[2] );
|
||||||
|
C_SUB( scratch[4] , scratch[0] , scratch[2] );
|
||||||
|
C_SUB( Fout[m2], *Fout, scratch[3] );
|
||||||
|
tw1 += fstride;
|
||||||
|
tw2 += fstride*2;
|
||||||
|
tw3 += fstride*3;
|
||||||
|
C_ADDTO( *Fout , scratch[3] );
|
||||||
|
|
||||||
|
if(st->inverse) {
|
||||||
|
Fout[m].r = scratch[5].r - scratch[4].i;
|
||||||
|
Fout[m].i = scratch[5].i + scratch[4].r;
|
||||||
|
Fout[m3].r = scratch[5].r + scratch[4].i;
|
||||||
|
Fout[m3].i = scratch[5].i - scratch[4].r;
|
||||||
|
}else{
|
||||||
|
Fout[m].r = scratch[5].r + scratch[4].i;
|
||||||
|
Fout[m].i = scratch[5].i - scratch[4].r;
|
||||||
|
Fout[m3].r = scratch[5].r - scratch[4].i;
|
||||||
|
Fout[m3].i = scratch[5].i + scratch[4].r;
|
||||||
|
}
|
||||||
|
++Fout;
|
||||||
|
}while(--k);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void kf_bfly3(
|
||||||
|
kiss_fft_cpx * Fout,
|
||||||
|
const size_t fstride,
|
||||||
|
const kiss_fft_cfg st,
|
||||||
|
size_t m
|
||||||
|
)
|
||||||
|
{
|
||||||
|
size_t k=m;
|
||||||
|
const size_t m2 = 2*m;
|
||||||
|
kiss_fft_cpx *tw1,*tw2;
|
||||||
|
kiss_fft_cpx scratch[5];
|
||||||
|
kiss_fft_cpx epi3;
|
||||||
|
epi3 = st->twiddles[fstride*m];
|
||||||
|
|
||||||
|
tw1=tw2=st->twiddles;
|
||||||
|
|
||||||
|
do{
|
||||||
|
C_FIXDIV(*Fout,3); C_FIXDIV(Fout[m],3); C_FIXDIV(Fout[m2],3);
|
||||||
|
|
||||||
|
C_MUL(scratch[1],Fout[m] , *tw1);
|
||||||
|
C_MUL(scratch[2],Fout[m2] , *tw2);
|
||||||
|
|
||||||
|
C_ADD(scratch[3],scratch[1],scratch[2]);
|
||||||
|
C_SUB(scratch[0],scratch[1],scratch[2]);
|
||||||
|
tw1 += fstride;
|
||||||
|
tw2 += fstride*2;
|
||||||
|
|
||||||
|
Fout[m].r = Fout->r - HALF_OF(scratch[3].r);
|
||||||
|
Fout[m].i = Fout->i - HALF_OF(scratch[3].i);
|
||||||
|
|
||||||
|
C_MULBYSCALAR( scratch[0] , epi3.i );
|
||||||
|
|
||||||
|
C_ADDTO(*Fout,scratch[3]);
|
||||||
|
|
||||||
|
Fout[m2].r = Fout[m].r + scratch[0].i;
|
||||||
|
Fout[m2].i = Fout[m].i - scratch[0].r;
|
||||||
|
|
||||||
|
Fout[m].r -= scratch[0].i;
|
||||||
|
Fout[m].i += scratch[0].r;
|
||||||
|
|
||||||
|
++Fout;
|
||||||
|
}while(--k);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void kf_bfly5(
|
||||||
|
kiss_fft_cpx * Fout,
|
||||||
|
const size_t fstride,
|
||||||
|
const kiss_fft_cfg st,
|
||||||
|
int m
|
||||||
|
)
|
||||||
|
{
|
||||||
|
kiss_fft_cpx *Fout0,*Fout1,*Fout2,*Fout3,*Fout4;
|
||||||
|
int u;
|
||||||
|
kiss_fft_cpx scratch[13];
|
||||||
|
kiss_fft_cpx * twiddles = st->twiddles;
|
||||||
|
kiss_fft_cpx *tw;
|
||||||
|
kiss_fft_cpx ya,yb;
|
||||||
|
ya = twiddles[fstride*m];
|
||||||
|
yb = twiddles[fstride*2*m];
|
||||||
|
|
||||||
|
Fout0=Fout;
|
||||||
|
Fout1=Fout0+m;
|
||||||
|
Fout2=Fout0+2*m;
|
||||||
|
Fout3=Fout0+3*m;
|
||||||
|
Fout4=Fout0+4*m;
|
||||||
|
|
||||||
|
tw=st->twiddles;
|
||||||
|
for ( u=0; u<m; ++u ) {
|
||||||
|
C_FIXDIV( *Fout0,5); C_FIXDIV( *Fout1,5); C_FIXDIV( *Fout2,5); C_FIXDIV( *Fout3,5); C_FIXDIV( *Fout4,5);
|
||||||
|
scratch[0] = *Fout0;
|
||||||
|
|
||||||
|
C_MUL(scratch[1] ,*Fout1, tw[u*fstride]);
|
||||||
|
C_MUL(scratch[2] ,*Fout2, tw[2*u*fstride]);
|
||||||
|
C_MUL(scratch[3] ,*Fout3, tw[3*u*fstride]);
|
||||||
|
C_MUL(scratch[4] ,*Fout4, tw[4*u*fstride]);
|
||||||
|
|
||||||
|
C_ADD( scratch[7],scratch[1],scratch[4]);
|
||||||
|
C_SUB( scratch[10],scratch[1],scratch[4]);
|
||||||
|
C_ADD( scratch[8],scratch[2],scratch[3]);
|
||||||
|
C_SUB( scratch[9],scratch[2],scratch[3]);
|
||||||
|
|
||||||
|
Fout0->r += scratch[7].r + scratch[8].r;
|
||||||
|
Fout0->i += scratch[7].i + scratch[8].i;
|
||||||
|
|
||||||
|
scratch[5].r = scratch[0].r + S_MUL(scratch[7].r,ya.r) + S_MUL(scratch[8].r,yb.r);
|
||||||
|
scratch[5].i = scratch[0].i + S_MUL(scratch[7].i,ya.r) + S_MUL(scratch[8].i,yb.r);
|
||||||
|
|
||||||
|
scratch[6].r = S_MUL(scratch[10].i,ya.i) + S_MUL(scratch[9].i,yb.i);
|
||||||
|
scratch[6].i = -S_MUL(scratch[10].r,ya.i) - S_MUL(scratch[9].r,yb.i);
|
||||||
|
|
||||||
|
C_SUB(*Fout1,scratch[5],scratch[6]);
|
||||||
|
C_ADD(*Fout4,scratch[5],scratch[6]);
|
||||||
|
|
||||||
|
scratch[11].r = scratch[0].r + S_MUL(scratch[7].r,yb.r) + S_MUL(scratch[8].r,ya.r);
|
||||||
|
scratch[11].i = scratch[0].i + S_MUL(scratch[7].i,yb.r) + S_MUL(scratch[8].i,ya.r);
|
||||||
|
scratch[12].r = - S_MUL(scratch[10].i,yb.i) + S_MUL(scratch[9].i,ya.i);
|
||||||
|
scratch[12].i = S_MUL(scratch[10].r,yb.i) - S_MUL(scratch[9].r,ya.i);
|
||||||
|
|
||||||
|
C_ADD(*Fout2,scratch[11],scratch[12]);
|
||||||
|
C_SUB(*Fout3,scratch[11],scratch[12]);
|
||||||
|
|
||||||
|
++Fout0;++Fout1;++Fout2;++Fout3;++Fout4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* perform the butterfly for one stage of a mixed radix FFT */
|
||||||
|
static void kf_bfly_generic(
|
||||||
|
kiss_fft_cpx * Fout,
|
||||||
|
const size_t fstride,
|
||||||
|
const kiss_fft_cfg st,
|
||||||
|
int m,
|
||||||
|
int p
|
||||||
|
)
|
||||||
|
{
|
||||||
|
int u,k,q1,q;
|
||||||
|
kiss_fft_cpx * twiddles = st->twiddles;
|
||||||
|
kiss_fft_cpx t;
|
||||||
|
int Norig = st->nfft;
|
||||||
|
|
||||||
|
kiss_fft_cpx * scratch = (kiss_fft_cpx*)KISS_FFT_TMP_ALLOC(sizeof(kiss_fft_cpx)*p);
|
||||||
|
|
||||||
|
for ( u=0; u<m; ++u ) {
|
||||||
|
k=u;
|
||||||
|
for ( q1=0 ; q1<p ; ++q1 ) {
|
||||||
|
scratch[q1] = Fout[ k ];
|
||||||
|
C_FIXDIV(scratch[q1],p);
|
||||||
|
k += m;
|
||||||
|
}
|
||||||
|
|
||||||
|
k=u;
|
||||||
|
for ( q1=0 ; q1<p ; ++q1 ) {
|
||||||
|
int twidx=0;
|
||||||
|
Fout[ k ] = scratch[0];
|
||||||
|
for (q=1;q<p;++q ) {
|
||||||
|
twidx += fstride * k;
|
||||||
|
if (twidx>=Norig) twidx-=Norig;
|
||||||
|
C_MUL(t,scratch[q] , twiddles[twidx] );
|
||||||
|
C_ADDTO( Fout[ k ] ,t);
|
||||||
|
}
|
||||||
|
k += m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KISS_FFT_TMP_FREE(scratch);
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
void kf_work(
|
||||||
|
kiss_fft_cpx * Fout,
|
||||||
|
const kiss_fft_cpx * f,
|
||||||
|
const size_t fstride,
|
||||||
|
int in_stride,
|
||||||
|
int * factors,
|
||||||
|
const kiss_fft_cfg st
|
||||||
|
)
|
||||||
|
{
|
||||||
|
kiss_fft_cpx * Fout_beg=Fout;
|
||||||
|
const int p=*factors++; /* the radix */
|
||||||
|
const int m=*factors++; /* stage's fft length/p */
|
||||||
|
const kiss_fft_cpx * Fout_end = Fout + p*m;
|
||||||
|
|
||||||
|
#ifdef _OPENMP
|
||||||
|
// use openmp extensions at the
|
||||||
|
// top-level (not recursive)
|
||||||
|
if (fstride==1 && p<=5)
|
||||||
|
{
|
||||||
|
int k;
|
||||||
|
|
||||||
|
// execute the p different work units in different threads
|
||||||
|
# pragma omp parallel for
|
||||||
|
for (k=0;k<p;++k)
|
||||||
|
kf_work( Fout +k*m, f+ fstride*in_stride*k,fstride*p,in_stride,factors,st);
|
||||||
|
// all threads have joined by this point
|
||||||
|
|
||||||
|
switch (p) {
|
||||||
|
case 2: kf_bfly2(Fout,fstride,st,m); break;
|
||||||
|
case 3: kf_bfly3(Fout,fstride,st,m); break;
|
||||||
|
case 4: kf_bfly4(Fout,fstride,st,m); break;
|
||||||
|
case 5: kf_bfly5(Fout,fstride,st,m); break;
|
||||||
|
default: kf_bfly_generic(Fout,fstride,st,m,p); break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (m==1) {
|
||||||
|
do{
|
||||||
|
*Fout = *f;
|
||||||
|
f += fstride*in_stride;
|
||||||
|
}while(++Fout != Fout_end );
|
||||||
|
}else{
|
||||||
|
do{
|
||||||
|
// recursive call:
|
||||||
|
// DFT of size m*p performed by doing
|
||||||
|
// p instances of smaller DFTs of size m,
|
||||||
|
// each one takes a decimated version of the input
|
||||||
|
kf_work( Fout , f, fstride*p, in_stride, factors,st);
|
||||||
|
f += fstride*in_stride;
|
||||||
|
}while( (Fout += m) != Fout_end );
|
||||||
|
}
|
||||||
|
|
||||||
|
Fout=Fout_beg;
|
||||||
|
|
||||||
|
// recombine the p smaller DFTs
|
||||||
|
switch (p) {
|
||||||
|
case 2: kf_bfly2(Fout,fstride,st,m); break;
|
||||||
|
case 3: kf_bfly3(Fout,fstride,st,m); break;
|
||||||
|
case 4: kf_bfly4(Fout,fstride,st,m); break;
|
||||||
|
case 5: kf_bfly5(Fout,fstride,st,m); break;
|
||||||
|
default: kf_bfly_generic(Fout,fstride,st,m,p); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* facbuf is populated by p1,m1,p2,m2, ...
|
||||||
|
where
|
||||||
|
p[i] * m[i] = m[i-1]
|
||||||
|
m0 = n */
|
||||||
|
static
|
||||||
|
void kf_factor(int n,int * facbuf)
|
||||||
|
{
|
||||||
|
int p=4;
|
||||||
|
double floor_sqrt;
|
||||||
|
floor_sqrt = floor( sqrt((double)n) );
|
||||||
|
|
||||||
|
/*factor out powers of 4, powers of 2, then any remaining primes */
|
||||||
|
do {
|
||||||
|
while (n % p) {
|
||||||
|
switch (p) {
|
||||||
|
case 4: p = 2; break;
|
||||||
|
case 2: p = 3; break;
|
||||||
|
default: p += 2; break;
|
||||||
|
}
|
||||||
|
if (p > floor_sqrt)
|
||||||
|
p = n; /* no more factors, skip to end */
|
||||||
|
}
|
||||||
|
n /= p;
|
||||||
|
*facbuf++ = p;
|
||||||
|
*facbuf++ = n;
|
||||||
|
} while (n > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* User-callable function to allocate all necessary storage space for the fft.
|
||||||
|
*
|
||||||
|
* The return value is a contiguous block of memory, allocated with malloc. As such,
|
||||||
|
* It can be freed with free(), rather than a kiss_fft-specific function.
|
||||||
|
* */
|
||||||
|
kiss_fft_cfg kiss_fft_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem )
|
||||||
|
{
|
||||||
|
kiss_fft_cfg st=NULL;
|
||||||
|
size_t memneeded = sizeof(struct kiss_fft_state)
|
||||||
|
+ sizeof(kiss_fft_cpx)*(nfft-1); /* twiddle factors*/
|
||||||
|
|
||||||
|
if ( lenmem==NULL ) {
|
||||||
|
st = ( kiss_fft_cfg)KISS_FFT_MALLOC( memneeded );
|
||||||
|
}else{
|
||||||
|
if (mem != NULL && *lenmem >= memneeded)
|
||||||
|
st = (kiss_fft_cfg)mem;
|
||||||
|
*lenmem = memneeded;
|
||||||
|
}
|
||||||
|
if (st) {
|
||||||
|
int i;
|
||||||
|
st->nfft=nfft;
|
||||||
|
st->inverse = inverse_fft;
|
||||||
|
|
||||||
|
for (i=0;i<nfft;++i) {
|
||||||
|
const double pi=3.141592653589793238462643383279502884197169399375105820974944;
|
||||||
|
double phase = -2*pi*i / nfft;
|
||||||
|
if (st->inverse)
|
||||||
|
phase *= -1;
|
||||||
|
kf_cexp(st->twiddles+i, phase );
|
||||||
|
}
|
||||||
|
|
||||||
|
kf_factor(nfft,st->factors);
|
||||||
|
}
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void kiss_fft_stride(kiss_fft_cfg st,const kiss_fft_cpx *fin,kiss_fft_cpx *fout,int in_stride)
|
||||||
|
{
|
||||||
|
if (fin == fout) {
|
||||||
|
//NOTE: this is not really an in-place FFT algorithm.
|
||||||
|
//It just performs an out-of-place FFT into a temp buffer
|
||||||
|
kiss_fft_cpx * tmpbuf = (kiss_fft_cpx*)KISS_FFT_TMP_ALLOC( sizeof(kiss_fft_cpx)*st->nfft);
|
||||||
|
kf_work(tmpbuf,fin,1,in_stride, st->factors,st);
|
||||||
|
memcpy(fout,tmpbuf,sizeof(kiss_fft_cpx)*st->nfft);
|
||||||
|
KISS_FFT_TMP_FREE(tmpbuf);
|
||||||
|
}else{
|
||||||
|
kf_work( fout, fin, 1,in_stride, st->factors,st );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void kiss_fft(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout)
|
||||||
|
{
|
||||||
|
kiss_fft_stride(cfg,fin,fout,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void kiss_fft_cleanup(void)
|
||||||
|
{
|
||||||
|
// nothing needed any more
|
||||||
|
}
|
||||||
|
|
||||||
|
int kiss_fft_next_fast_size(int n)
|
||||||
|
{
|
||||||
|
while(1) {
|
||||||
|
int m=n;
|
||||||
|
while ( (m%2) == 0 ) m/=2;
|
||||||
|
while ( (m%3) == 0 ) m/=3;
|
||||||
|
while ( (m%5) == 0 ) m/=5;
|
||||||
|
if (m<=1)
|
||||||
|
break; /* n is completely factorable by twos, threes, and fives */
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
Vendored
+132
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
|
||||||
|
* This file is part of KISS FFT - https://github.com/mborgerding/kissfft
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
* See COPYING file for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KISS_FFT_H
|
||||||
|
#define KISS_FFT_H
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
ATTENTION!
|
||||||
|
If you would like a :
|
||||||
|
-- a utility that will handle the caching of fft objects
|
||||||
|
-- real-only (no imaginary time component ) FFT
|
||||||
|
-- a multi-dimensional FFT
|
||||||
|
-- a command-line utility to perform ffts
|
||||||
|
-- a command-line utility to perform fast-convolution filtering
|
||||||
|
|
||||||
|
Then see kfc.h kiss_fftr.h kiss_fftnd.h fftutil.c kiss_fastfir.c
|
||||||
|
in the tools/ directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef USE_SIMD
|
||||||
|
# include <xmmintrin.h>
|
||||||
|
# define kiss_fft_scalar __m128
|
||||||
|
#define KISS_FFT_MALLOC(nbytes) _mm_malloc(nbytes,16)
|
||||||
|
#define KISS_FFT_FREE _mm_free
|
||||||
|
#else
|
||||||
|
#define KISS_FFT_MALLOC malloc
|
||||||
|
#define KISS_FFT_FREE free
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef FIXED_POINT
|
||||||
|
#include <sys/types.h>
|
||||||
|
# if (FIXED_POINT == 32)
|
||||||
|
# define kiss_fft_scalar int32_t
|
||||||
|
# else
|
||||||
|
# define kiss_fft_scalar int16_t
|
||||||
|
# endif
|
||||||
|
#else
|
||||||
|
# ifndef kiss_fft_scalar
|
||||||
|
/* default is float */
|
||||||
|
# define kiss_fft_scalar float
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
kiss_fft_scalar r;
|
||||||
|
kiss_fft_scalar i;
|
||||||
|
}kiss_fft_cpx;
|
||||||
|
|
||||||
|
typedef struct kiss_fft_state* kiss_fft_cfg;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* kiss_fft_alloc
|
||||||
|
*
|
||||||
|
* Initialize a FFT (or IFFT) algorithm's cfg/state buffer.
|
||||||
|
*
|
||||||
|
* typical usage: kiss_fft_cfg mycfg=kiss_fft_alloc(1024,0,NULL,NULL);
|
||||||
|
*
|
||||||
|
* The return value from fft_alloc is a cfg buffer used internally
|
||||||
|
* by the fft routine or NULL.
|
||||||
|
*
|
||||||
|
* If lenmem is NULL, then kiss_fft_alloc will allocate a cfg buffer using malloc.
|
||||||
|
* The returned value should be free()d when done to avoid memory leaks.
|
||||||
|
*
|
||||||
|
* The state can be placed in a user supplied buffer 'mem':
|
||||||
|
* If lenmem is not NULL and mem is not NULL and *lenmem is large enough,
|
||||||
|
* then the function places the cfg in mem and the size used in *lenmem
|
||||||
|
* and returns mem.
|
||||||
|
*
|
||||||
|
* If lenmem is not NULL and ( mem is NULL or *lenmem is not large enough),
|
||||||
|
* then the function returns NULL and places the minimum cfg
|
||||||
|
* buffer size in *lenmem.
|
||||||
|
* */
|
||||||
|
|
||||||
|
kiss_fft_cfg kiss_fft_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* kiss_fft(cfg,in_out_buf)
|
||||||
|
*
|
||||||
|
* Perform an FFT on a complex input buffer.
|
||||||
|
* for a forward FFT,
|
||||||
|
* fin should be f[0] , f[1] , ... ,f[nfft-1]
|
||||||
|
* fout will be F[0] , F[1] , ... ,F[nfft-1]
|
||||||
|
* Note that each element is complex and can be accessed like
|
||||||
|
f[k].r and f[k].i
|
||||||
|
* */
|
||||||
|
void kiss_fft(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout);
|
||||||
|
|
||||||
|
/*
|
||||||
|
A more generic version of the above function. It reads its input from every Nth sample.
|
||||||
|
* */
|
||||||
|
void kiss_fft_stride(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout,int fin_stride);
|
||||||
|
|
||||||
|
/* If kiss_fft_alloc allocated a buffer, it is one contiguous
|
||||||
|
buffer and can be simply free()d when no longer needed*/
|
||||||
|
#define kiss_fft_free KISS_FFT_FREE
|
||||||
|
|
||||||
|
/*
|
||||||
|
Cleans up some memory that gets managed internally. Not necessary to call, but it might clean up
|
||||||
|
your compiler output to call this before you exit.
|
||||||
|
*/
|
||||||
|
void kiss_fft_cleanup(void);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns the smallest integer k, such that k>=n and k has only "fast" factors (2,3,5)
|
||||||
|
*/
|
||||||
|
int kiss_fft_next_fast_size(int n);
|
||||||
|
|
||||||
|
/* for real ffts, we need an even size */
|
||||||
|
#define kiss_fftr_next_fast_size_real(n) \
|
||||||
|
(kiss_fft_next_fast_size( ((n)+1)>>1)<<1)
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
Vendored
+153
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
|
||||||
|
* This file is part of KISS FFT - https://github.com/mborgerding/kissfft
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
* See COPYING file for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "kiss_fftr.h"
|
||||||
|
#include "_kiss_fft_guts.h"
|
||||||
|
|
||||||
|
struct kiss_fftr_state{
|
||||||
|
kiss_fft_cfg substate;
|
||||||
|
kiss_fft_cpx * tmpbuf;
|
||||||
|
kiss_fft_cpx * super_twiddles;
|
||||||
|
#ifdef USE_SIMD
|
||||||
|
void * pad;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
kiss_fftr_cfg st = NULL;
|
||||||
|
size_t subsize = 0, memneeded;
|
||||||
|
|
||||||
|
if (nfft & 1) {
|
||||||
|
fprintf(stderr,"Real FFT optimization must be even.\n");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
nfft >>= 1;
|
||||||
|
|
||||||
|
kiss_fft_alloc (nfft, inverse_fft, NULL, &subsize);
|
||||||
|
memneeded = sizeof(struct kiss_fftr_state) + subsize + sizeof(kiss_fft_cpx) * ( nfft * 3 / 2);
|
||||||
|
|
||||||
|
if (lenmem == NULL) {
|
||||||
|
st = (kiss_fftr_cfg) KISS_FFT_MALLOC (memneeded);
|
||||||
|
} else {
|
||||||
|
if (*lenmem >= memneeded)
|
||||||
|
st = (kiss_fftr_cfg) mem;
|
||||||
|
*lenmem = memneeded;
|
||||||
|
}
|
||||||
|
if (!st)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
st->substate = (kiss_fft_cfg) (st + 1); /*just beyond kiss_fftr_state struct */
|
||||||
|
st->tmpbuf = (kiss_fft_cpx *) (((char *) st->substate) + subsize);
|
||||||
|
st->super_twiddles = st->tmpbuf + nfft;
|
||||||
|
kiss_fft_alloc(nfft, inverse_fft, st->substate, &subsize);
|
||||||
|
|
||||||
|
for (i = 0; i < nfft/2; ++i) {
|
||||||
|
double phase =
|
||||||
|
-3.14159265358979323846264338327 * ((double) (i+1) / nfft + .5);
|
||||||
|
if (inverse_fft)
|
||||||
|
phase *= -1;
|
||||||
|
kf_cexp (st->super_twiddles+i,phase);
|
||||||
|
}
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
void kiss_fftr(kiss_fftr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata)
|
||||||
|
{
|
||||||
|
/* input buffer timedata is stored row-wise */
|
||||||
|
int k,ncfft;
|
||||||
|
kiss_fft_cpx fpnk,fpk,f1k,f2k,tw,tdc;
|
||||||
|
|
||||||
|
if ( st->substate->inverse) {
|
||||||
|
fprintf(stderr,"kiss fft usage error: improper alloc\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ncfft = st->substate->nfft;
|
||||||
|
|
||||||
|
/*perform the parallel fft of two real signals packed in real,imag*/
|
||||||
|
kiss_fft( st->substate , (const kiss_fft_cpx*)timedata, st->tmpbuf );
|
||||||
|
/* The real part of the DC element of the frequency spectrum in st->tmpbuf
|
||||||
|
* contains the sum of the even-numbered elements of the input time sequence
|
||||||
|
* The imag part is the sum of the odd-numbered elements
|
||||||
|
*
|
||||||
|
* The sum of tdc.r and tdc.i is the sum of the input time sequence.
|
||||||
|
* yielding DC of input time sequence
|
||||||
|
* The difference of tdc.r - tdc.i is the sum of the input (dot product) [1,-1,1,-1...
|
||||||
|
* yielding Nyquist bin of input time sequence
|
||||||
|
*/
|
||||||
|
|
||||||
|
tdc.r = st->tmpbuf[0].r;
|
||||||
|
tdc.i = st->tmpbuf[0].i;
|
||||||
|
C_FIXDIV(tdc,2);
|
||||||
|
CHECK_OVERFLOW_OP(tdc.r ,+, tdc.i);
|
||||||
|
CHECK_OVERFLOW_OP(tdc.r ,-, tdc.i);
|
||||||
|
freqdata[0].r = tdc.r + tdc.i;
|
||||||
|
freqdata[ncfft].r = tdc.r - tdc.i;
|
||||||
|
#ifdef USE_SIMD
|
||||||
|
freqdata[ncfft].i = freqdata[0].i = _mm_set1_ps(0);
|
||||||
|
#else
|
||||||
|
freqdata[ncfft].i = freqdata[0].i = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
for ( k=1;k <= ncfft/2 ; ++k ) {
|
||||||
|
fpk = st->tmpbuf[k];
|
||||||
|
fpnk.r = st->tmpbuf[ncfft-k].r;
|
||||||
|
fpnk.i = - st->tmpbuf[ncfft-k].i;
|
||||||
|
C_FIXDIV(fpk,2);
|
||||||
|
C_FIXDIV(fpnk,2);
|
||||||
|
|
||||||
|
C_ADD( f1k, fpk , fpnk );
|
||||||
|
C_SUB( f2k, fpk , fpnk );
|
||||||
|
C_MUL( tw , f2k , st->super_twiddles[k-1]);
|
||||||
|
|
||||||
|
freqdata[k].r = HALF_OF(f1k.r + tw.r);
|
||||||
|
freqdata[k].i = HALF_OF(f1k.i + tw.i);
|
||||||
|
freqdata[ncfft-k].r = HALF_OF(f1k.r - tw.r);
|
||||||
|
freqdata[ncfft-k].i = HALF_OF(tw.i - f1k.i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void kiss_fftri(kiss_fftr_cfg st,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata)
|
||||||
|
{
|
||||||
|
/* input buffer timedata is stored row-wise */
|
||||||
|
int k, ncfft;
|
||||||
|
|
||||||
|
if (st->substate->inverse == 0) {
|
||||||
|
fprintf (stderr, "kiss fft usage error: improper alloc\n");
|
||||||
|
exit (1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ncfft = st->substate->nfft;
|
||||||
|
|
||||||
|
st->tmpbuf[0].r = freqdata[0].r + freqdata[ncfft].r;
|
||||||
|
st->tmpbuf[0].i = freqdata[0].r - freqdata[ncfft].r;
|
||||||
|
C_FIXDIV(st->tmpbuf[0],2);
|
||||||
|
|
||||||
|
for (k = 1; k <= ncfft / 2; ++k) {
|
||||||
|
kiss_fft_cpx fk, fnkc, fek, fok, tmp;
|
||||||
|
fk = freqdata[k];
|
||||||
|
fnkc.r = freqdata[ncfft - k].r;
|
||||||
|
fnkc.i = -freqdata[ncfft - k].i;
|
||||||
|
C_FIXDIV( fk , 2 );
|
||||||
|
C_FIXDIV( fnkc , 2 );
|
||||||
|
|
||||||
|
C_ADD (fek, fk, fnkc);
|
||||||
|
C_SUB (tmp, fk, fnkc);
|
||||||
|
C_MUL (fok, tmp, st->super_twiddles[k-1]);
|
||||||
|
C_ADD (st->tmpbuf[k], fek, fok);
|
||||||
|
C_SUB (st->tmpbuf[ncfft - k], fek, fok);
|
||||||
|
#ifdef USE_SIMD
|
||||||
|
st->tmpbuf[ncfft - k].i *= _mm_set1_ps(-1.0);
|
||||||
|
#else
|
||||||
|
st->tmpbuf[ncfft - k].i *= -1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
kiss_fft (st->substate, st->tmpbuf, (kiss_fft_cpx *) timedata);
|
||||||
|
}
|
||||||
Vendored
+54
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
|
||||||
|
* This file is part of KISS FFT - https://github.com/mborgerding/kissfft
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
* See COPYING file for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KISS_FTR_H
|
||||||
|
#define KISS_FTR_H
|
||||||
|
|
||||||
|
#include "kiss_fft.h"
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Real optimized version can save about 45% cpu time vs. complex fft of a real seq.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef struct kiss_fftr_state *kiss_fftr_cfg;
|
||||||
|
|
||||||
|
|
||||||
|
kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem, size_t * lenmem);
|
||||||
|
/*
|
||||||
|
nfft must be even
|
||||||
|
|
||||||
|
If you don't care to allocate space, use mem = lenmem = NULL
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
void kiss_fftr(kiss_fftr_cfg cfg,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata);
|
||||||
|
/*
|
||||||
|
input timedata has nfft scalar points
|
||||||
|
output freqdata has nfft/2+1 complex points
|
||||||
|
*/
|
||||||
|
|
||||||
|
void kiss_fftri(kiss_fftr_cfg cfg,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata);
|
||||||
|
/*
|
||||||
|
input freqdata has nfft/2+1 complex points
|
||||||
|
output timedata has nfft scalar points
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define kiss_fftr_free KISS_FFT_FREE
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#
|
||||||
|
# On MS Windows using Msys/MinGW gfortran invoke like this:
|
||||||
|
#
|
||||||
|
# FC=gfortran make
|
||||||
|
#
|
||||||
|
# On macOS using MacPorts gfortran invoke like this:
|
||||||
|
#
|
||||||
|
# FC=gfortran make
|
||||||
|
#
|
||||||
|
# or if the gfortran compiler is named gfortran-mp-8 or similar
|
||||||
|
#
|
||||||
|
# FC=gfortran-mp-8 make
|
||||||
|
#
|
||||||
|
# otherwise invoke like this:
|
||||||
|
#
|
||||||
|
# make
|
||||||
|
#
|
||||||
|
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
EXE = .exe
|
||||||
|
endif
|
||||||
|
|
||||||
|
EXES = hashcodes$(EXE) std_call_to_c28$(EXE) nonstd_to_c58$(EXE) \
|
||||||
|
free_text_to_f71$(EXE) grid4_to_g15$(EXE) grid6_to_g25$(EXE) \
|
||||||
|
gen_crc14$(EXE)
|
||||||
|
|
||||||
|
%.o: %.f90
|
||||||
|
$(FC) -c $(FFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
all: $(EXES)
|
||||||
|
|
||||||
|
hashcodes$(EXE): hashcodes.o
|
||||||
|
${FC} -o $@ $^
|
||||||
|
|
||||||
|
std_call_to_c28$(EXE): std_call_to_c28.o
|
||||||
|
${FC} -o $@ $^
|
||||||
|
|
||||||
|
nonstd_to_c58$(EXE): nonstd_to_c58.o
|
||||||
|
${FC} -o $@ $^
|
||||||
|
|
||||||
|
free_text_to_f71$(EXE): free_text_to_f71.o
|
||||||
|
${FC} -o $@ $^
|
||||||
|
|
||||||
|
grid4_to_g15$(EXE): grid4_to_g15.o
|
||||||
|
${FC} -o $@ $^
|
||||||
|
|
||||||
|
grid6_to_g25$(EXE): grid6_to_g25.o
|
||||||
|
${FC} -o $@ $^
|
||||||
|
|
||||||
|
gen_crc14$(EXE): gen_crc14.o
|
||||||
|
${FC} -o $@ $^
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm $(EXES) *.o
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
! Abbreviations for ARRL/RAC Sections as a Fortran 90 data statement:
|
||||||
|
|
||||||
|
|
||||||
|
data csec/ &
|
||||||
|
"AB ","AK ","AL ","AR ","AZ ","BC ","CO ","CT ","DE ","EB ", &
|
||||||
|
"EMA","ENY","EPA","EWA","GA ","GTA","IA ","ID ","IL ","IN ", &
|
||||||
|
"KS ","KY ","LA ","LAX","MAR","MB ","MDC","ME ","MI ","MN ", &
|
||||||
|
"MO ","MS ","MT ","NC ","ND ","NE ","NFL","NH ","NL ","NLI", &
|
||||||
|
"NM ","NNJ","NNY","NT ","NTX","NV ","OH ","OK ","ONE","ONN", &
|
||||||
|
"ONS","OR ","ORG","PAC","PR ","QC ","RI ","SB ","SC ","SCV", &
|
||||||
|
"SD ","SDG","SF ","SFL","SJV","SK ","SNJ","STX","SV ","TN ", &
|
||||||
|
"UT ","VA ","VI ","VT ","WCF","WI ","WMA","WNY","WPA","WTX", &
|
||||||
|
"WV ","WWA","WY ","DX "/
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
program free_text_to_f71
|
||||||
|
|
||||||
|
character*13 c13,w
|
||||||
|
character*71 f71
|
||||||
|
character*42 c
|
||||||
|
character*1 qa(10),qb(10)
|
||||||
|
data c/' 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?'/
|
||||||
|
|
||||||
|
nargs=iargc()
|
||||||
|
if(nargs.ne.1) then
|
||||||
|
print*,'Usage: free_text_to_f71 "<message>"'
|
||||||
|
print*,'Example: free_text_to_f71 "TNX BOB 73 GL"'
|
||||||
|
go to 999
|
||||||
|
endif
|
||||||
|
call getarg(1,c13)
|
||||||
|
call mp_short_init
|
||||||
|
qa=char(0)
|
||||||
|
w=adjustr(c13)
|
||||||
|
do i=1,13
|
||||||
|
j=index(c,w(i:i))-1
|
||||||
|
if(j.lt.0) j=0
|
||||||
|
call mp_short_mult(qb,qa(2:10),9,42) !qb(1:9)=42*qa(2:9)
|
||||||
|
call mp_short_add(qa,qb(2:10),9,j) !qa(1:9)=qb(2:9)+j
|
||||||
|
enddo
|
||||||
|
write(f71,1000) qa(2:10)
|
||||||
|
1000 format(b7.7,8b8.8)
|
||||||
|
write(*,1010) c13,f71
|
||||||
|
1010 format('Free text: ',a13/'f71: ',a71)
|
||||||
|
|
||||||
|
999 end program free_text_to_f71
|
||||||
|
|
||||||
|
subroutine mp_short_ops(w,u)
|
||||||
|
! Multi-precision arithmetic with storage in character arrays.
|
||||||
|
character*1 w(*),u(*)
|
||||||
|
integer i,ireg,j,n,ir,iv,ii1,ii2
|
||||||
|
character*1 creg(4)
|
||||||
|
save ii1,ii2
|
||||||
|
equivalence (ireg,creg)
|
||||||
|
|
||||||
|
entry mp_short_init
|
||||||
|
ireg=256*ichar('2')+ichar('1')
|
||||||
|
do j=1,4
|
||||||
|
if (creg(j).eq.'1') ii1=j
|
||||||
|
if (creg(j).eq.'2') ii2=j
|
||||||
|
enddo
|
||||||
|
return
|
||||||
|
|
||||||
|
entry mp_short_add(w,u,n,iv)
|
||||||
|
ireg=256*iv
|
||||||
|
do j=n,1,-1
|
||||||
|
ireg=ichar(u(j))+ichar(creg(ii2))
|
||||||
|
w(j+1)=creg(ii1)
|
||||||
|
enddo
|
||||||
|
w(1)=creg(ii2)
|
||||||
|
return
|
||||||
|
|
||||||
|
entry mp_short_mult(w,u,n,iv)
|
||||||
|
ireg=0
|
||||||
|
do j=n,1,-1
|
||||||
|
ireg=ichar(u(j))*iv+ichar(creg(ii2))
|
||||||
|
w(j+1)=creg(ii1)
|
||||||
|
enddo
|
||||||
|
w(1)=creg(ii2)
|
||||||
|
return
|
||||||
|
|
||||||
|
return
|
||||||
|
end subroutine mp_short_ops
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
program gen_crc14
|
||||||
|
|
||||||
|
character m77*77,c14*14
|
||||||
|
|
||||||
|
integer mc(96),r(15),p(15),ncrc
|
||||||
|
! polynomial for 14-bit CRC 0x6757
|
||||||
|
data p/1,1,0,0,1,1,1,0,1,0,1,0,1,1,1/
|
||||||
|
|
||||||
|
nargs=iargc()
|
||||||
|
if(nargs.ne.1) then
|
||||||
|
print*,'Usage: gen_crc14 <77-bit message>'
|
||||||
|
print*,'Example: gen_crc14 "00000000000000000000000000100000010011011111110011011100100010100001010000001"'
|
||||||
|
go to 999
|
||||||
|
endif
|
||||||
|
|
||||||
|
! pad the 77bit message out to 96 bits
|
||||||
|
call getarg(1,m77)
|
||||||
|
read(m77,'(77i1)') mc(1:77)
|
||||||
|
mc(78:96)=0
|
||||||
|
|
||||||
|
! divide by polynomial
|
||||||
|
r=mc(1:15)
|
||||||
|
do i=0,81
|
||||||
|
r(15)=mc(i+15)
|
||||||
|
r=mod(r+r(1)*p,2)
|
||||||
|
r=cshift(r,1)
|
||||||
|
enddo
|
||||||
|
|
||||||
|
! the crc is in r(1:14) - print it in various ways:
|
||||||
|
write(c14,'(14b1)') r(1:14)
|
||||||
|
write(*,'(a40,1x,a14)') 'crc14 as a string: ',c14
|
||||||
|
read(c14,'(b14.14)') ncrc
|
||||||
|
write(*,'(a40,i6)') 'crc14 as an integer: ',ncrc
|
||||||
|
write(*,'(a40,1x,b14.14)') 'binary representation of the integer: ',ncrc
|
||||||
|
|
||||||
|
999 end program gen_crc14
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
This file contains the generator matrix for the FT8/FT4 (174,91) LDPC code.
|
||||||
|
The matrix has 91 columns and 83 rows.
|
||||||
|
|
||||||
|
1000001100101001110011100001000110111111001100011110101011110101000010011111001001111111110
|
||||||
|
0111011000011100001001100100111000100101110000100101100100110011010101001001001100010011001
|
||||||
|
1101110000100110010110010000001011111011001001110111110001100100000100001010000110111101110
|
||||||
|
0001101100111111010000010111100001011000110011010010110111010011001111101100011111110110001
|
||||||
|
0000100111111101101001001111111011100000010000011001010111111101000000110100011110000011101
|
||||||
|
0000011101111100110011001100000100011011100010000111001111101101010111000011110101001000101
|
||||||
|
0010100110110110001010101111111000111100101000000011011011110100111111100001101010011101101
|
||||||
|
0110000001010100111110101111010111110011010111011001011011010011101100001100100011000011111
|
||||||
|
1110001000000111100110001110010000110001000011101110110100100111100010000100101011101001000
|
||||||
|
0111011101011100100111000000100011101000000011100010011011011101101011100101011000110001100
|
||||||
|
1011000010111000000100010000001010001100001010111111100110010111001000010011010010000111110
|
||||||
|
0001100010100000110010010010001100011111110001100000101011011111010111000101111010100011001
|
||||||
|
0111011001000111000111101000001100000010101000000111001000011110000000011011000100101011100
|
||||||
|
1111111110111100110010111000000011001010100000110100000111111010111110110100011110110010111
|
||||||
|
0110011010100111001010100001010110001111100100110010010110100010101111110110011100010111000
|
||||||
|
1100010000100100001101101000100111111110100001011011000111000101000100110110001110100001100
|
||||||
|
0000110111111111011100111001010000010100110100011010000110110011010010110001110000100111000
|
||||||
|
0001010110110100100010000011000001100011011011001000101110011001100010010100100101110010111
|
||||||
|
0010100110101000100111000000110100111101111010000001110101100110010101001000100110110000111
|
||||||
|
0100111100010010011011110011011111111010010100011100101111100110000110111101011010111001010
|
||||||
|
1001100111000100011100100011100111010000110110010111110100111100100001001110000010010100000
|
||||||
|
0001100100011001101101110101000100011001011101100101011000100001101110110100111100011110100
|
||||||
|
0000100111011011000100101101011100110001111110101110111000001011100001101101111101101011100
|
||||||
|
0100100010001111110000110011110111110100001111111011110111101110101001001110101011111011010
|
||||||
|
1000001001110100001000111110111001000000101101100111010111110111010101101110101101011111111
|
||||||
|
1010101111100001100101111100010010000100110010110111010001110101011100010100010010101001101
|
||||||
|
0010101101010000000011100100101111000000111011000101101001101101001010111101101111011101000
|
||||||
|
1100010001110100101010100101001111010111000000100001100001110110000101100110100100110110000
|
||||||
|
1000111010111010000110100001001111011011001100111001000010111101011001110001100011001110110
|
||||||
|
0111010100111000010001000110011100111010001001110111100000101100110001000010000000010010111
|
||||||
|
0000011011111111100000111010000101000101110000110111000000110101101001011100000100100110100
|
||||||
|
0011101100110111010000010111100001011000110011000010110111010011001111101100001111110110001
|
||||||
|
1001101001001010010110100010100011101110000101111100101010011100001100100100100001000010110
|
||||||
|
1011110000101001111101000110010100110000100111001001011101111110100010010110000100001010010
|
||||||
|
0010011001100011101011100110110111011111100010110101110011100010101110110010100101001000100
|
||||||
|
0100011011110010001100011110111111100100010101110000001101001100000110000001010001000001100
|
||||||
|
0011111110110010110011101000010110101011111010011011000011000111001011100000011011111011111
|
||||||
|
1101111010000111010010000001111100101000001011000001010100111001011100011010000010100010111
|
||||||
|
1111110011010111110011001111001000111100011010011111101010011001101110111010000101000001001
|
||||||
|
1111000000100110000101000100011111101001010010010000110010101000111001000111010011001110110
|
||||||
|
0100010000010000000100010101100000011000000110010110111110010101110011011101011100000001001
|
||||||
|
0000100010001111110000110001110111110100101111111011110111100010101001001110101011111011010
|
||||||
|
1011100011111110111100011011011000110000011101110010100111111011000010100000011110001100000
|
||||||
|
0101101011111110101001111010110011001100101101110111101110111100100111011001100110101001000
|
||||||
|
0100100110100111000000010110101011000110010100111111011001011110110011011100100100000111011
|
||||||
|
0001100101000100110100001000010110111110010011100111110110101000110101101100110001111101000
|
||||||
|
0010010100011111011000101010110111000100000000110010111100001110111001110001010000000000001
|
||||||
|
0101011001000111000111111000011100000010101000000111001000011110000000001011000100101011100
|
||||||
|
0010101110001110010010010010001111110010110111010101000111100010110101010011011111111010000
|
||||||
|
0110101101010101000010100100000010100110011011110100011101010101110111101001010111000010011
|
||||||
|
1010000110001010110100101000110101001110001001111111111010010010101001001111011011001000010
|
||||||
|
0001000011000010111001011000011000111000100011001011100000101010001111011000000001110101100
|
||||||
|
1110111100110100101001000001100000010111111011100000001000010011001111011011001011101011000
|
||||||
|
0111111010011100000011000101010000110010010110101001110000010101100000110110111000000000000
|
||||||
|
0011011010010011111001010111001011010001111111011110010011001101111100000111100111101000011
|
||||||
|
1011111110110010110011101100010110101011111000011011000011000111001011100000011111111011111
|
||||||
|
0111111011100001100000100011000011000101100000111100110011001100010101111101010010110000100
|
||||||
|
1010000001100110110010110010111111101101101011111100100111110101001001100110010000010010011
|
||||||
|
1011101100100011011100100101101010111100010001111100110001011111010011001100010011001101001
|
||||||
|
1101111011011001110110111010001110111110111001000000110001011001101101010110000010011011010
|
||||||
|
1101100110100111000000010110101011000110010100111110011011011110110011011100100100000011011
|
||||||
|
1001101011010100011010101110110101011111011100000111111100101000000010101011010111111100010
|
||||||
|
1110010110010010000111000111011110000010001001011000011100110001011011010111110100111100001
|
||||||
|
0100111100010100110110101000001001000010101010001011100001101101110010100111001100110101001
|
||||||
|
1000101110001011010100000111101011010100011001111101010001000100000111011111011101110000111
|
||||||
|
0010001010000011000111001001110011110001000101101001010001100111101011010000010010110110100
|
||||||
|
0010000100111011100000111000111111100010101011100101010011000011100011101110011100011000000
|
||||||
|
0101110110010010011010110110110111010111000111110000100001010001100000011010010011100001001
|
||||||
|
0110011010101011011110011101010010110010100111101110011011100110100101010000100111100101011
|
||||||
|
1001010110000001010010000110100000101101011101001000101000111000110111010110100010111010101
|
||||||
|
1011100011001110000000100000110011110000011010011100001100101010011100100011101010110001010
|
||||||
|
1111010000110011000111010110110101000110000101100000011111101001010101110101001001110100011
|
||||||
|
0110110110100010001110111010010000100100101110010101100101100001001100111100111110011100100
|
||||||
|
1010011000110110101111001011110001111011001100001100010111111011111010101110011001111111111
|
||||||
|
0101110010110000110110000110101000000111110111110110010101001010100100001000100110100010000
|
||||||
|
1111000100011111000100000110100001001000011110000000111111001001111011001101110110000000101
|
||||||
|
0001111110111011010100110110010011111011100011010010110010011101011100110000110101011011101
|
||||||
|
1111110010111000011010111100011100001010010100001100100111010000001010100101110100000011010
|
||||||
|
1010010100110100010000110011000000101001111010101100000101011111001100100010111000110100110
|
||||||
|
1100100110001001110110011100011111000011110100111011100011000101010111010111010100010011000
|
||||||
|
0111101110110011100010110010111100000001100001101101010001100110010000111010111010010110001
|
||||||
|
0010011001000100111010111010110111101011010001001011100101000110011111010001111101000010110
|
||||||
|
0110000010001100110010000101011101011001010010111111101110110101010111010110100101100000000
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
program grid4_to_g15
|
||||||
|
|
||||||
|
parameter (MAXGRID4=32400)
|
||||||
|
character*4 w,grid4
|
||||||
|
character c1*1,c2*2
|
||||||
|
logical is_grid4
|
||||||
|
is_grid4(grid4)=len(trim(grid4)).eq.4 .and. &
|
||||||
|
grid4(1:1).ge.'A' .and. grid4(1:1).le.'R' .and. &
|
||||||
|
grid4(2:2).ge.'A' .and. grid4(2:2).le.'R' .and. &
|
||||||
|
grid4(3:3).ge.'0' .and. grid4(3:3).le.'9' .and. &
|
||||||
|
grid4(4:4).ge.'0' .and. grid4(4:4).le.'9'
|
||||||
|
|
||||||
|
nargs=iargc()
|
||||||
|
if(nargs.ne.1) then
|
||||||
|
print*,'Convert a 4-character grid, signal report, etc., to a g15 value.'
|
||||||
|
print*,'Usage examples:'
|
||||||
|
print*,'grid4_to_g15 FN20'
|
||||||
|
print*,'grid4_to_g15 -11'
|
||||||
|
print*,'grid4_to_g15 +02'
|
||||||
|
print*,'grid4_to_g15 RRR'
|
||||||
|
print*,'grid4_to_g15 RR73'
|
||||||
|
print*,'grid4_to_g15 73'
|
||||||
|
print*,'grid4_to_g15 ""'
|
||||||
|
go to 999
|
||||||
|
endif
|
||||||
|
call getarg(1,w)
|
||||||
|
if(is_grid4(w) .and. w.ne.'RR73') then
|
||||||
|
j1=(ichar(w(1:1))-ichar('A'))*18*10*10
|
||||||
|
j2=(ichar(w(2:2))-ichar('A'))*10*10
|
||||||
|
j3=(ichar(w(3:3))-ichar('0'))*10
|
||||||
|
j4=(ichar(w(4:4))-ichar('0'))
|
||||||
|
igrid4=j1+j2+j3+j4
|
||||||
|
else
|
||||||
|
c1=w(1:1)
|
||||||
|
if(c1.ne.'+' .and. c1.ne.'-'.and. trim(w).ne.'RRR' .and. w.ne.'RR73' &
|
||||||
|
.and. trim(w).ne.'73' .and. len(trim(w)).ne.0) go to 900
|
||||||
|
if(c1.eq.'+' .or. c1.eq.'-') then
|
||||||
|
read(w,*,err=900) irpt
|
||||||
|
irpt=irpt+35
|
||||||
|
endif
|
||||||
|
if(len(trim(w)).eq.0) irpt=1
|
||||||
|
if(trim(w).eq.'RRR') irpt=2
|
||||||
|
if(w.eq.'RR73') irpt=3
|
||||||
|
if(trim(w).eq.'73') irpt=4
|
||||||
|
igrid4=MAXGRID4 + irpt
|
||||||
|
endif
|
||||||
|
|
||||||
|
write(*,1000) w,igrid4,igrid4
|
||||||
|
1000 format('Encoded word: ',a4,' g15 in binary: ',b15.15,' decimal:',i6)
|
||||||
|
go to 999
|
||||||
|
|
||||||
|
900 write(*,1900)
|
||||||
|
1900 format('Invalid input')
|
||||||
|
|
||||||
|
999 end program grid4_to_g15
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
program grid6_to_g25
|
||||||
|
|
||||||
|
parameter (MAXGRID4=32400)
|
||||||
|
character*6 w,grid6
|
||||||
|
character c1*1,c2*2
|
||||||
|
logical is_grid6
|
||||||
|
|
||||||
|
is_grid6(grid6)=len(trim(grid6)).eq.6 .and. &
|
||||||
|
grid6(1:1).ge.'A' .and. grid6(1:1).le.'R' .and. &
|
||||||
|
grid6(2:2).ge.'A' .and. grid6(2:2).le.'R' .and. &
|
||||||
|
grid6(3:3).ge.'0' .and. grid6(3:3).le.'9' .and. &
|
||||||
|
grid6(4:4).ge.'0' .and. grid6(4:4).le.'9' .and. &
|
||||||
|
grid6(5:5).ge.'A' .and. grid6(5:5).le.'X' .and. &
|
||||||
|
grid6(6:6).ge.'A' .and. grid6(6:6).le.'X'
|
||||||
|
|
||||||
|
nargs=iargc()
|
||||||
|
if(nargs.ne.1) then
|
||||||
|
print*,'Convert a 6-character grid to a g25 value.'
|
||||||
|
print*,'Usage: grid6_to_g25 IO91NP'
|
||||||
|
go to 999
|
||||||
|
endif
|
||||||
|
call getarg(1,w)
|
||||||
|
if(.not. is_grid6(w)) go to 900
|
||||||
|
|
||||||
|
j1=(ichar(w(1:1))-ichar('A'))*18*10*10*24*24
|
||||||
|
j2=(ichar(w(2:2))-ichar('A'))*10*10*24*24
|
||||||
|
j3=(ichar(w(3:3))-ichar('0'))*10*24*24
|
||||||
|
j4=(ichar(w(4:4))-ichar('0'))*24*24
|
||||||
|
j5=(ichar(w(5:5))-ichar('A'))*24
|
||||||
|
j6=(ichar(w(6:6))-ichar('A'))
|
||||||
|
igrid6=j1+j2+j3+j4+j5+j6
|
||||||
|
|
||||||
|
write(*,1000) w,igrid6,igrid6
|
||||||
|
1000 format('Encoded word: ',a6,' g25 in binary: ',b25.25/ &
|
||||||
|
30x,'decimal:',i9)
|
||||||
|
go to 999
|
||||||
|
|
||||||
|
900 write(*,1900)
|
||||||
|
1900 format('Invalid input')
|
||||||
|
|
||||||
|
999 end program grid6_to_g25
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
program hashcodes
|
||||||
|
|
||||||
|
parameter (NTOKENS=2063592)
|
||||||
|
integer*8 nprime,n8(3)
|
||||||
|
integer nbits(3),ihash(3)
|
||||||
|
character*11 callsign
|
||||||
|
character*38 c
|
||||||
|
data c/' 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/'/
|
||||||
|
data nprime/47055833459_8/,nbits/10,12,22/
|
||||||
|
|
||||||
|
nargs=iargc()
|
||||||
|
if(nargs.ne.1) then
|
||||||
|
print*,'Usage: hashcodes <callsign>'
|
||||||
|
print*,'Examples: hashcodes PJ4/K1ABC'
|
||||||
|
print*,' hashcodes YW18FIFA'
|
||||||
|
go to 999
|
||||||
|
endif
|
||||||
|
call getarg(1,callsign)
|
||||||
|
callsign=adjustl(callsign)
|
||||||
|
|
||||||
|
do k=1,3
|
||||||
|
n8(k)=0
|
||||||
|
do i=1,11
|
||||||
|
j=index(c,callsign(i:i)) - 1
|
||||||
|
n8(k)=38*n8(k) + j
|
||||||
|
enddo
|
||||||
|
ihash(k)=ishft(nprime*n8(k),nbits(k)-64)
|
||||||
|
enddo
|
||||||
|
ih22_biased=ihash(3) + NTOKENS
|
||||||
|
write(*,1000) callsign,ihash,ih22_biased
|
||||||
|
1000 format('Callsign',9x,'h10',7x,'h12',7x,'h22'/41('-')/ &
|
||||||
|
a11,i9,2i10,/'Biased for storage in c28:',i14)
|
||||||
|
|
||||||
|
999 end program hashcodes
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
program nonstd_to_c58
|
||||||
|
|
||||||
|
integer*8 n58
|
||||||
|
character*11 callsign
|
||||||
|
character*38 c
|
||||||
|
data c/' 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/'/
|
||||||
|
|
||||||
|
nargs=iargc()
|
||||||
|
if(nargs.ne.1) then
|
||||||
|
print*,'Usage: nonstd_to_c58 <callsign>'
|
||||||
|
print*,'Examples: nonstd_to_c58 PJ4/K1ABC'
|
||||||
|
print*,' nonstd_to_c58 YW18FIFA'
|
||||||
|
go to 999
|
||||||
|
endif
|
||||||
|
call getarg(1,callsign)
|
||||||
|
|
||||||
|
n58=0
|
||||||
|
do i=1,11
|
||||||
|
n58=n58*38 + index(c,callsign(i:i)) - 1
|
||||||
|
enddo
|
||||||
|
write(*,1000) callsign,n58,n58
|
||||||
|
1000 format('Callsign: ',a11/'c58 (binary): ' b58.58/'c58 (decimal):',i20)
|
||||||
|
|
||||||
|
999 end program nonstd_to_c58
|
||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
This file specifies the sparse 83x174 parity-check matrix for the
|
||||||
|
FT8/FT4 (174,91) LDPC code. Each of the 174 columns contains
|
||||||
|
exactly 3 ones. The rows contain either 6 or 7 ones.
|
||||||
|
The matrix is specified by the following list consisting of
|
||||||
|
174 lines, each of which includes 3 numbers.
|
||||||
|
Each line corresponds to a column of the parity check matrix.
|
||||||
|
The three numbers are indices of the rows that contain a one in
|
||||||
|
the corresponding column. The indices range from 1 through 83.
|
||||||
|
|
||||||
|
16 45 73
|
||||||
|
25 51 62
|
||||||
|
33 58 78
|
||||||
|
1 44 45
|
||||||
|
2 7 61
|
||||||
|
3 6 54
|
||||||
|
4 35 48
|
||||||
|
5 13 21
|
||||||
|
8 56 79
|
||||||
|
9 64 69
|
||||||
|
10 19 66
|
||||||
|
11 36 60
|
||||||
|
12 37 58
|
||||||
|
14 32 43
|
||||||
|
15 63 80
|
||||||
|
17 28 77
|
||||||
|
18 74 83
|
||||||
|
22 53 81
|
||||||
|
23 30 34
|
||||||
|
24 31 40
|
||||||
|
26 41 76
|
||||||
|
27 57 70
|
||||||
|
29 49 65
|
||||||
|
3 38 78
|
||||||
|
5 39 82
|
||||||
|
46 50 73
|
||||||
|
51 52 74
|
||||||
|
55 71 72
|
||||||
|
44 67 72
|
||||||
|
43 68 78
|
||||||
|
1 32 59
|
||||||
|
2 6 71
|
||||||
|
4 16 54
|
||||||
|
7 65 67
|
||||||
|
8 30 42
|
||||||
|
9 22 31
|
||||||
|
10 18 76
|
||||||
|
11 23 82
|
||||||
|
12 28 61
|
||||||
|
13 52 79
|
||||||
|
14 50 51
|
||||||
|
15 81 83
|
||||||
|
17 29 60
|
||||||
|
19 33 64
|
||||||
|
20 26 73
|
||||||
|
21 34 40
|
||||||
|
24 27 77
|
||||||
|
25 55 58
|
||||||
|
35 53 66
|
||||||
|
36 48 68
|
||||||
|
37 46 75
|
||||||
|
38 45 47
|
||||||
|
39 57 69
|
||||||
|
41 56 62
|
||||||
|
20 49 53
|
||||||
|
46 52 63
|
||||||
|
45 70 75
|
||||||
|
27 35 80
|
||||||
|
1 15 30
|
||||||
|
2 68 80
|
||||||
|
3 36 51
|
||||||
|
4 28 51
|
||||||
|
5 31 56
|
||||||
|
6 20 37
|
||||||
|
7 40 82
|
||||||
|
8 60 69
|
||||||
|
9 10 49
|
||||||
|
11 44 57
|
||||||
|
12 39 59
|
||||||
|
13 24 55
|
||||||
|
14 21 65
|
||||||
|
16 71 78
|
||||||
|
17 30 76
|
||||||
|
18 25 80
|
||||||
|
19 61 83
|
||||||
|
22 38 77
|
||||||
|
23 41 50
|
||||||
|
7 26 58
|
||||||
|
29 32 81
|
||||||
|
33 40 73
|
||||||
|
18 34 48
|
||||||
|
13 42 64
|
||||||
|
5 26 43
|
||||||
|
47 69 72
|
||||||
|
54 55 70
|
||||||
|
45 62 68
|
||||||
|
10 63 67
|
||||||
|
14 66 72
|
||||||
|
22 60 74
|
||||||
|
35 39 79
|
||||||
|
1 46 64
|
||||||
|
1 24 66
|
||||||
|
2 5 70
|
||||||
|
3 31 65
|
||||||
|
4 49 58
|
||||||
|
1 4 5
|
||||||
|
6 60 67
|
||||||
|
7 32 75
|
||||||
|
8 48 82
|
||||||
|
9 35 41
|
||||||
|
10 39 62
|
||||||
|
11 14 61
|
||||||
|
12 71 74
|
||||||
|
13 23 78
|
||||||
|
11 35 55
|
||||||
|
15 16 79
|
||||||
|
7 9 16
|
||||||
|
17 54 63
|
||||||
|
18 50 57
|
||||||
|
19 30 47
|
||||||
|
20 64 80
|
||||||
|
21 28 69
|
||||||
|
22 25 43
|
||||||
|
13 22 37
|
||||||
|
2 47 51
|
||||||
|
23 54 74
|
||||||
|
26 34 72
|
||||||
|
27 36 37
|
||||||
|
21 36 63
|
||||||
|
29 40 44
|
||||||
|
19 26 57
|
||||||
|
3 46 82
|
||||||
|
14 15 58
|
||||||
|
33 52 53
|
||||||
|
30 43 52
|
||||||
|
6 9 52
|
||||||
|
27 33 65
|
||||||
|
25 69 73
|
||||||
|
38 55 83
|
||||||
|
20 39 77
|
||||||
|
18 29 56
|
||||||
|
32 48 71
|
||||||
|
42 51 59
|
||||||
|
28 44 79
|
||||||
|
34 60 62
|
||||||
|
31 45 61
|
||||||
|
46 68 77
|
||||||
|
6 24 76
|
||||||
|
8 10 78
|
||||||
|
40 41 70
|
||||||
|
17 50 53
|
||||||
|
42 66 68
|
||||||
|
4 22 72
|
||||||
|
36 64 81
|
||||||
|
13 29 47
|
||||||
|
2 8 81
|
||||||
|
56 67 73
|
||||||
|
5 38 50
|
||||||
|
12 38 64
|
||||||
|
59 72 80
|
||||||
|
3 26 79
|
||||||
|
45 76 81
|
||||||
|
1 65 74
|
||||||
|
7 18 77
|
||||||
|
11 56 59
|
||||||
|
14 39 54
|
||||||
|
16 37 66
|
||||||
|
10 28 55
|
||||||
|
15 60 70
|
||||||
|
17 25 82
|
||||||
|
20 30 31
|
||||||
|
12 67 68
|
||||||
|
23 75 80
|
||||||
|
27 32 62
|
||||||
|
24 69 75
|
||||||
|
19 21 71
|
||||||
|
34 53 61
|
||||||
|
35 46 47
|
||||||
|
33 59 76
|
||||||
|
40 43 83
|
||||||
|
41 42 63
|
||||||
|
49 75 83
|
||||||
|
20 44 48
|
||||||
|
42 49 57
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
! Abbreviations for US States and Canadian Provinces as a Fortran 90
|
||||||
|
! data statement:
|
||||||
|
|
||||||
|
data cmult/ &
|
||||||
|
"AL ","AK ","AZ ","AR ","CA ","CO ","CT ","DE ","FL ","GA ", &
|
||||||
|
"HI ","ID ","IL ","IN ","IA ","KS ","KY ","LA ","ME ","MD ", &
|
||||||
|
"MA ","MI ","MN ","MS ","MO ","MT ","NE ","NV ","NH ","NJ ", &
|
||||||
|
"NM ","NY ","NC ","ND ","OH ","OK ","OR ","PA ","RI ","SC ", &
|
||||||
|
"SD ","TN ","TX ","UT ","VT ","VA ","WA ","WV ","WI ","WY ", &
|
||||||
|
"NB ","NS ","QC ","ON ","MB ","SK ","AB ","BC ","NWT","NF ", &
|
||||||
|
"LB ","NU ","YT ","PEI","DC "/
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
program std_call_to_c28
|
||||||
|
|
||||||
|
parameter (NTOKENS=2063592,MAX22=4194304)
|
||||||
|
character*6 call_std
|
||||||
|
character a1*37,a2*36,a3*10,a4*27
|
||||||
|
data a1/' 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'/
|
||||||
|
data a2/'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'/
|
||||||
|
data a3/'0123456789'/
|
||||||
|
data a4/' ABCDEFGHIJKLMNOPQRSTUVWXYZ'/
|
||||||
|
|
||||||
|
nargs=iargc()
|
||||||
|
if(nargs.ne.1) then
|
||||||
|
print*,'Usage: std_call_to_c28 <call_std>'
|
||||||
|
print*,'Example: std_call_to_c28 K1ABC'
|
||||||
|
go to 999
|
||||||
|
endif
|
||||||
|
call getarg(1,call_std)
|
||||||
|
call_std=adjustr(call_std)
|
||||||
|
i1=index(a1,call_std(1:1))-1
|
||||||
|
i2=index(a2,call_std(2:2))-1
|
||||||
|
i3=index(a3,call_std(3:3))-1
|
||||||
|
i4=index(a4,call_std(4:4))-1
|
||||||
|
i5=index(a4,call_std(5:5))-1
|
||||||
|
i6=index(a4,call_std(6:6))-1
|
||||||
|
n28=NTOKENS + MAX22 + 36*10*27*27*27*i1 + 10*27*27*27*i2 + &
|
||||||
|
27*27*27*i3 + 27*27*i4 + 27*i5 + i6
|
||||||
|
|
||||||
|
write(*,1000) call_std,n28
|
||||||
|
1000 format('Callsign: ',a6,2x,'c28 as decimal integer:',i10)
|
||||||
|
|
||||||
|
999 end program std_call_to_c28
|
||||||
Vendored
+392
@@ -0,0 +1,392 @@
|
|||||||
|
#include "constants.h"
|
||||||
|
|
||||||
|
// Costas sync tone pattern
|
||||||
|
const uint8_t kFT8_Costas_pattern[7] = { 3, 1, 4, 0, 6, 5, 2 };
|
||||||
|
const uint8_t kFT4_Costas_pattern[4][4] = {
|
||||||
|
{ 0, 1, 3, 2 },
|
||||||
|
{ 1, 0, 2, 3 },
|
||||||
|
{ 2, 3, 1, 0 },
|
||||||
|
{ 3, 2, 0, 1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gray code map (FTx bits -> channel symbols)
|
||||||
|
const uint8_t kFT8_Gray_map[8] = { 0, 1, 3, 2, 5, 6, 4, 7 };
|
||||||
|
const uint8_t kFT4_Gray_map[4] = { 0, 1, 3, 2 };
|
||||||
|
|
||||||
|
const uint8_t kFT4_XOR_sequence[10] = {
|
||||||
|
0x4Au, // 01001010
|
||||||
|
0x5Eu, // 01011110
|
||||||
|
0x89u, // 10001001
|
||||||
|
0xB4u, // 10110100
|
||||||
|
0xB0u, // 10110000
|
||||||
|
0x8Au, // 10001010
|
||||||
|
0x79u, // 01111001
|
||||||
|
0x55u, // 01010101
|
||||||
|
0xBEu, // 10111110
|
||||||
|
0x28u, // 00101 [000]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parity generator matrix for (174,91) LDPC code, stored in bitpacked format (MSB first)
|
||||||
|
const uint8_t kFTX_LDPC_generator[FTX_LDPC_M][FTX_LDPC_K_BYTES] = {
|
||||||
|
{ 0x83, 0x29, 0xce, 0x11, 0xbf, 0x31, 0xea, 0xf5, 0x09, 0xf2, 0x7f, 0xc0 },
|
||||||
|
{ 0x76, 0x1c, 0x26, 0x4e, 0x25, 0xc2, 0x59, 0x33, 0x54, 0x93, 0x13, 0x20 },
|
||||||
|
{ 0xdc, 0x26, 0x59, 0x02, 0xfb, 0x27, 0x7c, 0x64, 0x10, 0xa1, 0xbd, 0xc0 },
|
||||||
|
{ 0x1b, 0x3f, 0x41, 0x78, 0x58, 0xcd, 0x2d, 0xd3, 0x3e, 0xc7, 0xf6, 0x20 },
|
||||||
|
{ 0x09, 0xfd, 0xa4, 0xfe, 0xe0, 0x41, 0x95, 0xfd, 0x03, 0x47, 0x83, 0xa0 },
|
||||||
|
{ 0x07, 0x7c, 0xcc, 0xc1, 0x1b, 0x88, 0x73, 0xed, 0x5c, 0x3d, 0x48, 0xa0 },
|
||||||
|
{ 0x29, 0xb6, 0x2a, 0xfe, 0x3c, 0xa0, 0x36, 0xf4, 0xfe, 0x1a, 0x9d, 0xa0 },
|
||||||
|
{ 0x60, 0x54, 0xfa, 0xf5, 0xf3, 0x5d, 0x96, 0xd3, 0xb0, 0xc8, 0xc3, 0xe0 },
|
||||||
|
{ 0xe2, 0x07, 0x98, 0xe4, 0x31, 0x0e, 0xed, 0x27, 0x88, 0x4a, 0xe9, 0x00 },
|
||||||
|
{ 0x77, 0x5c, 0x9c, 0x08, 0xe8, 0x0e, 0x26, 0xdd, 0xae, 0x56, 0x31, 0x80 },
|
||||||
|
{ 0xb0, 0xb8, 0x11, 0x02, 0x8c, 0x2b, 0xf9, 0x97, 0x21, 0x34, 0x87, 0xc0 },
|
||||||
|
{ 0x18, 0xa0, 0xc9, 0x23, 0x1f, 0xc6, 0x0a, 0xdf, 0x5c, 0x5e, 0xa3, 0x20 },
|
||||||
|
{ 0x76, 0x47, 0x1e, 0x83, 0x02, 0xa0, 0x72, 0x1e, 0x01, 0xb1, 0x2b, 0x80 },
|
||||||
|
{ 0xff, 0xbc, 0xcb, 0x80, 0xca, 0x83, 0x41, 0xfa, 0xfb, 0x47, 0xb2, 0xe0 },
|
||||||
|
{ 0x66, 0xa7, 0x2a, 0x15, 0x8f, 0x93, 0x25, 0xa2, 0xbf, 0x67, 0x17, 0x00 },
|
||||||
|
{ 0xc4, 0x24, 0x36, 0x89, 0xfe, 0x85, 0xb1, 0xc5, 0x13, 0x63, 0xa1, 0x80 },
|
||||||
|
{ 0x0d, 0xff, 0x73, 0x94, 0x14, 0xd1, 0xa1, 0xb3, 0x4b, 0x1c, 0x27, 0x00 },
|
||||||
|
{ 0x15, 0xb4, 0x88, 0x30, 0x63, 0x6c, 0x8b, 0x99, 0x89, 0x49, 0x72, 0xe0 },
|
||||||
|
{ 0x29, 0xa8, 0x9c, 0x0d, 0x3d, 0xe8, 0x1d, 0x66, 0x54, 0x89, 0xb0, 0xe0 },
|
||||||
|
{ 0x4f, 0x12, 0x6f, 0x37, 0xfa, 0x51, 0xcb, 0xe6, 0x1b, 0xd6, 0xb9, 0x40 },
|
||||||
|
{ 0x99, 0xc4, 0x72, 0x39, 0xd0, 0xd9, 0x7d, 0x3c, 0x84, 0xe0, 0x94, 0x00 },
|
||||||
|
{ 0x19, 0x19, 0xb7, 0x51, 0x19, 0x76, 0x56, 0x21, 0xbb, 0x4f, 0x1e, 0x80 },
|
||||||
|
{ 0x09, 0xdb, 0x12, 0xd7, 0x31, 0xfa, 0xee, 0x0b, 0x86, 0xdf, 0x6b, 0x80 },
|
||||||
|
{ 0x48, 0x8f, 0xc3, 0x3d, 0xf4, 0x3f, 0xbd, 0xee, 0xa4, 0xea, 0xfb, 0x40 },
|
||||||
|
{ 0x82, 0x74, 0x23, 0xee, 0x40, 0xb6, 0x75, 0xf7, 0x56, 0xeb, 0x5f, 0xe0 },
|
||||||
|
{ 0xab, 0xe1, 0x97, 0xc4, 0x84, 0xcb, 0x74, 0x75, 0x71, 0x44, 0xa9, 0xa0 },
|
||||||
|
{ 0x2b, 0x50, 0x0e, 0x4b, 0xc0, 0xec, 0x5a, 0x6d, 0x2b, 0xdb, 0xdd, 0x00 },
|
||||||
|
{ 0xc4, 0x74, 0xaa, 0x53, 0xd7, 0x02, 0x18, 0x76, 0x16, 0x69, 0x36, 0x00 },
|
||||||
|
{ 0x8e, 0xba, 0x1a, 0x13, 0xdb, 0x33, 0x90, 0xbd, 0x67, 0x18, 0xce, 0xc0 },
|
||||||
|
{ 0x75, 0x38, 0x44, 0x67, 0x3a, 0x27, 0x78, 0x2c, 0xc4, 0x20, 0x12, 0xe0 },
|
||||||
|
{ 0x06, 0xff, 0x83, 0xa1, 0x45, 0xc3, 0x70, 0x35, 0xa5, 0xc1, 0x26, 0x80 },
|
||||||
|
{ 0x3b, 0x37, 0x41, 0x78, 0x58, 0xcc, 0x2d, 0xd3, 0x3e, 0xc3, 0xf6, 0x20 },
|
||||||
|
{ 0x9a, 0x4a, 0x5a, 0x28, 0xee, 0x17, 0xca, 0x9c, 0x32, 0x48, 0x42, 0xc0 },
|
||||||
|
{ 0xbc, 0x29, 0xf4, 0x65, 0x30, 0x9c, 0x97, 0x7e, 0x89, 0x61, 0x0a, 0x40 },
|
||||||
|
{ 0x26, 0x63, 0xae, 0x6d, 0xdf, 0x8b, 0x5c, 0xe2, 0xbb, 0x29, 0x48, 0x80 },
|
||||||
|
{ 0x46, 0xf2, 0x31, 0xef, 0xe4, 0x57, 0x03, 0x4c, 0x18, 0x14, 0x41, 0x80 },
|
||||||
|
{ 0x3f, 0xb2, 0xce, 0x85, 0xab, 0xe9, 0xb0, 0xc7, 0x2e, 0x06, 0xfb, 0xe0 },
|
||||||
|
{ 0xde, 0x87, 0x48, 0x1f, 0x28, 0x2c, 0x15, 0x39, 0x71, 0xa0, 0xa2, 0xe0 },
|
||||||
|
{ 0xfc, 0xd7, 0xcc, 0xf2, 0x3c, 0x69, 0xfa, 0x99, 0xbb, 0xa1, 0x41, 0x20 },
|
||||||
|
{ 0xf0, 0x26, 0x14, 0x47, 0xe9, 0x49, 0x0c, 0xa8, 0xe4, 0x74, 0xce, 0xc0 },
|
||||||
|
{ 0x44, 0x10, 0x11, 0x58, 0x18, 0x19, 0x6f, 0x95, 0xcd, 0xd7, 0x01, 0x20 },
|
||||||
|
{ 0x08, 0x8f, 0xc3, 0x1d, 0xf4, 0xbf, 0xbd, 0xe2, 0xa4, 0xea, 0xfb, 0x40 },
|
||||||
|
{ 0xb8, 0xfe, 0xf1, 0xb6, 0x30, 0x77, 0x29, 0xfb, 0x0a, 0x07, 0x8c, 0x00 },
|
||||||
|
{ 0x5a, 0xfe, 0xa7, 0xac, 0xcc, 0xb7, 0x7b, 0xbc, 0x9d, 0x99, 0xa9, 0x00 },
|
||||||
|
{ 0x49, 0xa7, 0x01, 0x6a, 0xc6, 0x53, 0xf6, 0x5e, 0xcd, 0xc9, 0x07, 0x60 },
|
||||||
|
{ 0x19, 0x44, 0xd0, 0x85, 0xbe, 0x4e, 0x7d, 0xa8, 0xd6, 0xcc, 0x7d, 0x00 },
|
||||||
|
{ 0x25, 0x1f, 0x62, 0xad, 0xc4, 0x03, 0x2f, 0x0e, 0xe7, 0x14, 0x00, 0x20 },
|
||||||
|
{ 0x56, 0x47, 0x1f, 0x87, 0x02, 0xa0, 0x72, 0x1e, 0x00, 0xb1, 0x2b, 0x80 },
|
||||||
|
{ 0x2b, 0x8e, 0x49, 0x23, 0xf2, 0xdd, 0x51, 0xe2, 0xd5, 0x37, 0xfa, 0x00 },
|
||||||
|
{ 0x6b, 0x55, 0x0a, 0x40, 0xa6, 0x6f, 0x47, 0x55, 0xde, 0x95, 0xc2, 0x60 },
|
||||||
|
{ 0xa1, 0x8a, 0xd2, 0x8d, 0x4e, 0x27, 0xfe, 0x92, 0xa4, 0xf6, 0xc8, 0x40 },
|
||||||
|
{ 0x10, 0xc2, 0xe5, 0x86, 0x38, 0x8c, 0xb8, 0x2a, 0x3d, 0x80, 0x75, 0x80 },
|
||||||
|
{ 0xef, 0x34, 0xa4, 0x18, 0x17, 0xee, 0x02, 0x13, 0x3d, 0xb2, 0xeb, 0x00 },
|
||||||
|
{ 0x7e, 0x9c, 0x0c, 0x54, 0x32, 0x5a, 0x9c, 0x15, 0x83, 0x6e, 0x00, 0x00 },
|
||||||
|
{ 0x36, 0x93, 0xe5, 0x72, 0xd1, 0xfd, 0xe4, 0xcd, 0xf0, 0x79, 0xe8, 0x60 },
|
||||||
|
{ 0xbf, 0xb2, 0xce, 0xc5, 0xab, 0xe1, 0xb0, 0xc7, 0x2e, 0x07, 0xfb, 0xe0 },
|
||||||
|
{ 0x7e, 0xe1, 0x82, 0x30, 0xc5, 0x83, 0xcc, 0xcc, 0x57, 0xd4, 0xb0, 0x80 },
|
||||||
|
{ 0xa0, 0x66, 0xcb, 0x2f, 0xed, 0xaf, 0xc9, 0xf5, 0x26, 0x64, 0x12, 0x60 },
|
||||||
|
{ 0xbb, 0x23, 0x72, 0x5a, 0xbc, 0x47, 0xcc, 0x5f, 0x4c, 0xc4, 0xcd, 0x20 },
|
||||||
|
{ 0xde, 0xd9, 0xdb, 0xa3, 0xbe, 0xe4, 0x0c, 0x59, 0xb5, 0x60, 0x9b, 0x40 },
|
||||||
|
{ 0xd9, 0xa7, 0x01, 0x6a, 0xc6, 0x53, 0xe6, 0xde, 0xcd, 0xc9, 0x03, 0x60 },
|
||||||
|
{ 0x9a, 0xd4, 0x6a, 0xed, 0x5f, 0x70, 0x7f, 0x28, 0x0a, 0xb5, 0xfc, 0x40 },
|
||||||
|
{ 0xe5, 0x92, 0x1c, 0x77, 0x82, 0x25, 0x87, 0x31, 0x6d, 0x7d, 0x3c, 0x20 },
|
||||||
|
{ 0x4f, 0x14, 0xda, 0x82, 0x42, 0xa8, 0xb8, 0x6d, 0xca, 0x73, 0x35, 0x20 },
|
||||||
|
{ 0x8b, 0x8b, 0x50, 0x7a, 0xd4, 0x67, 0xd4, 0x44, 0x1d, 0xf7, 0x70, 0xe0 },
|
||||||
|
{ 0x22, 0x83, 0x1c, 0x9c, 0xf1, 0x16, 0x94, 0x67, 0xad, 0x04, 0xb6, 0x80 },
|
||||||
|
{ 0x21, 0x3b, 0x83, 0x8f, 0xe2, 0xae, 0x54, 0xc3, 0x8e, 0xe7, 0x18, 0x00 },
|
||||||
|
{ 0x5d, 0x92, 0x6b, 0x6d, 0xd7, 0x1f, 0x08, 0x51, 0x81, 0xa4, 0xe1, 0x20 },
|
||||||
|
{ 0x66, 0xab, 0x79, 0xd4, 0xb2, 0x9e, 0xe6, 0xe6, 0x95, 0x09, 0xe5, 0x60 },
|
||||||
|
{ 0x95, 0x81, 0x48, 0x68, 0x2d, 0x74, 0x8a, 0x38, 0xdd, 0x68, 0xba, 0xa0 },
|
||||||
|
{ 0xb8, 0xce, 0x02, 0x0c, 0xf0, 0x69, 0xc3, 0x2a, 0x72, 0x3a, 0xb1, 0x40 },
|
||||||
|
{ 0xf4, 0x33, 0x1d, 0x6d, 0x46, 0x16, 0x07, 0xe9, 0x57, 0x52, 0x74, 0x60 },
|
||||||
|
{ 0x6d, 0xa2, 0x3b, 0xa4, 0x24, 0xb9, 0x59, 0x61, 0x33, 0xcf, 0x9c, 0x80 },
|
||||||
|
{ 0xa6, 0x36, 0xbc, 0xbc, 0x7b, 0x30, 0xc5, 0xfb, 0xea, 0xe6, 0x7f, 0xe0 },
|
||||||
|
{ 0x5c, 0xb0, 0xd8, 0x6a, 0x07, 0xdf, 0x65, 0x4a, 0x90, 0x89, 0xa2, 0x00 },
|
||||||
|
{ 0xf1, 0x1f, 0x10, 0x68, 0x48, 0x78, 0x0f, 0xc9, 0xec, 0xdd, 0x80, 0xa0 },
|
||||||
|
{ 0x1f, 0xbb, 0x53, 0x64, 0xfb, 0x8d, 0x2c, 0x9d, 0x73, 0x0d, 0x5b, 0xa0 },
|
||||||
|
{ 0xfc, 0xb8, 0x6b, 0xc7, 0x0a, 0x50, 0xc9, 0xd0, 0x2a, 0x5d, 0x03, 0x40 },
|
||||||
|
{ 0xa5, 0x34, 0x43, 0x30, 0x29, 0xea, 0xc1, 0x5f, 0x32, 0x2e, 0x34, 0xc0 },
|
||||||
|
{ 0xc9, 0x89, 0xd9, 0xc7, 0xc3, 0xd3, 0xb8, 0xc5, 0x5d, 0x75, 0x13, 0x00 },
|
||||||
|
{ 0x7b, 0xb3, 0x8b, 0x2f, 0x01, 0x86, 0xd4, 0x66, 0x43, 0xae, 0x96, 0x20 },
|
||||||
|
{ 0x26, 0x44, 0xeb, 0xad, 0xeb, 0x44, 0xb9, 0x46, 0x7d, 0x1f, 0x42, 0xc0 },
|
||||||
|
{ 0x60, 0x8c, 0xc8, 0x57, 0x59, 0x4b, 0xfb, 0xb5, 0x5d, 0x69, 0x60, 0x00 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Each row describes one LDPC parity check.
|
||||||
|
// Each number is an index into the codeword (1-origin).
|
||||||
|
// The codeword bits mentioned in each row must XOR to zero.
|
||||||
|
const uint8_t kFTX_LDPC_Nm[FTX_LDPC_M][7] = {
|
||||||
|
{ 4, 31, 59, 91, 92, 96, 153 },
|
||||||
|
{ 5, 32, 60, 93, 115, 146, 0 },
|
||||||
|
{ 6, 24, 61, 94, 122, 151, 0 },
|
||||||
|
{ 7, 33, 62, 95, 96, 143, 0 },
|
||||||
|
{ 8, 25, 63, 83, 93, 96, 148 },
|
||||||
|
{ 6, 32, 64, 97, 126, 138, 0 },
|
||||||
|
{ 5, 34, 65, 78, 98, 107, 154 },
|
||||||
|
{ 9, 35, 66, 99, 139, 146, 0 },
|
||||||
|
{ 10, 36, 67, 100, 107, 126, 0 },
|
||||||
|
{ 11, 37, 67, 87, 101, 139, 158 },
|
||||||
|
{ 12, 38, 68, 102, 105, 155, 0 },
|
||||||
|
{ 13, 39, 69, 103, 149, 162, 0 },
|
||||||
|
{ 8, 40, 70, 82, 104, 114, 145 },
|
||||||
|
{ 14, 41, 71, 88, 102, 123, 156 },
|
||||||
|
{ 15, 42, 59, 106, 123, 159, 0 },
|
||||||
|
{ 1, 33, 72, 106, 107, 157, 0 },
|
||||||
|
{ 16, 43, 73, 108, 141, 160, 0 },
|
||||||
|
{ 17, 37, 74, 81, 109, 131, 154 },
|
||||||
|
{ 11, 44, 75, 110, 121, 166, 0 },
|
||||||
|
{ 45, 55, 64, 111, 130, 161, 173 },
|
||||||
|
{ 8, 46, 71, 112, 119, 166, 0 },
|
||||||
|
{ 18, 36, 76, 89, 113, 114, 143 },
|
||||||
|
{ 19, 38, 77, 104, 116, 163, 0 },
|
||||||
|
{ 20, 47, 70, 92, 138, 165, 0 },
|
||||||
|
{ 2, 48, 74, 113, 128, 160, 0 },
|
||||||
|
{ 21, 45, 78, 83, 117, 121, 151 },
|
||||||
|
{ 22, 47, 58, 118, 127, 164, 0 },
|
||||||
|
{ 16, 39, 62, 112, 134, 158, 0 },
|
||||||
|
{ 23, 43, 79, 120, 131, 145, 0 },
|
||||||
|
{ 19, 35, 59, 73, 110, 125, 161 },
|
||||||
|
{ 20, 36, 63, 94, 136, 161, 0 },
|
||||||
|
{ 14, 31, 79, 98, 132, 164, 0 },
|
||||||
|
{ 3, 44, 80, 124, 127, 169, 0 },
|
||||||
|
{ 19, 46, 81, 117, 135, 167, 0 },
|
||||||
|
{ 7, 49, 58, 90, 100, 105, 168 },
|
||||||
|
{ 12, 50, 61, 118, 119, 144, 0 },
|
||||||
|
{ 13, 51, 64, 114, 118, 157, 0 },
|
||||||
|
{ 24, 52, 76, 129, 148, 149, 0 },
|
||||||
|
{ 25, 53, 69, 90, 101, 130, 156 },
|
||||||
|
{ 20, 46, 65, 80, 120, 140, 170 },
|
||||||
|
{ 21, 54, 77, 100, 140, 171, 0 },
|
||||||
|
{ 35, 82, 133, 142, 171, 174, 0 },
|
||||||
|
{ 14, 30, 83, 113, 125, 170, 0 },
|
||||||
|
{ 4, 29, 68, 120, 134, 173, 0 },
|
||||||
|
{ 1, 4, 52, 57, 86, 136, 152 },
|
||||||
|
{ 26, 51, 56, 91, 122, 137, 168 },
|
||||||
|
{ 52, 84, 110, 115, 145, 168, 0 },
|
||||||
|
{ 7, 50, 81, 99, 132, 173, 0 },
|
||||||
|
{ 23, 55, 67, 95, 172, 174, 0 },
|
||||||
|
{ 26, 41, 77, 109, 141, 148, 0 },
|
||||||
|
{ 2, 27, 41, 61, 62, 115, 133 },
|
||||||
|
{ 27, 40, 56, 124, 125, 126, 0 },
|
||||||
|
{ 18, 49, 55, 124, 141, 167, 0 },
|
||||||
|
{ 6, 33, 85, 108, 116, 156, 0 },
|
||||||
|
{ 28, 48, 70, 85, 105, 129, 158 },
|
||||||
|
{ 9, 54, 63, 131, 147, 155, 0 },
|
||||||
|
{ 22, 53, 68, 109, 121, 174, 0 },
|
||||||
|
{ 3, 13, 48, 78, 95, 123, 0 },
|
||||||
|
{ 31, 69, 133, 150, 155, 169, 0 },
|
||||||
|
{ 12, 43, 66, 89, 97, 135, 159 },
|
||||||
|
{ 5, 39, 75, 102, 136, 167, 0 },
|
||||||
|
{ 2, 54, 86, 101, 135, 164, 0 },
|
||||||
|
{ 15, 56, 87, 108, 119, 171, 0 },
|
||||||
|
{ 10, 44, 82, 91, 111, 144, 149 },
|
||||||
|
{ 23, 34, 71, 94, 127, 153, 0 },
|
||||||
|
{ 11, 49, 88, 92, 142, 157, 0 },
|
||||||
|
{ 29, 34, 87, 97, 147, 162, 0 },
|
||||||
|
{ 30, 50, 60, 86, 137, 142, 162 },
|
||||||
|
{ 10, 53, 66, 84, 112, 128, 165 },
|
||||||
|
{ 22, 57, 85, 93, 140, 159, 0 },
|
||||||
|
{ 28, 32, 72, 103, 132, 166, 0 },
|
||||||
|
{ 28, 29, 84, 88, 117, 143, 150 },
|
||||||
|
{ 1, 26, 45, 80, 128, 147, 0 },
|
||||||
|
{ 17, 27, 89, 103, 116, 153, 0 },
|
||||||
|
{ 51, 57, 98, 163, 165, 172, 0 },
|
||||||
|
{ 21, 37, 73, 138, 152, 169, 0 },
|
||||||
|
{ 16, 47, 76, 130, 137, 154, 0 },
|
||||||
|
{ 3, 24, 30, 72, 104, 139, 0 },
|
||||||
|
{ 9, 40, 90, 106, 134, 151, 0 },
|
||||||
|
{ 15, 58, 60, 74, 111, 150, 163 },
|
||||||
|
{ 18, 42, 79, 144, 146, 152, 0 },
|
||||||
|
{ 25, 38, 65, 99, 122, 160, 0 },
|
||||||
|
{ 17, 42, 75, 129, 170, 172, 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Each row corresponds to a codeword bit.
|
||||||
|
// The numbers indicate which three LDPC parity checks (rows in Nm) refer to the codeword bit.
|
||||||
|
// 1-origin.
|
||||||
|
const uint8_t kFTX_LDPC_Mn[FTX_LDPC_N][3] = {
|
||||||
|
{ 16, 45, 73 },
|
||||||
|
{ 25, 51, 62 },
|
||||||
|
{ 33, 58, 78 },
|
||||||
|
{ 1, 44, 45 },
|
||||||
|
{ 2, 7, 61 },
|
||||||
|
{ 3, 6, 54 },
|
||||||
|
{ 4, 35, 48 },
|
||||||
|
{ 5, 13, 21 },
|
||||||
|
{ 8, 56, 79 },
|
||||||
|
{ 9, 64, 69 },
|
||||||
|
{ 10, 19, 66 },
|
||||||
|
{ 11, 36, 60 },
|
||||||
|
{ 12, 37, 58 },
|
||||||
|
{ 14, 32, 43 },
|
||||||
|
{ 15, 63, 80 },
|
||||||
|
{ 17, 28, 77 },
|
||||||
|
{ 18, 74, 83 },
|
||||||
|
{ 22, 53, 81 },
|
||||||
|
{ 23, 30, 34 },
|
||||||
|
{ 24, 31, 40 },
|
||||||
|
{ 26, 41, 76 },
|
||||||
|
{ 27, 57, 70 },
|
||||||
|
{ 29, 49, 65 },
|
||||||
|
{ 3, 38, 78 },
|
||||||
|
{ 5, 39, 82 },
|
||||||
|
{ 46, 50, 73 },
|
||||||
|
{ 51, 52, 74 },
|
||||||
|
{ 55, 71, 72 },
|
||||||
|
{ 44, 67, 72 },
|
||||||
|
{ 43, 68, 78 },
|
||||||
|
{ 1, 32, 59 },
|
||||||
|
{ 2, 6, 71 },
|
||||||
|
{ 4, 16, 54 },
|
||||||
|
{ 7, 65, 67 },
|
||||||
|
{ 8, 30, 42 },
|
||||||
|
{ 9, 22, 31 },
|
||||||
|
{ 10, 18, 76 },
|
||||||
|
{ 11, 23, 82 },
|
||||||
|
{ 12, 28, 61 },
|
||||||
|
{ 13, 52, 79 },
|
||||||
|
{ 14, 50, 51 },
|
||||||
|
{ 15, 81, 83 },
|
||||||
|
{ 17, 29, 60 },
|
||||||
|
{ 19, 33, 64 },
|
||||||
|
{ 20, 26, 73 },
|
||||||
|
{ 21, 34, 40 },
|
||||||
|
{ 24, 27, 77 },
|
||||||
|
{ 25, 55, 58 },
|
||||||
|
{ 35, 53, 66 },
|
||||||
|
{ 36, 48, 68 },
|
||||||
|
{ 37, 46, 75 },
|
||||||
|
{ 38, 45, 47 },
|
||||||
|
{ 39, 57, 69 },
|
||||||
|
{ 41, 56, 62 },
|
||||||
|
{ 20, 49, 53 },
|
||||||
|
{ 46, 52, 63 },
|
||||||
|
{ 45, 70, 75 },
|
||||||
|
{ 27, 35, 80 },
|
||||||
|
{ 1, 15, 30 },
|
||||||
|
{ 2, 68, 80 },
|
||||||
|
{ 3, 36, 51 },
|
||||||
|
{ 4, 28, 51 },
|
||||||
|
{ 5, 31, 56 },
|
||||||
|
{ 6, 20, 37 },
|
||||||
|
{ 7, 40, 82 },
|
||||||
|
{ 8, 60, 69 },
|
||||||
|
{ 9, 10, 49 },
|
||||||
|
{ 11, 44, 57 },
|
||||||
|
{ 12, 39, 59 },
|
||||||
|
{ 13, 24, 55 },
|
||||||
|
{ 14, 21, 65 },
|
||||||
|
{ 16, 71, 78 },
|
||||||
|
{ 17, 30, 76 },
|
||||||
|
{ 18, 25, 80 },
|
||||||
|
{ 19, 61, 83 },
|
||||||
|
{ 22, 38, 77 },
|
||||||
|
{ 23, 41, 50 },
|
||||||
|
{ 7, 26, 58 },
|
||||||
|
{ 29, 32, 81 },
|
||||||
|
{ 33, 40, 73 },
|
||||||
|
{ 18, 34, 48 },
|
||||||
|
{ 13, 42, 64 },
|
||||||
|
{ 5, 26, 43 },
|
||||||
|
{ 47, 69, 72 },
|
||||||
|
{ 54, 55, 70 },
|
||||||
|
{ 45, 62, 68 },
|
||||||
|
{ 10, 63, 67 },
|
||||||
|
{ 14, 66, 72 },
|
||||||
|
{ 22, 60, 74 },
|
||||||
|
{ 35, 39, 79 },
|
||||||
|
{ 1, 46, 64 },
|
||||||
|
{ 1, 24, 66 },
|
||||||
|
{ 2, 5, 70 },
|
||||||
|
{ 3, 31, 65 },
|
||||||
|
{ 4, 49, 58 },
|
||||||
|
{ 1, 4, 5 },
|
||||||
|
{ 6, 60, 67 },
|
||||||
|
{ 7, 32, 75 },
|
||||||
|
{ 8, 48, 82 },
|
||||||
|
{ 9, 35, 41 },
|
||||||
|
{ 10, 39, 62 },
|
||||||
|
{ 11, 14, 61 },
|
||||||
|
{ 12, 71, 74 },
|
||||||
|
{ 13, 23, 78 },
|
||||||
|
{ 11, 35, 55 },
|
||||||
|
{ 15, 16, 79 },
|
||||||
|
{ 7, 9, 16 },
|
||||||
|
{ 17, 54, 63 },
|
||||||
|
{ 18, 50, 57 },
|
||||||
|
{ 19, 30, 47 },
|
||||||
|
{ 20, 64, 80 },
|
||||||
|
{ 21, 28, 69 },
|
||||||
|
{ 22, 25, 43 },
|
||||||
|
{ 13, 22, 37 },
|
||||||
|
{ 2, 47, 51 },
|
||||||
|
{ 23, 54, 74 },
|
||||||
|
{ 26, 34, 72 },
|
||||||
|
{ 27, 36, 37 },
|
||||||
|
{ 21, 36, 63 },
|
||||||
|
{ 29, 40, 44 },
|
||||||
|
{ 19, 26, 57 },
|
||||||
|
{ 3, 46, 82 },
|
||||||
|
{ 14, 15, 58 },
|
||||||
|
{ 33, 52, 53 },
|
||||||
|
{ 30, 43, 52 },
|
||||||
|
{ 6, 9, 52 },
|
||||||
|
{ 27, 33, 65 },
|
||||||
|
{ 25, 69, 73 },
|
||||||
|
{ 38, 55, 83 },
|
||||||
|
{ 20, 39, 77 },
|
||||||
|
{ 18, 29, 56 },
|
||||||
|
{ 32, 48, 71 },
|
||||||
|
{ 42, 51, 59 },
|
||||||
|
{ 28, 44, 79 },
|
||||||
|
{ 34, 60, 62 },
|
||||||
|
{ 31, 45, 61 },
|
||||||
|
{ 46, 68, 77 },
|
||||||
|
{ 6, 24, 76 },
|
||||||
|
{ 8, 10, 78 },
|
||||||
|
{ 40, 41, 70 },
|
||||||
|
{ 17, 50, 53 },
|
||||||
|
{ 42, 66, 68 },
|
||||||
|
{ 4, 22, 72 },
|
||||||
|
{ 36, 64, 81 },
|
||||||
|
{ 13, 29, 47 },
|
||||||
|
{ 2, 8, 81 },
|
||||||
|
{ 56, 67, 73 },
|
||||||
|
{ 5, 38, 50 },
|
||||||
|
{ 12, 38, 64 },
|
||||||
|
{ 59, 72, 80 },
|
||||||
|
{ 3, 26, 79 },
|
||||||
|
{ 45, 76, 81 },
|
||||||
|
{ 1, 65, 74 },
|
||||||
|
{ 7, 18, 77 },
|
||||||
|
{ 11, 56, 59 },
|
||||||
|
{ 14, 39, 54 },
|
||||||
|
{ 16, 37, 66 },
|
||||||
|
{ 10, 28, 55 },
|
||||||
|
{ 15, 60, 70 },
|
||||||
|
{ 17, 25, 82 },
|
||||||
|
{ 20, 30, 31 },
|
||||||
|
{ 12, 67, 68 },
|
||||||
|
{ 23, 75, 80 },
|
||||||
|
{ 27, 32, 62 },
|
||||||
|
{ 24, 69, 75 },
|
||||||
|
{ 19, 21, 71 },
|
||||||
|
{ 34, 53, 61 },
|
||||||
|
{ 35, 46, 47 },
|
||||||
|
{ 33, 59, 76 },
|
||||||
|
{ 40, 43, 83 },
|
||||||
|
{ 41, 42, 63 },
|
||||||
|
{ 49, 75, 83 },
|
||||||
|
{ 20, 44, 48 },
|
||||||
|
{ 42, 49, 57 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const uint8_t kFTX_LDPC_Num_rows[FTX_LDPC_M] = {
|
||||||
|
7, 6, 6, 6, 7, 6, 7, 6, 6, 7, 6, 6, 7, 7, 6, 6,
|
||||||
|
6, 7, 6, 7, 6, 7, 6, 6, 6, 7, 6, 6, 6, 7, 6, 6,
|
||||||
|
6, 6, 7, 6, 6, 6, 7, 7, 6, 6, 6, 6, 7, 7, 6, 6,
|
||||||
|
6, 6, 7, 6, 6, 6, 7, 6, 6, 6, 6, 7, 6, 6, 6, 7,
|
||||||
|
6, 6, 6, 7, 7, 6, 6, 7, 6, 6, 6, 6, 6, 6, 6, 7,
|
||||||
|
6, 6, 6
|
||||||
|
};
|
||||||
Vendored
+121
@@ -0,0 +1,121 @@
|
|||||||
|
#ifndef _INCLUDE_CONSTANTS_H_
|
||||||
|
#define _INCLUDE_CONSTANTS_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define FT8_SYMBOL_PERIOD (0.160f) ///< FT8 symbol duration, defines tone deviation in Hz and symbol rate
|
||||||
|
#define FT8_SLOT_TIME (15.0f) ///< FT8 slot period
|
||||||
|
|
||||||
|
#define FT4_SYMBOL_PERIOD (0.048f) ///< FT4 symbol duration, defines tone deviation in Hz and symbol rate
|
||||||
|
#define FT4_SLOT_TIME (7.5f) ///< FT4 slot period
|
||||||
|
|
||||||
|
#define FT2_SYMBOL_PERIOD (0.024f) ///< FT2 symbol duration (288 samples @ 12 kHz)
|
||||||
|
#define FT2_SLOT_TIME (3.75f) ///< FT2 slot period
|
||||||
|
|
||||||
|
// Define FT8 symbol counts
|
||||||
|
// FT8 message structure:
|
||||||
|
// S D1 S D2 S
|
||||||
|
// S - sync block (7 symbols of Costas pattern)
|
||||||
|
// D1 - first data block (29 symbols each encoding 3 bits)
|
||||||
|
#define FT8_ND (58) ///< Data symbols
|
||||||
|
#define FT8_NN (79) ///< Total channel symbols (FT8_NS + FT8_ND)
|
||||||
|
#define FT8_LENGTH_SYNC (7) ///< Length of each sync group
|
||||||
|
#define FT8_NUM_SYNC (3) ///< Number of sync groups
|
||||||
|
#define FT8_SYNC_OFFSET (36) ///< Offset between sync groups
|
||||||
|
|
||||||
|
// Define FT4 symbol counts
|
||||||
|
// FT4 message structure:
|
||||||
|
// R Sa D1 Sb D2 Sc D3 Sd R
|
||||||
|
// R - ramping symbol (no payload information conveyed)
|
||||||
|
// Sx - one of four _different_ sync blocks (4 symbols of Costas pattern)
|
||||||
|
// Dy - data block (29 symbols each encoding 2 bits)
|
||||||
|
#define FT4_ND (87) ///< Data symbols
|
||||||
|
#define FT4_NR (2) ///< Ramp symbols (beginning + end)
|
||||||
|
#define FT4_NN (105) ///< Total channel symbols (FT4_NS + FT4_ND + FT4_NR)
|
||||||
|
#define FT4_LENGTH_SYNC (4) ///< Length of each sync group
|
||||||
|
#define FT4_NUM_SYNC (4) ///< Number of sync groups
|
||||||
|
#define FT4_SYNC_OFFSET (33) ///< Offset between sync groups
|
||||||
|
|
||||||
|
// FT2 reuses the FT4 channel structure with a shorter slot and symbol period.
|
||||||
|
#define FT2_ND FT4_ND
|
||||||
|
#define FT2_NR FT4_NR
|
||||||
|
#define FT2_NN FT4_NN
|
||||||
|
#define FT2_LENGTH_SYNC FT4_LENGTH_SYNC
|
||||||
|
#define FT2_NUM_SYNC FT4_NUM_SYNC
|
||||||
|
#define FT2_SYNC_OFFSET FT4_SYNC_OFFSET
|
||||||
|
|
||||||
|
// Define LDPC parameters
|
||||||
|
#define FTX_LDPC_N (174) ///< Number of bits in the encoded message (payload with LDPC checksum bits)
|
||||||
|
#define FTX_LDPC_K (91) ///< Number of payload bits (including CRC)
|
||||||
|
#define FTX_LDPC_M (83) ///< Number of LDPC checksum bits (FTX_LDPC_N - FTX_LDPC_K)
|
||||||
|
#define FTX_LDPC_N_BYTES ((FTX_LDPC_N + 7) / 8) ///< Number of whole bytes needed to store 174 bits (full message)
|
||||||
|
#define FTX_LDPC_K_BYTES ((FTX_LDPC_K + 7) / 8) ///< Number of whole bytes needed to store 91 bits (payload + CRC only)
|
||||||
|
|
||||||
|
// Define CRC parameters
|
||||||
|
#define FT8_CRC_POLYNOMIAL ((uint16_t)0x2757u) ///< CRC-14 polynomial without the leading (MSB) 1
|
||||||
|
#define FT8_CRC_WIDTH (14)
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
FTX_PROTOCOL_FT4,
|
||||||
|
FTX_PROTOCOL_FT8,
|
||||||
|
FTX_PROTOCOL_FT2
|
||||||
|
} ftx_protocol_t;
|
||||||
|
|
||||||
|
static inline float ftx_protocol_symbol_period(ftx_protocol_t protocol)
|
||||||
|
{
|
||||||
|
return (protocol == FTX_PROTOCOL_FT8)
|
||||||
|
? FT8_SYMBOL_PERIOD
|
||||||
|
: ((protocol == FTX_PROTOCOL_FT2) ? FT2_SYMBOL_PERIOD : FT4_SYMBOL_PERIOD);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline float ftx_protocol_slot_time(ftx_protocol_t protocol)
|
||||||
|
{
|
||||||
|
return (protocol == FTX_PROTOCOL_FT8)
|
||||||
|
? FT8_SLOT_TIME
|
||||||
|
: ((protocol == FTX_PROTOCOL_FT2) ? FT2_SLOT_TIME : FT4_SLOT_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int ftx_protocol_uses_ft4_layout(ftx_protocol_t protocol)
|
||||||
|
{
|
||||||
|
return (protocol == FTX_PROTOCOL_FT4) || (protocol == FTX_PROTOCOL_FT2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Costas 7x7 tone pattern for synchronization
|
||||||
|
extern const uint8_t kFT8_Costas_pattern[7];
|
||||||
|
extern const uint8_t kFT4_Costas_pattern[4][4];
|
||||||
|
|
||||||
|
/// Gray code map to encode 8 symbols (tones)
|
||||||
|
extern const uint8_t kFT8_Gray_map[8];
|
||||||
|
extern const uint8_t kFT4_Gray_map[4];
|
||||||
|
|
||||||
|
extern const uint8_t kFT4_XOR_sequence[10];
|
||||||
|
|
||||||
|
/// Parity generator matrix for (174,91) LDPC code, stored in bitpacked format (MSB first)
|
||||||
|
extern const uint8_t kFTX_LDPC_generator[FTX_LDPC_M][FTX_LDPC_K_BYTES];
|
||||||
|
|
||||||
|
/// LDPC(174,91) parity check matrix, containing 83 rows,
|
||||||
|
/// each row describes one parity check,
|
||||||
|
/// each number is an index into the codeword (1-origin).
|
||||||
|
/// The codeword bits mentioned in each row must xor to zero.
|
||||||
|
/// From WSJT-X's ldpc_174_91_c_reordered_parity.f90.
|
||||||
|
extern const uint8_t kFTX_LDPC_Nm[FTX_LDPC_M][7];
|
||||||
|
|
||||||
|
/// Mn from WSJT-X's bpdecode174.f90. Each row corresponds to a codeword bit.
|
||||||
|
/// The numbers indicate which three parity checks (rows in Nm) refer to the codeword bit.
|
||||||
|
/// The numbers use 1 as the origin (first entry).
|
||||||
|
extern const uint8_t kFTX_LDPC_Mn[FTX_LDPC_N][3];
|
||||||
|
|
||||||
|
/// Number of rows (columns in C/C++) in the array Nm.
|
||||||
|
extern const uint8_t kFTX_LDPC_Num_rows[FTX_LDPC_M];
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_CONSTANTS_H_
|
||||||
Vendored
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#include "crc.h"
|
||||||
|
#include "constants.h"
|
||||||
|
|
||||||
|
#define TOPBIT (1u << (FT8_CRC_WIDTH - 1))
|
||||||
|
|
||||||
|
// Compute 14-bit CRC for a sequence of given number of bits
|
||||||
|
// Adapted from https://barrgroup.com/Embedded-Systems/How-To/CRC-Calculation-C-Code
|
||||||
|
// [IN] message - byte sequence (MSB first)
|
||||||
|
// [IN] num_bits - number of bits in the sequence
|
||||||
|
uint16_t ftx_compute_crc(const uint8_t message[], int num_bits)
|
||||||
|
{
|
||||||
|
uint16_t remainder = 0;
|
||||||
|
int idx_byte = 0;
|
||||||
|
|
||||||
|
// Perform modulo-2 division, a bit at a time.
|
||||||
|
for (int idx_bit = 0; idx_bit < num_bits; ++idx_bit)
|
||||||
|
{
|
||||||
|
if (idx_bit % 8 == 0)
|
||||||
|
{
|
||||||
|
// Bring the next byte into the remainder.
|
||||||
|
remainder ^= (message[idx_byte] << (FT8_CRC_WIDTH - 8));
|
||||||
|
++idx_byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to divide the current data bit.
|
||||||
|
if (remainder & TOPBIT)
|
||||||
|
{
|
||||||
|
remainder = (remainder << 1) ^ FT8_CRC_POLYNOMIAL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
remainder = (remainder << 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return remainder & ((TOPBIT << 1) - 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t ftx_extract_crc(const uint8_t a91[])
|
||||||
|
{
|
||||||
|
uint16_t chksum = ((a91[9] & 0x07) << 11) | (a91[10] << 3) | (a91[11] >> 5);
|
||||||
|
return chksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ftx_add_crc(const uint8_t payload[], uint8_t a91[])
|
||||||
|
{
|
||||||
|
// Copy 77 bits of payload data
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
a91[i] = payload[i];
|
||||||
|
|
||||||
|
// Clear 3 bits after the payload to make 82 bits
|
||||||
|
a91[9] &= 0xF8u;
|
||||||
|
a91[10] = 0;
|
||||||
|
|
||||||
|
// Calculate CRC of 82 bits (77 + 5 zeros)
|
||||||
|
// 'The CRC is calculated on the source-encoded message, zero-extended from 77 to 82 bits'
|
||||||
|
uint16_t checksum = ftx_compute_crc(a91, 96 - 14);
|
||||||
|
|
||||||
|
// Store the CRC at the end of 77 bit message
|
||||||
|
a91[9] |= (uint8_t)(checksum >> 11);
|
||||||
|
a91[10] = (uint8_t)(checksum >> 3);
|
||||||
|
a91[11] = (uint8_t)(checksum << 5);
|
||||||
|
}
|
||||||
Vendored
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#ifndef _INCLUDE_CRC_H_
|
||||||
|
#define _INCLUDE_CRC_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Compute 14-bit CRC for a sequence of given number of bits using FT8/FT4 CRC polynomial
|
||||||
|
// [IN] message - byte sequence (MSB first)
|
||||||
|
// [IN] num_bits - number of bits in the sequence
|
||||||
|
uint16_t ftx_compute_crc(const uint8_t message[], int num_bits);
|
||||||
|
|
||||||
|
/// Extract the FT8/FT4 CRC of a packed message (during decoding)
|
||||||
|
/// @param[in] a91 77 bits of payload data + CRC
|
||||||
|
/// @return Extracted CRC
|
||||||
|
uint16_t ftx_extract_crc(const uint8_t a91[]);
|
||||||
|
|
||||||
|
/// Add FT8/FT4 CRC to a packed message (during encoding)
|
||||||
|
/// @param[in] payload 77 bits of payload data
|
||||||
|
/// @param[out] a91 91 bits of payload data + CRC
|
||||||
|
void ftx_add_crc(const uint8_t payload[], uint8_t a91[]);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_CRC_H_
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#ifndef _DEBUG_H_INCLUDED_
|
||||||
|
#define _DEBUG_H_INCLUDED_
|
||||||
|
|
||||||
|
#define LOG_DEBUG 0
|
||||||
|
#define LOG_INFO 1
|
||||||
|
#define LOG_WARN 2
|
||||||
|
#define LOG_ERROR 3
|
||||||
|
#define LOG_FATAL 4
|
||||||
|
|
||||||
|
#ifdef LOG_LEVEL
|
||||||
|
#ifndef LOG_PRINTF
|
||||||
|
#include <stdio.h>
|
||||||
|
#define LOG_PRINTF(...) fprintf(stderr, __VA_ARGS__)
|
||||||
|
#endif
|
||||||
|
#define LOG(level, ...) \
|
||||||
|
if (level >= LOG_LEVEL) \
|
||||||
|
LOG_PRINTF(__VA_ARGS__)
|
||||||
|
#else // ifdef LOG_LEVEL
|
||||||
|
#define LOG(level, ...)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _DEBUG_H_INCLUDED_
|
||||||
Vendored
+773
@@ -0,0 +1,773 @@
|
|||||||
|
#include "decode.h"
|
||||||
|
#include "constants.h"
|
||||||
|
#include "crc.h"
|
||||||
|
#include "ldpc.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <complex.h>
|
||||||
|
|
||||||
|
// #define LOG_LEVEL LOG_DEBUG
|
||||||
|
// #include "debug.h"
|
||||||
|
|
||||||
|
// Lookup table for y = 10*log10(1 + 10^(x/10)), where
|
||||||
|
// y - increase in signal level dB when adding a weaker independent signal
|
||||||
|
// x - specific relative strength of the weaker signal in dB
|
||||||
|
// Table index corresponds to x in dB (index 0: 0 dB, index 1: -1 dB etc)
|
||||||
|
static const float db_power_sum[40] = {
|
||||||
|
3.01029995663981f, 2.53901891043867f, 2.1244260279434f, 1.76434862436485f, 1.45540463109294f,
|
||||||
|
1.19331048066095f, 0.973227937086954f, 0.790097496525665f, 0.638920341433796f, 0.514969420252302f,
|
||||||
|
0.413926851582251f, 0.331956199884278f, 0.265723755961025f, 0.212384019142551f, 0.16954289279533f,
|
||||||
|
0.135209221080382f, 0.10774225511957f, 0.085799992300358f, 0.06829128312453f, 0.054333142200458f,
|
||||||
|
0.043213737826426f, 0.034360947517284f, 0.027316043349389f, 0.021711921641451f, 0.017255250287928f,
|
||||||
|
0.013711928326833f, 0.010895305999614f, 0.008656680827934f, 0.006877654943187f, 0.005464004928574f,
|
||||||
|
0.004340774793186f, 0.003448354310253f, 0.002739348814965f, 0.002176083232619f, 0.001728613409904f,
|
||||||
|
0.001373142636584f, 0.001090761428665f, 0.000866444976964f, 0.000688255828734f, 0.000546709946839f
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Compute log likelihood log(p(1) / p(0)) of 174 message bits for later use in soft-decision LDPC decoding
|
||||||
|
/// @param[in] wf Waterfall data collected during message slot
|
||||||
|
/// @param[in] cand Candidate to extract the message from
|
||||||
|
/// @param[in] code_map Symbol encoding map
|
||||||
|
/// @param[out] log174 Output of decoded log likelihoods for each of the 174 message bits
|
||||||
|
static void ft4_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174);
|
||||||
|
static void ft2_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174);
|
||||||
|
static void ft8_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174);
|
||||||
|
|
||||||
|
/// Packs a string of bits each represented as a zero/non-zero byte in bit_array[],
|
||||||
|
/// as a string of packed bits starting from the MSB of the first byte of packed[]
|
||||||
|
/// @param[in] plain Array of bits (0 and nonzero values) with num_bits entires
|
||||||
|
/// @param[in] num_bits Number of bits (entries) passed in bit_array
|
||||||
|
/// @param[out] packed Byte-packed bits representing the data in bit_array
|
||||||
|
static void pack_bits(const uint8_t bit_array[], int num_bits, uint8_t packed[]);
|
||||||
|
|
||||||
|
static float max2(float a, float b);
|
||||||
|
static float max4(float a, float b, float c, float d);
|
||||||
|
static void heapify_down(ftx_candidate_t heap[], int heap_size);
|
||||||
|
static void heapify_up(ftx_candidate_t heap[], int heap_size);
|
||||||
|
|
||||||
|
static void ftx_normalize_logl(float* log174);
|
||||||
|
static void ft4_extract_symbol(const WF_ELEM_T* wf, float* logl);
|
||||||
|
static void ft2_extract_logl_sequence(const float complex symbols[4][FT2_NN - FT2_NR], int start_sym, int n_syms, float* metrics);
|
||||||
|
static void ft8_extract_symbol(const WF_ELEM_T* wf, float* logl);
|
||||||
|
static void ft8_decode_multi_symbols(const WF_ELEM_T* wf, int num_bins, int n_syms, int bit_idx, float* log174);
|
||||||
|
|
||||||
|
static inline float complex wf_elem_to_complex(const WF_ELEM_T elem)
|
||||||
|
{
|
||||||
|
float mag = WF_ELEM_MAG(elem);
|
||||||
|
float amplitude = powf(10.0f, mag / 20.0f);
|
||||||
|
return amplitude * cexpf(I * elem.phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const WF_ELEM_T* get_cand_mag(const ftx_waterfall_t* wf, const ftx_candidate_t* candidate)
|
||||||
|
{
|
||||||
|
int offset = candidate->time_offset;
|
||||||
|
offset = (offset * wf->time_osr) + candidate->time_sub;
|
||||||
|
offset = (offset * wf->freq_osr) + candidate->freq_sub;
|
||||||
|
offset = (offset * wf->num_bins) + candidate->freq_offset;
|
||||||
|
return wf->mag + offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ft8_sync_score(const ftx_waterfall_t* wf, const ftx_candidate_t* candidate)
|
||||||
|
{
|
||||||
|
int score = 0;
|
||||||
|
int num_average = 0;
|
||||||
|
|
||||||
|
// Get the pointer to symbol 0 of the candidate
|
||||||
|
const WF_ELEM_T* mag_cand = get_cand_mag(wf, candidate);
|
||||||
|
|
||||||
|
// Compute average score over sync symbols (m+k = 0-7, 36-43, 72-79)
|
||||||
|
for (int m = 0; m < FT8_NUM_SYNC; ++m)
|
||||||
|
{
|
||||||
|
for (int k = 0; k < FT8_LENGTH_SYNC; ++k)
|
||||||
|
{
|
||||||
|
int block = (FT8_SYNC_OFFSET * m) + k; // relative to the message
|
||||||
|
int block_abs = candidate->time_offset + block; // relative to the captured signal
|
||||||
|
// Check for time boundaries
|
||||||
|
if (block_abs < 0)
|
||||||
|
continue;
|
||||||
|
if (block_abs >= wf->num_blocks)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Get the pointer to symbol 'block' of the candidate
|
||||||
|
const WF_ELEM_T* p8 = mag_cand + (block * wf->block_stride);
|
||||||
|
|
||||||
|
// Weighted difference between the expected and all other symbols
|
||||||
|
// Does not work as well as the alternative score below
|
||||||
|
// score += 8 * p8[kFT8_Costas_pattern[k]] -
|
||||||
|
// p8[0] - p8[1] - p8[2] - p8[3] -
|
||||||
|
// p8[4] - p8[5] - p8[6] - p8[7];
|
||||||
|
// ++num_average;
|
||||||
|
|
||||||
|
// Check only the neighbors of the expected symbol frequency- and time-wise
|
||||||
|
int sm = kFT8_Costas_pattern[k]; // Index of the expected bin
|
||||||
|
if (sm > 0)
|
||||||
|
{
|
||||||
|
// look at one frequency bin lower
|
||||||
|
score += WF_ELEM_MAG_INT(p8[sm]) - WF_ELEM_MAG_INT(p8[sm - 1]);
|
||||||
|
++num_average;
|
||||||
|
}
|
||||||
|
if (sm < 7)
|
||||||
|
{
|
||||||
|
// look at one frequency bin higher
|
||||||
|
score += WF_ELEM_MAG_INT(p8[sm]) - WF_ELEM_MAG_INT(p8[sm + 1]);
|
||||||
|
++num_average;
|
||||||
|
}
|
||||||
|
if ((k > 0) && (block_abs > 0))
|
||||||
|
{
|
||||||
|
// look one symbol back in time
|
||||||
|
score += WF_ELEM_MAG_INT(p8[sm]) - WF_ELEM_MAG_INT(p8[sm - wf->block_stride]);
|
||||||
|
++num_average;
|
||||||
|
}
|
||||||
|
if (((k + 1) < FT8_LENGTH_SYNC) && ((block_abs + 1) < wf->num_blocks))
|
||||||
|
{
|
||||||
|
// look one symbol forward in time
|
||||||
|
score += WF_ELEM_MAG_INT(p8[sm]) - WF_ELEM_MAG_INT(p8[sm + wf->block_stride]);
|
||||||
|
++num_average;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num_average > 0)
|
||||||
|
score /= num_average;
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ft2_sync_score(const ftx_waterfall_t* wf, const ftx_candidate_t* candidate)
|
||||||
|
{
|
||||||
|
const WF_ELEM_T* mag_cand = get_cand_mag(wf, candidate);
|
||||||
|
float score = 0.0f;
|
||||||
|
int groups = 0;
|
||||||
|
|
||||||
|
for (int m = 0; m < FT2_NUM_SYNC; ++m)
|
||||||
|
{
|
||||||
|
float complex sum = 0.0f;
|
||||||
|
bool complete = true;
|
||||||
|
for (int k = 0; k < FT2_LENGTH_SYNC; ++k)
|
||||||
|
{
|
||||||
|
int block = 1 + (FT2_SYNC_OFFSET * m) + k;
|
||||||
|
int block_abs = candidate->time_offset + block;
|
||||||
|
if ((block_abs < 0) || (block_abs >= wf->num_blocks))
|
||||||
|
{
|
||||||
|
complete = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WF_ELEM_T* sym = mag_cand + (block * wf->block_stride);
|
||||||
|
int tone = kFT4_Costas_pattern[m][k];
|
||||||
|
sum += wf_elem_to_complex(sym[tone]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!complete)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
score += cabsf(sum);
|
||||||
|
++groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groups == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return (int)lroundf((score / groups) * 8.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ft4_sync_score(const ftx_waterfall_t* wf, const ftx_candidate_t* candidate)
|
||||||
|
{
|
||||||
|
int score = 0;
|
||||||
|
int num_average = 0;
|
||||||
|
|
||||||
|
// Get the pointer to symbol 0 of the candidate
|
||||||
|
const WF_ELEM_T* mag_cand = get_cand_mag(wf, candidate);
|
||||||
|
|
||||||
|
// Compute average score over sync symbols (block = 1-4, 34-37, 67-70, 100-103)
|
||||||
|
for (int m = 0; m < FT4_NUM_SYNC; ++m)
|
||||||
|
{
|
||||||
|
for (int k = 0; k < FT4_LENGTH_SYNC; ++k)
|
||||||
|
{
|
||||||
|
int block = 1 + (FT4_SYNC_OFFSET * m) + k;
|
||||||
|
int block_abs = candidate->time_offset + block;
|
||||||
|
// Check for time boundaries
|
||||||
|
if (block_abs < 0)
|
||||||
|
continue;
|
||||||
|
if (block_abs >= wf->num_blocks)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Get the pointer to symbol 'block' of the candidate
|
||||||
|
const WF_ELEM_T* p4 = mag_cand + (block * wf->block_stride);
|
||||||
|
|
||||||
|
int sm = kFT4_Costas_pattern[m][k]; // Index of the expected bin
|
||||||
|
|
||||||
|
// score += (4 * p4[sm]) - p4[0] - p4[1] - p4[2] - p4[3];
|
||||||
|
// num_average += 4;
|
||||||
|
|
||||||
|
// Check only the neighbors of the expected symbol frequency- and time-wise
|
||||||
|
if (sm > 0)
|
||||||
|
{
|
||||||
|
// look at one frequency bin lower
|
||||||
|
score += WF_ELEM_MAG_INT(p4[sm]) - WF_ELEM_MAG_INT(p4[sm - 1]);
|
||||||
|
++num_average;
|
||||||
|
}
|
||||||
|
if (sm < 3)
|
||||||
|
{
|
||||||
|
// look at one frequency bin higher
|
||||||
|
score += WF_ELEM_MAG_INT(p4[sm]) - WF_ELEM_MAG_INT(p4[sm + 1]);
|
||||||
|
++num_average;
|
||||||
|
}
|
||||||
|
if ((k > 0) && (block_abs > 0))
|
||||||
|
{
|
||||||
|
// look one symbol back in time
|
||||||
|
score += WF_ELEM_MAG_INT(p4[sm]) - WF_ELEM_MAG_INT(p4[sm - wf->block_stride]);
|
||||||
|
++num_average;
|
||||||
|
}
|
||||||
|
if (((k + 1) < FT4_LENGTH_SYNC) && ((block_abs + 1) < wf->num_blocks))
|
||||||
|
{
|
||||||
|
// look one symbol forward in time
|
||||||
|
score += WF_ELEM_MAG_INT(p4[sm]) - WF_ELEM_MAG_INT(p4[sm + wf->block_stride]);
|
||||||
|
++num_average;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num_average > 0)
|
||||||
|
score /= num_average;
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ftx_find_candidates(const ftx_waterfall_t* wf, int num_candidates, ftx_candidate_t heap[], int min_score)
|
||||||
|
{
|
||||||
|
bool is_ft2 = (wf->protocol == FTX_PROTOCOL_FT2);
|
||||||
|
int (*sync_fun)(const ftx_waterfall_t*, const ftx_candidate_t*) =
|
||||||
|
is_ft2 ? ft2_sync_score : (ftx_protocol_uses_ft4_layout(wf->protocol) ? ft4_sync_score : ft8_sync_score);
|
||||||
|
int num_tones = ftx_protocol_uses_ft4_layout(wf->protocol) ? 4 : 8;
|
||||||
|
int time_offset_min = -10;
|
||||||
|
int time_offset_max = 20;
|
||||||
|
|
||||||
|
if (is_ft2)
|
||||||
|
{
|
||||||
|
time_offset_min = -2;
|
||||||
|
time_offset_max = wf->num_blocks - FT2_NN + 2;
|
||||||
|
if (time_offset_max <= time_offset_min)
|
||||||
|
{
|
||||||
|
time_offset_max = time_offset_min + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (wf->protocol == FTX_PROTOCOL_FT4)
|
||||||
|
{
|
||||||
|
// Keep roughly the same +/- seconds search span used by FT8.
|
||||||
|
// FT4 symbols are much shorter, so it needs a wider symbol-index window.
|
||||||
|
time_offset_min = -34;
|
||||||
|
time_offset_max = wf->num_blocks - FT4_NN + 34;
|
||||||
|
if (time_offset_max <= time_offset_min)
|
||||||
|
{
|
||||||
|
time_offset_max = time_offset_min + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int heap_size = 0;
|
||||||
|
ftx_candidate_t candidate;
|
||||||
|
|
||||||
|
// Here we allow time offsets that exceed signal boundaries, as long as we still have all data bits.
|
||||||
|
// I.e. we can afford to skip the first 7 or the last 7 Costas symbols, as long as we track how many
|
||||||
|
// sync symbols we included in the score, so the score is averaged.
|
||||||
|
for (candidate.time_sub = 0; candidate.time_sub < wf->time_osr; ++candidate.time_sub)
|
||||||
|
{
|
||||||
|
for (candidate.freq_sub = 0; candidate.freq_sub < wf->freq_osr; ++candidate.freq_sub)
|
||||||
|
{
|
||||||
|
for (candidate.time_offset = time_offset_min; candidate.time_offset < time_offset_max; ++candidate.time_offset)
|
||||||
|
{
|
||||||
|
for (candidate.freq_offset = 0; (candidate.freq_offset + num_tones - 1) < wf->num_bins; ++candidate.freq_offset)
|
||||||
|
{
|
||||||
|
candidate.score = sync_fun(wf, &candidate);
|
||||||
|
|
||||||
|
if (candidate.score < min_score)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// If the heap is full AND the current candidate is better than
|
||||||
|
// the worst in the heap, we remove the worst and make space
|
||||||
|
if ((heap_size == num_candidates) && (candidate.score > heap[0].score))
|
||||||
|
{
|
||||||
|
--heap_size;
|
||||||
|
heap[0] = heap[heap_size];
|
||||||
|
heapify_down(heap, heap_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's free space in the heap, we add the current candidate
|
||||||
|
if (heap_size < num_candidates)
|
||||||
|
{
|
||||||
|
heap[heap_size] = candidate;
|
||||||
|
++heap_size;
|
||||||
|
heapify_up(heap, heap_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the candidates by sync strength - here we benefit from the heap structure
|
||||||
|
int len_unsorted = heap_size;
|
||||||
|
while (len_unsorted > 1)
|
||||||
|
{
|
||||||
|
// Take the top (index 0) element which is guaranteed to have the smallest score,
|
||||||
|
// exchange it with the last element in the heap, and decrease the heap size.
|
||||||
|
// Then restore the heap property in the new, smaller heap.
|
||||||
|
// At the end the elements will be sorted in descending order.
|
||||||
|
ftx_candidate_t tmp = heap[len_unsorted - 1];
|
||||||
|
heap[len_unsorted - 1] = heap[0];
|
||||||
|
heap[0] = tmp;
|
||||||
|
len_unsorted--;
|
||||||
|
heapify_down(heap, len_unsorted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return heap_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ft4_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174)
|
||||||
|
{
|
||||||
|
const WF_ELEM_T* mag = get_cand_mag(wf, cand); // Pointer to 4 magnitude bins of the first symbol
|
||||||
|
|
||||||
|
// Go over FSK tones and skip Costas sync symbols
|
||||||
|
for (int k = 0; k < FT4_ND; ++k)
|
||||||
|
{
|
||||||
|
// Skip either 5, 9 or 13 sync symbols
|
||||||
|
// TODO: replace magic numbers with constants
|
||||||
|
int sym_idx = k + ((k < 29) ? 5 : ((k < 58) ? 9 : 13));
|
||||||
|
int bit_idx = 2 * k;
|
||||||
|
|
||||||
|
// Check for time boundaries
|
||||||
|
int block = cand->time_offset + sym_idx;
|
||||||
|
if ((block < 0) || (block >= wf->num_blocks))
|
||||||
|
{
|
||||||
|
log174[bit_idx + 0] = 0;
|
||||||
|
log174[bit_idx + 1] = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ft4_extract_symbol(mag + (sym_idx * wf->block_stride), log174 + bit_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ft2_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174)
|
||||||
|
{
|
||||||
|
const WF_ELEM_T* mag = get_cand_mag(wf, cand);
|
||||||
|
float complex symbols[4][FT2_NN - FT2_NR];
|
||||||
|
float metric1[2 * (FT2_NN - FT2_NR)] = { 0 };
|
||||||
|
float metric2[2 * (FT2_NN - FT2_NR)] = { 0 };
|
||||||
|
float metric4[2 * (FT2_NN - FT2_NR)] = { 0 };
|
||||||
|
|
||||||
|
for (int frame_sym = 0; frame_sym < (FT2_NN - FT2_NR); ++frame_sym)
|
||||||
|
{
|
||||||
|
int sym_idx = frame_sym + 1; // skip ramp-up symbol
|
||||||
|
int block = cand->time_offset + sym_idx;
|
||||||
|
if ((block < 0) || (block >= wf->num_blocks))
|
||||||
|
{
|
||||||
|
for (int tone = 0; tone < 4; ++tone)
|
||||||
|
{
|
||||||
|
symbols[tone][frame_sym] = 0.0f;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WF_ELEM_T* sym = mag + (sym_idx * wf->block_stride);
|
||||||
|
for (int tone = 0; tone < 4; ++tone)
|
||||||
|
{
|
||||||
|
symbols[tone][frame_sym] = wf_elem_to_complex(sym[tone]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int start = 0; start <= (FT2_NN - FT2_NR) - 1; start += 1)
|
||||||
|
{
|
||||||
|
ft2_extract_logl_sequence(symbols, start, 1, metric1 + (2 * start));
|
||||||
|
}
|
||||||
|
for (int start = 0; start <= (FT2_NN - FT2_NR) - 2; start += 2)
|
||||||
|
{
|
||||||
|
ft2_extract_logl_sequence(symbols, start, 2, metric2 + (2 * start));
|
||||||
|
}
|
||||||
|
for (int start = 0; start <= (FT2_NN - FT2_NR) - 4; start += 4)
|
||||||
|
{
|
||||||
|
ft2_extract_logl_sequence(symbols, start, 4, metric4 + (2 * start));
|
||||||
|
}
|
||||||
|
|
||||||
|
metric2[204] = metric1[204];
|
||||||
|
metric2[205] = metric1[205];
|
||||||
|
metric4[200] = metric2[200];
|
||||||
|
metric4[201] = metric2[201];
|
||||||
|
metric4[202] = metric2[202];
|
||||||
|
metric4[203] = metric2[203];
|
||||||
|
metric4[204] = metric1[204];
|
||||||
|
metric4[205] = metric1[205];
|
||||||
|
|
||||||
|
for (int data_sym = 0; data_sym < FT2_ND; ++data_sym)
|
||||||
|
{
|
||||||
|
int frame_sym = data_sym + ((data_sym < 29) ? 4 : ((data_sym < 58) ? 8 : 12));
|
||||||
|
int src_bit = 2 * frame_sym;
|
||||||
|
int dst_bit = 2 * data_sym;
|
||||||
|
|
||||||
|
float a0 = metric1[src_bit + 0];
|
||||||
|
float b0 = metric2[src_bit + 0];
|
||||||
|
float c0 = metric4[src_bit + 0];
|
||||||
|
float a1 = metric1[src_bit + 1];
|
||||||
|
float b1 = metric2[src_bit + 1];
|
||||||
|
float c1 = metric4[src_bit + 1];
|
||||||
|
|
||||||
|
log174[dst_bit + 0] = (fabsf(a0) >= fabsf(b0) && fabsf(a0) >= fabsf(c0)) ? a0 : ((fabsf(b0) >= fabsf(c0)) ? b0 : c0);
|
||||||
|
log174[dst_bit + 1] = (fabsf(a1) >= fabsf(b1) && fabsf(a1) >= fabsf(c1)) ? a1 : ((fabsf(b1) >= fabsf(c1)) ? b1 : c1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ft8_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174)
|
||||||
|
{
|
||||||
|
const WF_ELEM_T* mag = get_cand_mag(wf, cand); // Pointer to 8 magnitude bins of the first symbol
|
||||||
|
|
||||||
|
// Go over FSK tones and skip Costas sync symbols
|
||||||
|
for (int k = 0; k < FT8_ND; ++k)
|
||||||
|
{
|
||||||
|
// Skip either 7 or 14 sync symbols
|
||||||
|
// TODO: replace magic numbers with constants
|
||||||
|
int sym_idx = k + ((k < 29) ? 7 : 14);
|
||||||
|
int bit_idx = 3 * k;
|
||||||
|
|
||||||
|
// Check for time boundaries
|
||||||
|
int block = cand->time_offset + sym_idx;
|
||||||
|
if ((block < 0) || (block >= wf->num_blocks))
|
||||||
|
{
|
||||||
|
log174[bit_idx + 0] = 0;
|
||||||
|
log174[bit_idx + 1] = 0;
|
||||||
|
log174[bit_idx + 2] = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ft8_extract_symbol(mag + (sym_idx * wf->block_stride), log174 + bit_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ftx_normalize_logl(float* log174)
|
||||||
|
{
|
||||||
|
// Compute the variance of log174
|
||||||
|
float sum = 0;
|
||||||
|
float sum2 = 0;
|
||||||
|
for (int i = 0; i < FTX_LDPC_N; ++i)
|
||||||
|
{
|
||||||
|
sum += log174[i];
|
||||||
|
sum2 += log174[i] * log174[i];
|
||||||
|
}
|
||||||
|
float inv_n = 1.0f / FTX_LDPC_N;
|
||||||
|
float variance = (sum2 - (sum * sum * inv_n)) * inv_n;
|
||||||
|
|
||||||
|
// Normalize log174 distribution and scale it with experimentally found coefficient
|
||||||
|
float norm_factor = sqrtf(24.0f / variance);
|
||||||
|
for (int i = 0; i < FTX_LDPC_N; ++i)
|
||||||
|
{
|
||||||
|
log174[i] *= norm_factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ftx_decode_candidate(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, int max_iterations, ftx_message_t* message, ftx_decode_status_t* status)
|
||||||
|
{
|
||||||
|
float log174[FTX_LDPC_N]; // message bits encoded as likelihood
|
||||||
|
if (wf->protocol == FTX_PROTOCOL_FT2)
|
||||||
|
{
|
||||||
|
ft2_extract_likelihood(wf, cand, log174);
|
||||||
|
}
|
||||||
|
else if (ftx_protocol_uses_ft4_layout(wf->protocol))
|
||||||
|
{
|
||||||
|
ft4_extract_likelihood(wf, cand, log174);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ft8_extract_likelihood(wf, cand, log174);
|
||||||
|
}
|
||||||
|
|
||||||
|
ftx_normalize_logl(log174);
|
||||||
|
|
||||||
|
uint8_t plain174[FTX_LDPC_N]; // message bits (0/1)
|
||||||
|
bp_decode(log174, max_iterations, plain174, &status->ldpc_errors);
|
||||||
|
// ldpc_decode(log174, max_iterations, plain174, &status->ldpc_errors);
|
||||||
|
|
||||||
|
if (status->ldpc_errors > 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract payload + CRC (first FTX_LDPC_K bits) packed into a byte array
|
||||||
|
uint8_t a91[FTX_LDPC_K_BYTES];
|
||||||
|
pack_bits(plain174, FTX_LDPC_K, a91);
|
||||||
|
|
||||||
|
// Extract CRC and check it
|
||||||
|
status->crc_extracted = ftx_extract_crc(a91);
|
||||||
|
// [1]: 'The CRC is calculated on the source-encoded message, zero-extended from 77 to 82 bits.'
|
||||||
|
a91[9] &= 0xF8;
|
||||||
|
a91[10] &= 0x00;
|
||||||
|
status->crc_calculated = ftx_compute_crc(a91, 96 - 14);
|
||||||
|
|
||||||
|
if (status->crc_extracted != status->crc_calculated)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse CRC value as a hash for the message (TODO: 14 bits only, should perhaps use full 16 or 32 bits?)
|
||||||
|
message->hash = status->crc_calculated;
|
||||||
|
|
||||||
|
if (ftx_protocol_uses_ft4_layout(wf->protocol))
|
||||||
|
{
|
||||||
|
// '[..] for FT4 only, in order to avoid transmitting a long string of zeros when sending CQ messages,
|
||||||
|
// the assembled 77-bit message is bitwise exclusive-OR’ed with [a] pseudorandom sequence before computing the CRC and FEC parity bits'
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
{
|
||||||
|
message->payload[i] = a91[i] ^ kFT4_XOR_sequence[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
{
|
||||||
|
message->payload[i] = a91[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOG(LOG_DEBUG, "Decoded message (CRC %04x), trying to unpack...\n", status->crc_extracted);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float max2(float a, float b)
|
||||||
|
{
|
||||||
|
return (a >= b) ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float max4(float a, float b, float c, float d)
|
||||||
|
{
|
||||||
|
return max2(max2(a, b), max2(c, d));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void heapify_down(ftx_candidate_t heap[], int heap_size)
|
||||||
|
{
|
||||||
|
// heapify from the root down
|
||||||
|
int current = 0; // root node
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int left = 2 * current + 1;
|
||||||
|
int right = left + 1;
|
||||||
|
|
||||||
|
// Find the smallest value of (parent, left child, right child)
|
||||||
|
int smallest = current;
|
||||||
|
if ((left < heap_size) && (heap[left].score < heap[smallest].score))
|
||||||
|
{
|
||||||
|
smallest = left;
|
||||||
|
}
|
||||||
|
if ((right < heap_size) && (heap[right].score < heap[smallest].score))
|
||||||
|
{
|
||||||
|
smallest = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smallest == current)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange the current node with the smallest child and move down to it
|
||||||
|
ftx_candidate_t tmp = heap[smallest];
|
||||||
|
heap[smallest] = heap[current];
|
||||||
|
heap[current] = tmp;
|
||||||
|
current = smallest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void heapify_up(ftx_candidate_t heap[], int heap_size)
|
||||||
|
{
|
||||||
|
// heapify from the last node up
|
||||||
|
int current = heap_size - 1;
|
||||||
|
while (current > 0)
|
||||||
|
{
|
||||||
|
int parent = (current - 1) / 2;
|
||||||
|
if (!(heap[current].score < heap[parent].score))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange the current node with its parent and move up
|
||||||
|
ftx_candidate_t tmp = heap[parent];
|
||||||
|
heap[parent] = heap[current];
|
||||||
|
heap[current] = tmp;
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute unnormalized log likelihood log(p(1) / p(0)) of 2 message bits (1 FSK symbol)
|
||||||
|
static void ft4_extract_symbol(const WF_ELEM_T* wf, float* logl)
|
||||||
|
{
|
||||||
|
// Cleaned up code for the simple case of n_syms==1
|
||||||
|
float s2[4];
|
||||||
|
|
||||||
|
for (int j = 0; j < 4; ++j)
|
||||||
|
{
|
||||||
|
s2[j] = WF_ELEM_MAG(wf[kFT4_Gray_map[j]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
logl[0] = max2(s2[2], s2[3]) - max2(s2[0], s2[1]);
|
||||||
|
logl[1] = max2(s2[1], s2[3]) - max2(s2[0], s2[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ft2_extract_logl_sequence(const float complex symbols[4][FT2_NN - FT2_NR], int start_sym, int n_syms, float* metrics)
|
||||||
|
{
|
||||||
|
const int n_bits = 2 * n_syms;
|
||||||
|
const int n_sequences = 1 << n_bits;
|
||||||
|
|
||||||
|
for (int bit = 0; bit < n_bits; ++bit)
|
||||||
|
{
|
||||||
|
float max_zero = -INFINITY;
|
||||||
|
float max_one = -INFINITY;
|
||||||
|
for (int seq = 0; seq < n_sequences; ++seq)
|
||||||
|
{
|
||||||
|
float complex sum = 0.0f;
|
||||||
|
for (int sym = 0; sym < n_syms; ++sym)
|
||||||
|
{
|
||||||
|
int shift = 2 * (n_syms - sym - 1);
|
||||||
|
int dibit = (seq >> shift) & 0x3;
|
||||||
|
int tone = kFT4_Gray_map[dibit];
|
||||||
|
sum += symbols[tone][start_sym + sym];
|
||||||
|
}
|
||||||
|
float strength = cabsf(sum);
|
||||||
|
int mask_bit = n_bits - bit - 1;
|
||||||
|
if (((seq >> mask_bit) & 0x1) != 0)
|
||||||
|
{
|
||||||
|
if (strength > max_one)
|
||||||
|
max_one = strength;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (strength > max_zero)
|
||||||
|
max_zero = strength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metrics[bit] = max_one - max_zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute unnormalized log likelihood log(p(1) / p(0)) of 3 message bits (1 FSK symbol)
|
||||||
|
static void ft8_extract_symbol(const WF_ELEM_T* wf, float* logl)
|
||||||
|
{
|
||||||
|
// Cleaned up code for the simple case of n_syms==1
|
||||||
|
#if 1
|
||||||
|
float s2[8];
|
||||||
|
|
||||||
|
for (int j = 0; j < 8; ++j)
|
||||||
|
{
|
||||||
|
s2[j] = WF_ELEM_MAG(wf[kFT8_Gray_map[j]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
logl[0] = max4(s2[4], s2[5], s2[6], s2[7]) - max4(s2[0], s2[1], s2[2], s2[3]);
|
||||||
|
logl[1] = max4(s2[2], s2[3], s2[6], s2[7]) - max4(s2[0], s2[1], s2[4], s2[5]);
|
||||||
|
logl[2] = max4(s2[1], s2[3], s2[5], s2[7]) - max4(s2[0], s2[2], s2[4], s2[6]);
|
||||||
|
#else
|
||||||
|
float a[7] = {
|
||||||
|
// (float)wf[7] - (float)wf[0], // 0: p(111) / p(000)
|
||||||
|
(float)wf[5] - (float)wf[2], // 0: p(100) / p(011)
|
||||||
|
(float)wf[3] - (float)wf[0], // 1: p(010) / p(000)
|
||||||
|
(float)wf[6] - (float)wf[3], // 2: p(101) / p(010)
|
||||||
|
(float)wf[6] - (float)wf[2], // 3: p(101) / p(011)
|
||||||
|
(float)wf[7] - (float)wf[4], // 4: p(111) / p(110)
|
||||||
|
(float)wf[4] - (float)wf[1], // 5: p(110) / p(001)
|
||||||
|
(float)wf[5] - (float)wf[1] // 6: p(100) / p(001)
|
||||||
|
};
|
||||||
|
float k = 1.0f;
|
||||||
|
|
||||||
|
// logl[0] = k * (a[0] + a[2] + a[3] + a[5] + a[6]) / 5;
|
||||||
|
// logl[1] = k * (a[0] / 4 + (a[1] - a[3]) * 5 / 24 + (a[5] - a[2]) / 6 + (a[4] - a[6]) / 24);
|
||||||
|
// logl[2] = k * (a[0] / 4 + (a[1] - a[3]) / 24 + (a[2] - a[5]) / 6 + (a[4] - a[6]) * 5 / 24);
|
||||||
|
logl[0] = k * (a[1] / 6 + a[2] / 3 + a[3] / 6 + a[4] / 6 + a[5] / 3 + a[6] / 6);
|
||||||
|
logl[1] = k * (-a[0] / 4 + a[1] * 7 / 24 + (a[4] - a[3]) / 8 + a[5] / 3 + a[6] / 24);
|
||||||
|
logl[2] = k * (-a[0] / 4 + (a[1] - a[6]) / 8 + a[2] / 3 + a[3] / 24 + a[4] * 7 / 24 - a[5] * 5 / 18);
|
||||||
|
#endif
|
||||||
|
// for (int i = 0; i < 8; ++i)
|
||||||
|
// printf("%d ", WF_ELEM_MAG_INT(wf[i]));
|
||||||
|
// for (int i = 0; i < 3; ++i)
|
||||||
|
// printf("%.1f ", logl[i]);
|
||||||
|
// printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute unnormalized log likelihood log(p(1) / p(0)) of bits corresponding to several FSK symbols at once
|
||||||
|
static void ft8_decode_multi_symbols(const WF_ELEM_T* wf, int num_bins, int n_syms, int bit_idx, float* log174)
|
||||||
|
{
|
||||||
|
const int n_bits = 3 * n_syms;
|
||||||
|
const int n_tones = (1 << n_bits);
|
||||||
|
|
||||||
|
float s2[n_tones];
|
||||||
|
|
||||||
|
for (int j = 0; j < n_tones; ++j)
|
||||||
|
{
|
||||||
|
int j1 = j & 0x07;
|
||||||
|
if (n_syms == 1)
|
||||||
|
{
|
||||||
|
s2[j] = WF_ELEM_MAG(wf[kFT8_Gray_map[j1]]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int j2 = (j >> 3) & 0x07;
|
||||||
|
if (n_syms == 2)
|
||||||
|
{
|
||||||
|
s2[j] = WF_ELEM_MAG(wf[kFT8_Gray_map[j2]]);
|
||||||
|
s2[j] += WF_ELEM_MAG(wf[kFT8_Gray_map[j1] + 4 * num_bins]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int j3 = (j >> 6) & 0x07;
|
||||||
|
s2[j] = WF_ELEM_MAG(wf[kFT8_Gray_map[j3]]);
|
||||||
|
s2[j] += WF_ELEM_MAG(wf[kFT8_Gray_map[j2] + 4 * num_bins]);
|
||||||
|
s2[j] += WF_ELEM_MAG(wf[kFT8_Gray_map[j1] + 8 * num_bins]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract bit significance (and convert them to float)
|
||||||
|
// 8 FSK tones = 3 bits
|
||||||
|
for (int i = 0; i < n_bits; ++i)
|
||||||
|
{
|
||||||
|
if (bit_idx + i >= FTX_LDPC_N)
|
||||||
|
{
|
||||||
|
// Respect array size
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t mask = (n_tones >> (i + 1));
|
||||||
|
float max_zero = -1000, max_one = -1000;
|
||||||
|
for (int n = 0; n < n_tones; ++n)
|
||||||
|
{
|
||||||
|
if (n & mask)
|
||||||
|
{
|
||||||
|
max_one = max2(max_one, s2[n]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
max_zero = max2(max_zero, s2[n]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log174[bit_idx + i] = max_one - max_zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packs a string of bits each represented as a zero/non-zero byte in plain[],
|
||||||
|
// as a string of packed bits starting from the MSB of the first byte of packed[]
|
||||||
|
static void pack_bits(const uint8_t bit_array[], int num_bits, uint8_t packed[])
|
||||||
|
{
|
||||||
|
int num_bytes = (num_bits + 7) / 8;
|
||||||
|
for (int i = 0; i < num_bytes; ++i)
|
||||||
|
{
|
||||||
|
packed[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t mask = 0x80;
|
||||||
|
int byte_idx = 0;
|
||||||
|
for (int i = 0; i < num_bits; ++i)
|
||||||
|
{
|
||||||
|
if (bit_array[i])
|
||||||
|
{
|
||||||
|
packed[byte_idx] |= mask;
|
||||||
|
}
|
||||||
|
mask >>= 1;
|
||||||
|
if (!mask)
|
||||||
|
{
|
||||||
|
mask = 0x80;
|
||||||
|
++byte_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+96
@@ -0,0 +1,96 @@
|
|||||||
|
#ifndef _INCLUDE_DECODE_H_
|
||||||
|
#define _INCLUDE_DECODE_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include "constants.h"
|
||||||
|
#include "message.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
float mag;
|
||||||
|
float phase;
|
||||||
|
} waterfall_cpx_t;
|
||||||
|
|
||||||
|
#define WATERFALL_USE_PHASE
|
||||||
|
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
#define WF_ELEM_T waterfall_cpx_t
|
||||||
|
#define WF_ELEM_MAG(x) ((x).mag)
|
||||||
|
#define WF_ELEM_MAG_INT(x) (int)(2 * ((x).mag + 120.0f))
|
||||||
|
#else
|
||||||
|
#define WF_ELEM_T uint8_t
|
||||||
|
#define WF_ELEM_MAG(x) ((float)(x)*0.5f - 120.0f)
|
||||||
|
#define WF_ELEM_MAG_INT(x) (int)(x)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Input structure to ftx_find_sync() function. This structure describes stored waterfall data over the whole message slot.
|
||||||
|
/// Fields time_osr and freq_osr specify additional oversampling rate for time and frequency resolution.
|
||||||
|
/// If time_osr=1, FFT magnitude data is collected once for every symbol transmitted, i.e. every 1/6.25 = 0.16 seconds.
|
||||||
|
/// Values time_osr > 1 mean each symbol is further subdivided in time.
|
||||||
|
/// If freq_osr=1, each bin in the FFT magnitude data corresponds to 6.25 Hz, which is the tone spacing.
|
||||||
|
/// Values freq_osr > 1 mean the tone spacing is further subdivided by FFT analysis.
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
int max_blocks; ///< number of blocks (symbols) allocated in the mag array
|
||||||
|
int num_blocks; ///< number of blocks (symbols) stored in the mag array
|
||||||
|
int num_bins; ///< number of FFT bins in terms of 6.25 Hz
|
||||||
|
int time_osr; ///< number of time subdivisions
|
||||||
|
int freq_osr; ///< number of frequency subdivisions
|
||||||
|
WF_ELEM_T* mag; ///< FFT magnitudes stored as uint8_t[blocks][time_osr][freq_osr][num_bins]
|
||||||
|
int block_stride; ///< Helper value = time_osr * freq_osr * num_bins
|
||||||
|
ftx_protocol_t protocol; ///< Indicate if using FT4 or FT8
|
||||||
|
} ftx_waterfall_t;
|
||||||
|
|
||||||
|
/// Output structure of ftx_find_sync() and input structure of ftx_decode().
|
||||||
|
/// Holds the position of potential start of a message in time and frequency.
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
int16_t score; ///< Candidate score (non-negative number; higher score means higher likelihood)
|
||||||
|
int16_t time_offset; ///< Index of the time block
|
||||||
|
int16_t freq_offset; ///< Index of the frequency bin
|
||||||
|
uint8_t time_sub; ///< Index of the time subdivision used
|
||||||
|
uint8_t freq_sub; ///< Index of the frequency subdivision used
|
||||||
|
} ftx_candidate_t;
|
||||||
|
|
||||||
|
/// Structure that contains the status of various steps during decoding of a message
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
float freq;
|
||||||
|
float time;
|
||||||
|
int ldpc_errors; ///< Number of LDPC errors during decoding
|
||||||
|
uint16_t crc_extracted; ///< CRC value recovered from the message
|
||||||
|
uint16_t crc_calculated; ///< CRC value calculated over the payload
|
||||||
|
// int unpack_status; ///< Return value of the unpack routine
|
||||||
|
} ftx_decode_status_t;
|
||||||
|
|
||||||
|
/// Localize top N candidates in frequency and time according to their sync strength (looking at Costas symbols)
|
||||||
|
/// We treat and organize the candidate list as a min-heap (empty initially).
|
||||||
|
/// @param[in] power Waterfall data collected during message slot
|
||||||
|
/// @param[in] sync_pattern Synchronization pattern
|
||||||
|
/// @param[in] num_candidates Number of maximum candidates (size of heap array)
|
||||||
|
/// @param[in,out] heap Array of ftx_candidate_t type entries (with num_candidates allocated entries)
|
||||||
|
/// @param[in] min_score Minimal score allowed for pruning unlikely candidates (can be zero for no effect)
|
||||||
|
/// @return Number of candidates filled in the heap
|
||||||
|
int ftx_find_candidates(const ftx_waterfall_t* power, int num_candidates, ftx_candidate_t heap[], int min_score);
|
||||||
|
|
||||||
|
/// Attempt to decode a message candidate. Extracts the bit probabilities, runs LDPC decoder, checks CRC and unpacks the message in plain text.
|
||||||
|
/// @param[in] power Waterfall data collected during message slot
|
||||||
|
/// @param[in] cand Candidate to decode
|
||||||
|
/// @param[in] max_iterations Maximum allowed LDPC iterations (lower number means faster decode, but less precise)
|
||||||
|
/// @param[out] message ftx_message_t structure that will receive the decoded message
|
||||||
|
/// @param[out] status ftx_decode_status_t structure that will be filled with the status of various decoding steps
|
||||||
|
/// @return True if the decoding was successful, false otherwise (check status for details)
|
||||||
|
bool ftx_decode_candidate(const ftx_waterfall_t* power, const ftx_candidate_t* cand, int max_iterations, ftx_message_t* message, ftx_decode_status_t* status);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_DECODE_H_
|
||||||
Vendored
+200
@@ -0,0 +1,200 @@
|
|||||||
|
#include "encode.h"
|
||||||
|
#include "constants.h"
|
||||||
|
#include "crc.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
// Returns 1 if an odd number of bits are set in x, zero otherwise
|
||||||
|
static uint8_t parity8(uint8_t x)
|
||||||
|
{
|
||||||
|
x ^= x >> 4; // a b c d ae bf cg dh
|
||||||
|
x ^= x >> 2; // a b ac bd cae dbf aecg bfdh
|
||||||
|
x ^= x >> 1; // a ab bac acbd bdcae caedbf aecgbfdh
|
||||||
|
return x % 2; // modulo 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode via LDPC a 91-bit message and return a 174-bit codeword.
|
||||||
|
// The generator matrix has dimensions (87,87).
|
||||||
|
// The code is a (174,91) regular LDPC code with column weight 3.
|
||||||
|
// Arguments:
|
||||||
|
// [IN] message - array of 91 bits stored as 12 bytes (MSB first)
|
||||||
|
// [OUT] codeword - array of 174 bits stored as 22 bytes (MSB first)
|
||||||
|
static void encode174(const uint8_t* message, uint8_t* codeword)
|
||||||
|
{
|
||||||
|
// This implementation accesses the generator bits straight from the packed binary representation in kFTX_LDPC_generator
|
||||||
|
|
||||||
|
// Fill the codeword with message and zeros, as we will only update binary ones later
|
||||||
|
for (int j = 0; j < FTX_LDPC_N_BYTES; ++j)
|
||||||
|
{
|
||||||
|
codeword[j] = (j < FTX_LDPC_K_BYTES) ? message[j] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the byte index and bit mask for the first checksum bit
|
||||||
|
uint8_t col_mask = (0x80u >> (FTX_LDPC_K % 8u)); // bitmask of current byte
|
||||||
|
uint8_t col_idx = FTX_LDPC_K_BYTES - 1; // index into byte array
|
||||||
|
|
||||||
|
// Compute the LDPC checksum bits and store them in codeword
|
||||||
|
for (int i = 0; i < FTX_LDPC_M; ++i)
|
||||||
|
{
|
||||||
|
// Fast implementation of bitwise multiplication and parity checking
|
||||||
|
// Normally nsum would contain the result of dot product between message and kFTX_LDPC_generator[i],
|
||||||
|
// but we only compute the sum modulo 2.
|
||||||
|
uint8_t nsum = 0;
|
||||||
|
for (int j = 0; j < FTX_LDPC_K_BYTES; ++j)
|
||||||
|
{
|
||||||
|
uint8_t bits = message[j] & kFTX_LDPC_generator[i][j]; // bitwise AND (bitwise multiplication)
|
||||||
|
nsum ^= parity8(bits); // bitwise XOR (addition modulo 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the current checksum bit in codeword if nsum is odd
|
||||||
|
if (nsum % 2)
|
||||||
|
{
|
||||||
|
codeword[col_idx] |= col_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the byte index and bit mask for the next checksum bit
|
||||||
|
col_mask >>= 1;
|
||||||
|
if (col_mask == 0)
|
||||||
|
{
|
||||||
|
col_mask = 0x80u;
|
||||||
|
++col_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ft8_encode(const uint8_t* payload, uint8_t* tones)
|
||||||
|
{
|
||||||
|
uint8_t a91[FTX_LDPC_K_BYTES]; // Store 77 bits of payload + 14 bits CRC
|
||||||
|
|
||||||
|
// Compute and add CRC at the end of the message
|
||||||
|
// a91 contains 77 bits of payload + 14 bits of CRC
|
||||||
|
ftx_add_crc(payload, a91);
|
||||||
|
|
||||||
|
uint8_t codeword[FTX_LDPC_N_BYTES];
|
||||||
|
encode174(a91, codeword);
|
||||||
|
|
||||||
|
// Message structure: S7 D29 S7 D29 S7
|
||||||
|
// Total symbols: 79 (FT8_NN)
|
||||||
|
|
||||||
|
uint8_t mask = 0x80u; // Mask to extract 1 bit from codeword
|
||||||
|
int i_byte = 0; // Index of the current byte of the codeword
|
||||||
|
for (int i_tone = 0; i_tone < FT8_NN; ++i_tone)
|
||||||
|
{
|
||||||
|
if ((i_tone >= 0) && (i_tone < 7))
|
||||||
|
{
|
||||||
|
tones[i_tone] = kFT8_Costas_pattern[i_tone];
|
||||||
|
}
|
||||||
|
else if ((i_tone >= 36) && (i_tone < 43))
|
||||||
|
{
|
||||||
|
tones[i_tone] = kFT8_Costas_pattern[i_tone - 36];
|
||||||
|
}
|
||||||
|
else if ((i_tone >= 72) && (i_tone < 79))
|
||||||
|
{
|
||||||
|
tones[i_tone] = kFT8_Costas_pattern[i_tone - 72];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Extract 3 bits from codeword at i-th position
|
||||||
|
uint8_t bits3 = 0;
|
||||||
|
|
||||||
|
if (codeword[i_byte] & mask)
|
||||||
|
bits3 |= 4;
|
||||||
|
if (0 == (mask >>= 1))
|
||||||
|
{
|
||||||
|
mask = 0x80u;
|
||||||
|
i_byte++;
|
||||||
|
}
|
||||||
|
if (codeword[i_byte] & mask)
|
||||||
|
bits3 |= 2;
|
||||||
|
if (0 == (mask >>= 1))
|
||||||
|
{
|
||||||
|
mask = 0x80u;
|
||||||
|
i_byte++;
|
||||||
|
}
|
||||||
|
if (codeword[i_byte] & mask)
|
||||||
|
bits3 |= 1;
|
||||||
|
if (0 == (mask >>= 1))
|
||||||
|
{
|
||||||
|
mask = 0x80u;
|
||||||
|
i_byte++;
|
||||||
|
}
|
||||||
|
|
||||||
|
tones[i_tone] = kFT8_Gray_map[bits3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ft4_encode(const uint8_t* payload, uint8_t* tones)
|
||||||
|
{
|
||||||
|
uint8_t a91[FTX_LDPC_K_BYTES]; // Store 77 bits of payload + 14 bits CRC
|
||||||
|
uint8_t payload_xor[10]; // Encoded payload data
|
||||||
|
|
||||||
|
// '[..] for FT4 only, in order to avoid transmitting a long string of zeros when sending CQ messages,
|
||||||
|
// the assembled 77-bit message is bitwise exclusive-OR’ed with [a] pseudorandom sequence before computing the CRC and FEC parity bits'
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
{
|
||||||
|
payload_xor[i] = payload[i] ^ kFT4_XOR_sequence[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute and add CRC at the end of the message
|
||||||
|
// a91 contains 77 bits of payload + 14 bits of CRC
|
||||||
|
ftx_add_crc(payload_xor, a91);
|
||||||
|
|
||||||
|
uint8_t codeword[FTX_LDPC_N_BYTES];
|
||||||
|
encode174(a91, codeword); // 91 bits -> 174 bits
|
||||||
|
|
||||||
|
// Message structure: R S4_1 D29 S4_2 D29 S4_3 D29 S4_4 R
|
||||||
|
// Total symbols: 105 (FT4_NN)
|
||||||
|
|
||||||
|
uint8_t mask = 0x80u; // Mask to extract 1 bit from codeword
|
||||||
|
int i_byte = 0; // Index of the current byte of the codeword
|
||||||
|
for (int i_tone = 0; i_tone < FT4_NN; ++i_tone)
|
||||||
|
{
|
||||||
|
if ((i_tone == 0) || (i_tone == 104))
|
||||||
|
{
|
||||||
|
tones[i_tone] = 0; // R (ramp) symbol
|
||||||
|
}
|
||||||
|
else if ((i_tone >= 1) && (i_tone < 5))
|
||||||
|
{
|
||||||
|
tones[i_tone] = kFT4_Costas_pattern[0][i_tone - 1];
|
||||||
|
}
|
||||||
|
else if ((i_tone >= 34) && (i_tone < 38))
|
||||||
|
{
|
||||||
|
tones[i_tone] = kFT4_Costas_pattern[1][i_tone - 34];
|
||||||
|
}
|
||||||
|
else if ((i_tone >= 67) && (i_tone < 71))
|
||||||
|
{
|
||||||
|
tones[i_tone] = kFT4_Costas_pattern[2][i_tone - 67];
|
||||||
|
}
|
||||||
|
else if ((i_tone >= 100) && (i_tone < 104))
|
||||||
|
{
|
||||||
|
tones[i_tone] = kFT4_Costas_pattern[3][i_tone - 100];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Extract 2 bits from codeword at i-th position
|
||||||
|
uint8_t bits2 = 0;
|
||||||
|
|
||||||
|
if (codeword[i_byte] & mask)
|
||||||
|
bits2 |= 2;
|
||||||
|
if (0 == (mask >>= 1))
|
||||||
|
{
|
||||||
|
mask = 0x80u;
|
||||||
|
i_byte++;
|
||||||
|
}
|
||||||
|
if (codeword[i_byte] & mask)
|
||||||
|
bits2 |= 1;
|
||||||
|
if (0 == (mask >>= 1))
|
||||||
|
{
|
||||||
|
mask = 0x80u;
|
||||||
|
i_byte++;
|
||||||
|
}
|
||||||
|
tones[i_tone] = kFT4_Gray_map[bits2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ft2_encode(const uint8_t* payload, uint8_t* tones)
|
||||||
|
{
|
||||||
|
ft4_encode(payload, tones);
|
||||||
|
}
|
||||||
Vendored
+47
@@ -0,0 +1,47 @@
|
|||||||
|
#ifndef _INCLUDE_ENCODE_H_
|
||||||
|
#define _INCLUDE_ENCODE_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// typedef struct
|
||||||
|
// {
|
||||||
|
// uint8_t tones[FT8_NN];
|
||||||
|
// // for waveform readout:
|
||||||
|
// int n_spsym; // Number of waveform samples per symbol
|
||||||
|
// float *pulse; // [3 * n_spsym]
|
||||||
|
// int idx_symbol; // Index of the current symbol
|
||||||
|
// float f0; // Base frequency, Hertz
|
||||||
|
// float signal_rate; // Waveform sample rate, Hertz
|
||||||
|
// } encoder_t;
|
||||||
|
|
||||||
|
// void encoder_init(float signal_rate, float *pulse_buffer);
|
||||||
|
// void encoder_set_f0(float f0);
|
||||||
|
// void encoder_process(const message_t *message); // in: message
|
||||||
|
// void encoder_generate(float *block); // out: block of waveforms
|
||||||
|
|
||||||
|
/// Generate FT8 tone sequence from payload data
|
||||||
|
/// @param[in] payload - 10 byte array consisting of 77 bit payload
|
||||||
|
/// @param[out] tones - array of FT8_NN (79) bytes to store the generated tones (encoded as 0..7)
|
||||||
|
void ft8_encode(const uint8_t* payload, uint8_t* tones);
|
||||||
|
|
||||||
|
/// Generate FT4 tone sequence from payload data
|
||||||
|
/// @param[in] payload - 10 byte array consisting of 77 bit payload
|
||||||
|
/// @param[out] tones - array of FT4_NN (105) bytes to store the generated tones (encoded as 0..3)
|
||||||
|
void ft4_encode(const uint8_t* payload, uint8_t* tones);
|
||||||
|
|
||||||
|
/// Generate FT2 tone sequence from payload data.
|
||||||
|
/// FT2 uses the FT4 framing with a doubled symbol rate.
|
||||||
|
/// @param[in] payload - 10 byte array consisting of 77 bit payload
|
||||||
|
/// @param[out] tones - array of FT2_NN (105) bytes to store the generated tones (encoded as 0..3)
|
||||||
|
void ft2_encode(const uint8_t* payload, uint8_t* tones);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_ENCODE_H_
|
||||||
Vendored
+251
@@ -0,0 +1,251 @@
|
|||||||
|
//
|
||||||
|
// LDPC decoder for FT8.
|
||||||
|
//
|
||||||
|
// given a 174-bit codeword as an array of log-likelihood of zero,
|
||||||
|
// return a 174-bit corrected codeword, or zero-length array.
|
||||||
|
// last 87 bits are the (systematic) plain-text.
|
||||||
|
// this is an implementation of the sum-product algorithm
|
||||||
|
// from Sarah Johnson's Iterative Error Correction book.
|
||||||
|
// codeword[i] = log ( P(x=0) / P(x=1) )
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "ldpc.h"
|
||||||
|
#include "constants.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
static int ldpc_check(uint8_t codeword[]);
|
||||||
|
static float fast_tanh(float x);
|
||||||
|
static float fast_atanh(float x);
|
||||||
|
|
||||||
|
// codeword is 174 log-likelihoods.
|
||||||
|
// plain is a return value, 174 ints, to be 0 or 1.
|
||||||
|
// max_iters is how hard to try.
|
||||||
|
// ok == 87 means success.
|
||||||
|
void ldpc_decode(float codeword[], int max_iters, uint8_t plain[], int* ok)
|
||||||
|
{
|
||||||
|
float m[FTX_LDPC_M][FTX_LDPC_N]; // ~60 kB
|
||||||
|
float e[FTX_LDPC_M][FTX_LDPC_N]; // ~60 kB
|
||||||
|
int min_errors = FTX_LDPC_M;
|
||||||
|
|
||||||
|
for (int j = 0; j < FTX_LDPC_M; j++)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < FTX_LDPC_N; i++)
|
||||||
|
{
|
||||||
|
m[j][i] = codeword[i];
|
||||||
|
e[j][i] = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int iter = 0; iter < max_iters; iter++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < FTX_LDPC_M; j++)
|
||||||
|
{
|
||||||
|
for (int ii1 = 0; ii1 < kFTX_LDPC_Num_rows[j]; ii1++)
|
||||||
|
{
|
||||||
|
int i1 = kFTX_LDPC_Nm[j][ii1] - 1;
|
||||||
|
float a = 1.0f;
|
||||||
|
for (int ii2 = 0; ii2 < kFTX_LDPC_Num_rows[j]; ii2++)
|
||||||
|
{
|
||||||
|
int i2 = kFTX_LDPC_Nm[j][ii2] - 1;
|
||||||
|
if (i2 != i1)
|
||||||
|
{
|
||||||
|
a *= fast_tanh(-m[j][i2] / 2.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e[j][i1] = -2.0f * fast_atanh(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < FTX_LDPC_N; i++)
|
||||||
|
{
|
||||||
|
float l = codeword[i];
|
||||||
|
for (int j = 0; j < 3; j++)
|
||||||
|
l += e[kFTX_LDPC_Mn[i][j] - 1][i];
|
||||||
|
plain[i] = (l > 0) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int errors = ldpc_check(plain);
|
||||||
|
|
||||||
|
if (errors < min_errors)
|
||||||
|
{
|
||||||
|
// Update the current best result
|
||||||
|
min_errors = errors;
|
||||||
|
|
||||||
|
if (errors == 0)
|
||||||
|
{
|
||||||
|
break; // Found a perfect answer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < FTX_LDPC_N; i++)
|
||||||
|
{
|
||||||
|
for (int ji1 = 0; ji1 < 3; ji1++)
|
||||||
|
{
|
||||||
|
int j1 = kFTX_LDPC_Mn[i][ji1] - 1;
|
||||||
|
float l = codeword[i];
|
||||||
|
for (int ji2 = 0; ji2 < 3; ji2++)
|
||||||
|
{
|
||||||
|
if (ji1 != ji2)
|
||||||
|
{
|
||||||
|
int j2 = kFTX_LDPC_Mn[i][ji2] - 1;
|
||||||
|
l += e[j2][i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m[j1][i] = l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*ok = min_errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// does a 174-bit codeword pass the FT8's LDPC parity checks?
|
||||||
|
// returns the number of parity errors.
|
||||||
|
// 0 means total success.
|
||||||
|
//
|
||||||
|
static int ldpc_check(uint8_t codeword[])
|
||||||
|
{
|
||||||
|
int errors = 0;
|
||||||
|
|
||||||
|
for (int m = 0; m < FTX_LDPC_M; ++m)
|
||||||
|
{
|
||||||
|
uint8_t x = 0;
|
||||||
|
for (int i = 0; i < kFTX_LDPC_Num_rows[m]; ++i)
|
||||||
|
{
|
||||||
|
x ^= codeword[kFTX_LDPC_Nm[m][i] - 1];
|
||||||
|
}
|
||||||
|
if (x != 0)
|
||||||
|
{
|
||||||
|
++errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bp_decode(float codeword[], int max_iters, uint8_t plain[], int* ok)
|
||||||
|
{
|
||||||
|
float tov[FTX_LDPC_N][3];
|
||||||
|
float toc[FTX_LDPC_M][7];
|
||||||
|
|
||||||
|
int min_errors = FTX_LDPC_M;
|
||||||
|
|
||||||
|
// initialize message data
|
||||||
|
for (int n = 0; n < FTX_LDPC_N; ++n)
|
||||||
|
{
|
||||||
|
tov[n][0] = tov[n][1] = tov[n][2] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int iter = 0; iter < max_iters; ++iter)
|
||||||
|
{
|
||||||
|
// Do a hard decision guess (tov=0 in iter 0)
|
||||||
|
int plain_sum = 0;
|
||||||
|
for (int n = 0; n < FTX_LDPC_N; ++n)
|
||||||
|
{
|
||||||
|
plain[n] = ((codeword[n] + tov[n][0] + tov[n][1] + tov[n][2]) > 0) ? 1 : 0;
|
||||||
|
plain_sum += plain[n];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plain_sum == 0)
|
||||||
|
{
|
||||||
|
// message converged to all-zeros, which is prohibited
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if we have a codeword (check before we do any iter)
|
||||||
|
int errors = ldpc_check(plain);
|
||||||
|
|
||||||
|
if (errors < min_errors)
|
||||||
|
{
|
||||||
|
// we have a better guess - update the result
|
||||||
|
min_errors = errors;
|
||||||
|
|
||||||
|
if (errors == 0)
|
||||||
|
{
|
||||||
|
break; // Found a perfect answer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send messages from bits to check nodes
|
||||||
|
for (int m = 0; m < FTX_LDPC_M; ++m)
|
||||||
|
{
|
||||||
|
for (int n_idx = 0; n_idx < kFTX_LDPC_Num_rows[m]; ++n_idx)
|
||||||
|
{
|
||||||
|
int n = kFTX_LDPC_Nm[m][n_idx] - 1;
|
||||||
|
// for each (n, m)
|
||||||
|
float Tnm = codeword[n];
|
||||||
|
for (int m_idx = 0; m_idx < 3; ++m_idx)
|
||||||
|
{
|
||||||
|
if ((kFTX_LDPC_Mn[n][m_idx] - 1) != m)
|
||||||
|
{
|
||||||
|
Tnm += tov[n][m_idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toc[m][n_idx] = fast_tanh(-Tnm / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send messages from check nodes to variable nodes
|
||||||
|
for (int n = 0; n < FTX_LDPC_N; ++n)
|
||||||
|
{
|
||||||
|
for (int m_idx = 0; m_idx < 3; ++m_idx)
|
||||||
|
{
|
||||||
|
int m = kFTX_LDPC_Mn[n][m_idx] - 1;
|
||||||
|
// for each (n, m)
|
||||||
|
float Tmn = 1.0f;
|
||||||
|
for (int n_idx = 0; n_idx < kFTX_LDPC_Num_rows[m]; ++n_idx)
|
||||||
|
{
|
||||||
|
if ((kFTX_LDPC_Nm[m][n_idx] - 1) != n)
|
||||||
|
{
|
||||||
|
Tmn *= toc[m][n_idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tov[n][m_idx] = -2 * fast_atanh(Tmn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*ok = min_errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ideas for approximating tanh/atanh:
|
||||||
|
// * https://varietyofsound.wordpress.com/2011/02/14/efficient-tanh-computation-using-lamberts-continued-fraction/
|
||||||
|
// * http://functions.wolfram.com/ElementaryFunctions/ArcTanh/10/0001/
|
||||||
|
// * https://mathr.co.uk/blog/2017-09-06_approximating_hyperbolic_tangent.html
|
||||||
|
// * https://math.stackexchange.com/a/446411
|
||||||
|
|
||||||
|
static float fast_tanh(float x)
|
||||||
|
{
|
||||||
|
if (x < -4.97f)
|
||||||
|
{
|
||||||
|
return -1.0f;
|
||||||
|
}
|
||||||
|
if (x > 4.97f)
|
||||||
|
{
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
float x2 = x * x;
|
||||||
|
// float a = x * (135135.0f + x2 * (17325.0f + x2 * (378.0f + x2)));
|
||||||
|
// float b = 135135.0f + x2 * (62370.0f + x2 * (3150.0f + x2 * 28.0f));
|
||||||
|
// float a = x * (10395.0f + x2 * (1260.0f + x2 * 21.0f));
|
||||||
|
// float b = 10395.0f + x2 * (4725.0f + x2 * (210.0f + x2));
|
||||||
|
float a = x * (945.0f + x2 * (105.0f + x2));
|
||||||
|
float b = 945.0f + x2 * (420.0f + x2 * 15.0f);
|
||||||
|
return a / b;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float fast_atanh(float x)
|
||||||
|
{
|
||||||
|
float x2 = x * x;
|
||||||
|
// float a = x * (-15015.0f + x2 * (19250.0f + x2 * (-5943.0f + x2 * 256.0f)));
|
||||||
|
// float b = (-15015.0f + x2 * (24255.0f + x2 * (-11025.0f + x2 * 1225.0f)));
|
||||||
|
// float a = x * (-1155.0f + x2 * (1190.0f + x2 * -231.0f));
|
||||||
|
// float b = (-1155.0f + x2 * (1575.0f + x2 * (-525.0f + x2 * 25.0f)));
|
||||||
|
float a = x * (945.0f + x2 * (-735.0f + x2 * 64.0f));
|
||||||
|
float b = (945.0f + x2 * (-1050.0f + x2 * 225.0f));
|
||||||
|
return a / b;
|
||||||
|
}
|
||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#ifndef _INCLUDE_LDPC_H_
|
||||||
|
#define _INCLUDE_LDPC_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// codeword is 174 log-likelihoods.
|
||||||
|
// plain is a return value, 174 ints, to be 0 or 1.
|
||||||
|
// iters is how hard to try.
|
||||||
|
// ok == 87 means success.
|
||||||
|
void ldpc_decode(float codeword[], int max_iters, uint8_t plain[], int* ok);
|
||||||
|
|
||||||
|
void bp_decode(float codeword[], int max_iters, uint8_t plain[], int* ok);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_LDPC_H_
|
||||||
Vendored
+1156
File diff suppressed because it is too large
Load Diff
Vendored
+160
@@ -0,0 +1,160 @@
|
|||||||
|
#ifndef _INCLUDE_MESSAGE_H_
|
||||||
|
#define _INCLUDE_MESSAGE_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define FTX_PAYLOAD_LENGTH_BYTES 10 ///< number of bytes to hold 77 bits of FTx payload data
|
||||||
|
#define FTX_MAX_MESSAGE_LENGTH 35 ///< max message length = callsign[13] + space + callsign[13] + space + report[6] + terminator
|
||||||
|
#define FTX_MAX_MESSAGE_FIELDS 3 // may need to get longer for multi-part messages (DXpedition, contest etc.)
|
||||||
|
|
||||||
|
/// Structure that holds the decoded message
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint8_t payload[FTX_PAYLOAD_LENGTH_BYTES];
|
||||||
|
uint16_t hash; ///< Hash value to be used in hash table and quick checking for duplicates
|
||||||
|
} ftx_message_t;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------------
|
||||||
|
// i3.n3 Example message Bits Total Purpose
|
||||||
|
// ----------------------------------------------------------------------------------
|
||||||
|
// 0.0 FREE TEXT MSG 71 71 Free text
|
||||||
|
// 0.1 K1ABC RR73; W9XYZ <KH1/KH7Z> -12 28 28 10 5 71 DXpedition Mode
|
||||||
|
// 0.2 PA3XYZ/P R 590003 IO91NP 28 1 1 3 12 25 70 EU VHF contest
|
||||||
|
// 0.3 WA9XYZ KA1ABC R 16A EMA 28 28 1 4 3 7 71 ARRL Field Day
|
||||||
|
// 0.4 WA9XYZ KA1ABC R 32A EMA 28 28 1 4 3 7 71 ARRL Field Day
|
||||||
|
// 0.5 123456789ABCDEF012 71 71 Telemetry (18 hex)
|
||||||
|
// 0.6 K1ABC RR73; CQ W9XYZ EN37 28 28 15 71 Contesting
|
||||||
|
// 0.7 ... tbd
|
||||||
|
// 1 WA9XYZ/R KA1ABC/R R FN42 28 1 28 1 1 15 74 Standard msg
|
||||||
|
// 2 PA3XYZ/P GM4ABC/P R JO22 28 1 28 1 1 15 74 EU VHF contest
|
||||||
|
// 3 TU; W9XYZ K1ABC R 579 MA 1 28 28 1 3 13 74 ARRL RTTY Roundup
|
||||||
|
// 4 <WA9XYZ> PJ4/KA1ABC RR73 12 58 1 2 1 74 Nonstandard calls
|
||||||
|
// 5 TU; W9XYZ K1ABC R-07 FN 1 28 28 1 7 9 74 WWROF contest ?
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
FTX_MESSAGE_TYPE_FREE_TEXT, // 0.0 FREE TEXT MSG 71 71 Free text
|
||||||
|
FTX_MESSAGE_TYPE_DXPEDITION, // 0.1 K1ABC RR73; W9XYZ <KH1/KH7Z> -12 28 28 10 5 71 DXpedition Mode
|
||||||
|
FTX_MESSAGE_TYPE_EU_VHF, // 0.2 PA3XYZ/P R 590003 IO91NP 28 1 1 3 12 25 70 EU VHF contest
|
||||||
|
FTX_MESSAGE_TYPE_ARRL_FD, // 0.3 WA9XYZ KA1ABC R 16A EMA 28 28 1 4 3 7 71 ARRL Field Day
|
||||||
|
// 0.4 WA9XYZ KA1ABC R 32A EMA 28 28 1 4 3 7 71 ARRL Field Day
|
||||||
|
FTX_MESSAGE_TYPE_TELEMETRY, // 0.5 0123456789abcdef01 71 71 Telemetry (18 hex)
|
||||||
|
FTX_MESSAGE_TYPE_CONTESTING, // 0.6 K1ABC RR73; CQ W9XYZ EN37 28 28 15 71 Contesting
|
||||||
|
FTX_MESSAGE_TYPE_STANDARD, // 1 WA9XYZ/R KA1ABC/R R FN42 28 1 28 1 1 15 74 Standard msg
|
||||||
|
// 2 PA3XYZ/P GM4ABC/P R JO22 28 1 28 1 1 15 74 EU VHF contest
|
||||||
|
FTX_MESSAGE_TYPE_ARRL_RTTY, // 3 TU; W9XYZ K1ABC R 579 MA 1 28 28 1 3 13 74 ARRL RTTY Roundup
|
||||||
|
FTX_MESSAGE_TYPE_NONSTD_CALL, // 4 <WA9XYZ> PJ4/KA1ABC RR73 12 58 1 2 1 74 Nonstandard calls
|
||||||
|
FTX_MESSAGE_TYPE_WWROF, // 5 TU; W9XYZ K1ABC R-07 FN 1 28 28 1 7 9 74 WWROF contest ?
|
||||||
|
FTX_MESSAGE_TYPE_UNKNOWN // Unknown or invalid type
|
||||||
|
} ftx_message_type_t;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
FTX_CALLSIGN_HASH_22_BITS,
|
||||||
|
FTX_CALLSIGN_HASH_12_BITS,
|
||||||
|
FTX_CALLSIGN_HASH_10_BITS
|
||||||
|
} ftx_callsign_hash_type_t;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
/// Called when a callsign is looked up by its 22/12/10 bit hash code
|
||||||
|
bool (*lookup_hash)(ftx_callsign_hash_type_t hash_type, uint32_t hash, char* callsign);
|
||||||
|
/// Called when a callsign should hashed and stored (by its 22, 12 and 10 bit hash codes)
|
||||||
|
void (*save_hash)(const char* callsign, uint32_t n22);
|
||||||
|
} ftx_callsign_hash_interface_t;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
FTX_MESSAGE_RC_OK,
|
||||||
|
FTX_MESSAGE_RC_ERROR_CALLSIGN1,
|
||||||
|
FTX_MESSAGE_RC_ERROR_CALLSIGN2,
|
||||||
|
FTX_MESSAGE_RC_ERROR_SUFFIX,
|
||||||
|
FTX_MESSAGE_RC_ERROR_GRID,
|
||||||
|
FTX_MESSAGE_RC_ERROR_TYPE
|
||||||
|
} ftx_message_rc_t;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
FTX_FIELD_UNKNOWN,
|
||||||
|
FTX_FIELD_NONE,
|
||||||
|
FTX_FIELD_TOKEN, // RRR, RR73, 73, DE, QRZ, CQ, ...
|
||||||
|
FTX_FIELD_TOKEN_WITH_ARG, // CQ nnn, CQ abcd
|
||||||
|
FTX_FIELD_CALL,
|
||||||
|
FTX_FIELD_GRID,
|
||||||
|
FTX_FIELD_RST
|
||||||
|
} ftx_field_t;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
// parallel arrays:
|
||||||
|
// e.g. "CQ POTA W9XYZ AB12" generates
|
||||||
|
// types { FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_CALL, FTX_FIELD_CALL_GRID" }
|
||||||
|
// offsets { 0, 8, 14 }
|
||||||
|
// Both arrays end where offsets[i] < 0
|
||||||
|
ftx_field_t types[FTX_MAX_MESSAGE_FIELDS];
|
||||||
|
int16_t offsets[FTX_MAX_MESSAGE_FIELDS];
|
||||||
|
} ftx_message_offsets_t;
|
||||||
|
|
||||||
|
// Callsign types and sizes:
|
||||||
|
// * Std. call (basecall) - 1-2 letter/digit prefix (at least one letter), 1 digit area code, 1-3 letter suffix,
|
||||||
|
// total 3-6 chars (exception: 7 character calls with prefixes 3DA0- and 3XA..3XZ-)
|
||||||
|
// * Ext. std. call - basecall followed by /R or /P
|
||||||
|
// * Nonstd. call - all the rest, limited to 3-11 characters either alphanumeric or stroke (/)
|
||||||
|
// In case a call is looked up from its hash value, the call is enclosed in angular brackets (<CA0LL>).
|
||||||
|
|
||||||
|
void ftx_message_init(ftx_message_t* msg);
|
||||||
|
|
||||||
|
uint8_t ftx_message_get_i3(const ftx_message_t* msg);
|
||||||
|
uint8_t ftx_message_get_n3(const ftx_message_t* msg);
|
||||||
|
ftx_message_type_t ftx_message_get_type(const ftx_message_t* msg);
|
||||||
|
|
||||||
|
// bool ftx_message_check_recipient(const ftx_message_t* msg, const char* callsign);
|
||||||
|
|
||||||
|
/// Pack (encode) a callsign in the standard way, and return the numeric representation.
|
||||||
|
/// Returns -1 if \a callsign cannot be encoded in the standard way.
|
||||||
|
/// This function can be used to decide whether to call ftx_message_encode_std() or ftx_message_decode_nonstd().
|
||||||
|
/// Alternatively, ftx_message_encode_std() itself fails when one of the callsigns cannot be packed this way.
|
||||||
|
int32_t pack_basecall(const char* callsign, int length);
|
||||||
|
|
||||||
|
/// Pack (encode) a text message, guessing which message type to use and falling back on failure:
|
||||||
|
/// if there are 3 or fewer tokens, try ftx_message_encode_std first,
|
||||||
|
/// then ftx_message_encode_nonstd if that fails because of a non-standard callsign;
|
||||||
|
/// otherwise fall back to ftx_message_encode_free.
|
||||||
|
/// If you already know which type to use, you can call one of those functions directly.
|
||||||
|
ftx_message_rc_t ftx_message_encode(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* message_text);
|
||||||
|
|
||||||
|
/// Pack Type 1 (Standard 77-bit message) or Type 2 (ditto, with a "/P" call) message
|
||||||
|
/// Rules of callsign validity:
|
||||||
|
/// - call_to can be 'DE', 'CQ', 'QRZ', 'CQ_nnn' (three digits), or 'CQ_abcd' (four letters)
|
||||||
|
/// - nonstandard calls within <> brackets are allowed, if they don't contain '/'
|
||||||
|
ftx_message_rc_t ftx_message_encode_std(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* call_to, const char* call_de, const char* extra);
|
||||||
|
|
||||||
|
/// Pack Type 4 (One nonstandard call and one hashed call) message
|
||||||
|
ftx_message_rc_t ftx_message_encode_nonstd(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* call_to, const char* call_de, const char* extra);
|
||||||
|
|
||||||
|
/// Pack plain text, up to 13 characters
|
||||||
|
ftx_message_rc_t ftx_message_encode_free(ftx_message_t* msg, const char* text);
|
||||||
|
ftx_message_rc_t ftx_message_encode_telemetry(ftx_message_t* msg, const uint8_t* telemetry);
|
||||||
|
|
||||||
|
ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* message, ftx_message_offsets_t* offsets);
|
||||||
|
ftx_message_rc_t ftx_message_decode_std(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra, ftx_field_t field_types[FTX_MAX_MESSAGE_FIELDS]);
|
||||||
|
ftx_message_rc_t ftx_message_decode_nonstd(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra, ftx_field_t field_types[FTX_MAX_MESSAGE_FIELDS]);
|
||||||
|
void ftx_message_decode_free(const ftx_message_t* msg, char* text);
|
||||||
|
void ftx_message_decode_telemetry_hex(const ftx_message_t* msg, char* telemetry_hex);
|
||||||
|
void ftx_message_decode_telemetry(const ftx_message_t* msg, uint8_t* telemetry);
|
||||||
|
|
||||||
|
#ifdef FTX_DEBUG_PRINT
|
||||||
|
void ftx_message_print(ftx_message_t* msg);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_MESSAGE_H_
|
||||||
Vendored
+303
@@ -0,0 +1,303 @@
|
|||||||
|
#include "text.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
const char* trim_front(const char* str, char to_trim)
|
||||||
|
{
|
||||||
|
// Skip leading to_trim characters
|
||||||
|
while (*str == to_trim)
|
||||||
|
{
|
||||||
|
str++;
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
void trim_back(char* str, char to_trim)
|
||||||
|
{
|
||||||
|
// Skip trailing to_trim characters by replacing them with '\0' characters
|
||||||
|
int idx = strlen(str) - 1;
|
||||||
|
while (idx >= 0 && str[idx] == to_trim)
|
||||||
|
{
|
||||||
|
str[idx--] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char* trim(char* str)
|
||||||
|
{
|
||||||
|
str = (char*)trim_front(str, ' ');
|
||||||
|
trim_back(str, ' ');
|
||||||
|
// return a pointer to the first non-whitespace character
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* trim_brackets(char* str)
|
||||||
|
{
|
||||||
|
str = (char*)trim_front(str, '<');
|
||||||
|
trim_back(str, '>');
|
||||||
|
// return a pointer to the first non-whitespace character
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
void trim_copy(char* trimmed, const char* str)
|
||||||
|
{
|
||||||
|
str = (char*)trim_front(str, ' ');
|
||||||
|
int len = strlen(str) - 1;
|
||||||
|
while (len >= 0 && str[len] == ' ')
|
||||||
|
{
|
||||||
|
len--;
|
||||||
|
}
|
||||||
|
strncpy(trimmed, str, len + 1);
|
||||||
|
trimmed[len + 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
char to_upper(char c)
|
||||||
|
{
|
||||||
|
return (c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_digit(char c)
|
||||||
|
{
|
||||||
|
return (c >= '0') && (c <= '9');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_letter(char c)
|
||||||
|
{
|
||||||
|
return ((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z'));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_space(char c)
|
||||||
|
{
|
||||||
|
return (c == ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool in_range(char c, char min, char max)
|
||||||
|
{
|
||||||
|
return (c >= min) && (c <= max);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool starts_with(const char* string, const char* prefix)
|
||||||
|
{
|
||||||
|
return 0 == memcmp(string, prefix, strlen(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ends_with(const char* string, const char* suffix)
|
||||||
|
{
|
||||||
|
int pos = strlen(string) - strlen(suffix);
|
||||||
|
if (pos >= 0)
|
||||||
|
{
|
||||||
|
return 0 == memcmp(string + pos, suffix, strlen(suffix));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool equals(const char* string1, const char* string2)
|
||||||
|
{
|
||||||
|
return 0 == strcmp(string1, string2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text message formatting:
|
||||||
|
// - replaces lowercase letters with uppercase
|
||||||
|
// - merges consecutive spaces into single space
|
||||||
|
void fmtmsg(char* msg_out, const char* msg_in)
|
||||||
|
{
|
||||||
|
char c;
|
||||||
|
char last_out = 0;
|
||||||
|
while ((c = *msg_in))
|
||||||
|
{
|
||||||
|
if (c != ' ' || last_out != ' ')
|
||||||
|
{
|
||||||
|
last_out = to_upper(c);
|
||||||
|
*msg_out = last_out;
|
||||||
|
++msg_out;
|
||||||
|
}
|
||||||
|
++msg_in;
|
||||||
|
}
|
||||||
|
*msg_out = 0; // Add zero termination
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns pointer to the null terminator within the given string (destination)
|
||||||
|
char* append_string(char* string, const char* token)
|
||||||
|
{
|
||||||
|
while (*token != '\0')
|
||||||
|
{
|
||||||
|
*string = *token;
|
||||||
|
string++;
|
||||||
|
token++;
|
||||||
|
}
|
||||||
|
*string = '\0';
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* copy_token(char* token, int length, const char* string)
|
||||||
|
{
|
||||||
|
// Copy characters until a whitespace character or the end of string
|
||||||
|
while (*string != ' ' && *string != '\0')
|
||||||
|
{
|
||||||
|
if (length > 1)
|
||||||
|
{
|
||||||
|
*token = *string;
|
||||||
|
token++;
|
||||||
|
length--;
|
||||||
|
}
|
||||||
|
string++;
|
||||||
|
}
|
||||||
|
// Fill up the rest of token with \0 terminators
|
||||||
|
while (length > 0)
|
||||||
|
{
|
||||||
|
*token = '\0';
|
||||||
|
token++;
|
||||||
|
length--;
|
||||||
|
}
|
||||||
|
// Skip whitespace characters
|
||||||
|
while (*string == ' ')
|
||||||
|
{
|
||||||
|
string++;
|
||||||
|
}
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a 2 digit integer from string
|
||||||
|
int dd_to_int(const char* str, int length)
|
||||||
|
{
|
||||||
|
int result = 0;
|
||||||
|
bool negative;
|
||||||
|
int i;
|
||||||
|
if (str[0] == '-')
|
||||||
|
{
|
||||||
|
negative = true;
|
||||||
|
i = 1; // Consume the - sign
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
negative = false;
|
||||||
|
i = (str[0] == '+') ? 1 : 0; // Consume a + sign if found
|
||||||
|
}
|
||||||
|
|
||||||
|
while (i < length)
|
||||||
|
{
|
||||||
|
if (str[i] == 0)
|
||||||
|
break;
|
||||||
|
if (!is_digit(str[i]))
|
||||||
|
break;
|
||||||
|
result *= 10;
|
||||||
|
result += (str[i] - '0');
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return negative ? -result : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a 2 digit integer to string
|
||||||
|
void int_to_dd(char* str, int value, int width, bool full_sign)
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
*str = '-';
|
||||||
|
++str;
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
else if (full_sign)
|
||||||
|
{
|
||||||
|
*str = '+';
|
||||||
|
++str;
|
||||||
|
}
|
||||||
|
|
||||||
|
int divisor = 1;
|
||||||
|
for (int i = 0; i < width - 1; ++i)
|
||||||
|
{
|
||||||
|
divisor *= 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (divisor >= 1)
|
||||||
|
{
|
||||||
|
int digit = value / divisor;
|
||||||
|
|
||||||
|
*str = '0' + digit;
|
||||||
|
++str;
|
||||||
|
|
||||||
|
value -= digit * divisor;
|
||||||
|
divisor /= 10;
|
||||||
|
}
|
||||||
|
*str = 0; // Add zero terminator
|
||||||
|
}
|
||||||
|
|
||||||
|
char charn(int c, ft8_char_table_e table)
|
||||||
|
{
|
||||||
|
if ((table != FT8_CHAR_TABLE_ALPHANUM) && (table != FT8_CHAR_TABLE_NUMERIC))
|
||||||
|
{
|
||||||
|
if (c == 0)
|
||||||
|
return ' ';
|
||||||
|
c -= 1;
|
||||||
|
}
|
||||||
|
if (table != FT8_CHAR_TABLE_LETTERS_SPACE)
|
||||||
|
{
|
||||||
|
if (c < 10)
|
||||||
|
return '0' + c;
|
||||||
|
c -= 10;
|
||||||
|
}
|
||||||
|
if (table != FT8_CHAR_TABLE_NUMERIC)
|
||||||
|
{
|
||||||
|
if (c < 26)
|
||||||
|
return 'A' + c;
|
||||||
|
c -= 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table == FT8_CHAR_TABLE_FULL)
|
||||||
|
{
|
||||||
|
if (c < 5)
|
||||||
|
return "+-./?"[c];
|
||||||
|
}
|
||||||
|
else if (table == FT8_CHAR_TABLE_ALPHANUM_SPACE_SLASH)
|
||||||
|
{
|
||||||
|
if (c == 0)
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '_'; // unknown character, should never get here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert character to its index (charn in reverse) according to a table
|
||||||
|
int nchar(char c, ft8_char_table_e table)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
if ((table != FT8_CHAR_TABLE_ALPHANUM) && (table != FT8_CHAR_TABLE_NUMERIC))
|
||||||
|
{
|
||||||
|
if (c == ' ')
|
||||||
|
return n + 0;
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
if (table != FT8_CHAR_TABLE_LETTERS_SPACE)
|
||||||
|
{
|
||||||
|
if (c >= '0' && c <= '9')
|
||||||
|
return n + (c - '0');
|
||||||
|
n += 10;
|
||||||
|
}
|
||||||
|
if (table != FT8_CHAR_TABLE_NUMERIC)
|
||||||
|
{
|
||||||
|
if (c >= 'A' && c <= 'Z')
|
||||||
|
return n + (c - 'A');
|
||||||
|
n += 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table == FT8_CHAR_TABLE_FULL)
|
||||||
|
{
|
||||||
|
if (c == '+')
|
||||||
|
return n + 0;
|
||||||
|
if (c == '-')
|
||||||
|
return n + 1;
|
||||||
|
if (c == '.')
|
||||||
|
return n + 2;
|
||||||
|
if (c == '/')
|
||||||
|
return n + 3;
|
||||||
|
if (c == '?')
|
||||||
|
return n + 4;
|
||||||
|
}
|
||||||
|
else if (table == FT8_CHAR_TABLE_ALPHANUM_SPACE_SLASH)
|
||||||
|
{
|
||||||
|
if (c == '/')
|
||||||
|
return n + 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Character not found
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
Vendored
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#ifndef _INCLUDE_TEXT_H_
|
||||||
|
#define _INCLUDE_TEXT_H_
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Utility functions for characters and strings
|
||||||
|
|
||||||
|
const char* trim_front(const char* str, char to_trim);
|
||||||
|
void trim_back(char* str, char to_trim);
|
||||||
|
|
||||||
|
/// In-place whitespace trim from front and back:
|
||||||
|
/// 1) trims the back by changing whitespaces to '\0'
|
||||||
|
/// 2) trims the front by skipping whitespaces
|
||||||
|
/// @return trimmed string (pointer to first non-whitespace character)
|
||||||
|
char* trim(char* str);
|
||||||
|
|
||||||
|
/// Trim whitespace from start and end of string
|
||||||
|
void trim_copy(char* trimmed, const char* str);
|
||||||
|
|
||||||
|
/// In-place trim of <> characters from front and back:
|
||||||
|
/// 1) trims the back by changing > to '\0'
|
||||||
|
/// 2) trims the front by skipping <
|
||||||
|
/// @return trimmed string (pointer to first non-whitespace character)
|
||||||
|
char* trim_brackets(char* str);
|
||||||
|
|
||||||
|
char to_upper(char c);
|
||||||
|
bool is_digit(char c);
|
||||||
|
bool is_letter(char c);
|
||||||
|
bool is_space(char c);
|
||||||
|
bool in_range(char c, char min, char max);
|
||||||
|
bool starts_with(const char* string, const char* prefix);
|
||||||
|
bool ends_with(const char* string, const char* suffix);
|
||||||
|
bool equals(const char* string1, const char* string2);
|
||||||
|
|
||||||
|
// Text message formatting:
|
||||||
|
// - replaces lowercase letters with uppercase
|
||||||
|
// - merges consecutive spaces into single space
|
||||||
|
void fmtmsg(char* msg_out, const char* msg_in);
|
||||||
|
|
||||||
|
/// Extract and copy a space-delimited token from a string.
|
||||||
|
/// When the last token has been extracted, the return value points to the terminating zero character.
|
||||||
|
/// @param[out] token Buffer to receive the extracted token
|
||||||
|
/// @param[in] length Length of the token buffer (number of characters)
|
||||||
|
/// @param[in] string Pointer to the string
|
||||||
|
/// @return Pointer to the next token (can be passed to copy_token to extract the next token)
|
||||||
|
const char* copy_token(char* token, int length, const char* string);
|
||||||
|
|
||||||
|
char* append_string(char* string, const char* token);
|
||||||
|
|
||||||
|
// Parse a 2 digit integer from string
|
||||||
|
int dd_to_int(const char* str, int length);
|
||||||
|
|
||||||
|
// Convert a 2 digit integer to string
|
||||||
|
void int_to_dd(char* str, int value, int width, bool full_sign);
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
FT8_CHAR_TABLE_FULL, // table[42] " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?"
|
||||||
|
FT8_CHAR_TABLE_ALPHANUM_SPACE_SLASH, // table[38] " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"
|
||||||
|
FT8_CHAR_TABLE_ALPHANUM_SPACE, // table[37] " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
FT8_CHAR_TABLE_LETTERS_SPACE, // table[27] " ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
FT8_CHAR_TABLE_ALPHANUM, // table[36] "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
FT8_CHAR_TABLE_NUMERIC, // table[10] "0123456789"
|
||||||
|
} ft8_char_table_e;
|
||||||
|
|
||||||
|
/// Convert integer index to ASCII character according to one of character tables
|
||||||
|
char charn(int c, ft8_char_table_e table);
|
||||||
|
|
||||||
|
/// Look up the index of an ASCII character in one of character tables
|
||||||
|
int nchar(char c, ft8_char_table_e table);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_TEXT_H_
|
||||||
Vendored
+286
@@ -0,0 +1,286 @@
|
|||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include "ft8/text.h"
|
||||||
|
#include "ft8/encode.h"
|
||||||
|
#include "ft8/constants.h"
|
||||||
|
|
||||||
|
#include "fft/kiss_fftr.h"
|
||||||
|
#include "common/common.h"
|
||||||
|
#include "ft8/message.h"
|
||||||
|
|
||||||
|
#define LOG_LEVEL LOG_INFO
|
||||||
|
#include "ft8/debug.h"
|
||||||
|
|
||||||
|
// void convert_8bit_to_6bit(uint8_t* dst, const uint8_t* src, int nBits)
|
||||||
|
// {
|
||||||
|
// // Zero-fill the destination array as we will only be setting bits later
|
||||||
|
// for (int j = 0; j < (nBits + 5) / 6; ++j)
|
||||||
|
// {
|
||||||
|
// dst[j] = 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Set the relevant bits
|
||||||
|
// uint8_t mask_src = (1 << 7);
|
||||||
|
// uint8_t mask_dst = (1 << 5);
|
||||||
|
// for (int i = 0, j = 0; nBits > 0; --nBits)
|
||||||
|
// {
|
||||||
|
// if (src[i] & mask_src)
|
||||||
|
// {
|
||||||
|
// dst[j] |= mask_dst;
|
||||||
|
// }
|
||||||
|
// mask_src >>= 1;
|
||||||
|
// if (mask_src == 0)
|
||||||
|
// {
|
||||||
|
// mask_src = (1 << 7);
|
||||||
|
// ++i;
|
||||||
|
// }
|
||||||
|
// mask_dst >>= 1;
|
||||||
|
// if (mask_dst == 0)
|
||||||
|
// {
|
||||||
|
// mask_dst = (1 << 5);
|
||||||
|
// ++j;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/*
|
||||||
|
bool test1() {
|
||||||
|
//const char *msg = "CQ DL7ACA JO40"; // 62, 32, 32, 49, 37, 27, 59, 2, 30, 19, 49, 16
|
||||||
|
const char *msg = "VA3UG F1HMR 73"; // 52, 54, 60, 12, 55, 54, 7, 19, 2, 23, 59, 16
|
||||||
|
//const char *msg = "RA3Y VE3NLS 73"; // 46, 6, 32, 22, 55, 20, 11, 32, 53, 23, 59, 16
|
||||||
|
uint8_t a72[9];
|
||||||
|
|
||||||
|
int rc = packmsg(msg, a72);
|
||||||
|
if (rc < 0) return false;
|
||||||
|
|
||||||
|
LOG(LOG_INFO, "8-bit packed: ");
|
||||||
|
for (int i = 0; i < 9; ++i) {
|
||||||
|
LOG(LOG_INFO, "%02x ", a72[i]);
|
||||||
|
}
|
||||||
|
LOG(LOG_INFO, "\n");
|
||||||
|
|
||||||
|
uint8_t a72_6bit[12];
|
||||||
|
convert_8bit_to_6bit(a72_6bit, a72, 72);
|
||||||
|
LOG(LOG_INFO, "6-bit packed: ");
|
||||||
|
for (int i = 0; i < 12; ++i) {
|
||||||
|
LOG(LOG_INFO, "%d ", a72_6bit[i]);
|
||||||
|
}
|
||||||
|
LOG(LOG_INFO, "\n");
|
||||||
|
|
||||||
|
char msg_out_raw[14];
|
||||||
|
unpack(a72, msg_out_raw);
|
||||||
|
|
||||||
|
char msg_out[14];
|
||||||
|
fmtmsg(msg_out, msg_out_raw);
|
||||||
|
LOG(LOG_INFO, "msg_out = [%s]\n", msg_out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void test2() {
|
||||||
|
uint8_t test_in[11] = { 0xF1, 0x02, 0x03, 0x04, 0x05, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xFF };
|
||||||
|
uint8_t test_out[22];
|
||||||
|
|
||||||
|
encode174(test_in, test_out);
|
||||||
|
|
||||||
|
for (int j = 0; j < 22; ++j) {
|
||||||
|
LOG(LOG_INFO, "%02x ", test_out[j]);
|
||||||
|
}
|
||||||
|
LOG(LOG_INFO, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void test3() {
|
||||||
|
uint8_t test_in2[10] = { 0x11, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x10, 0x04, 0x01, 0x00 };
|
||||||
|
uint16_t crc1 = ftx_compute_crc(test_in2, 76); // Calculate CRC of 76 bits only
|
||||||
|
LOG(LOG_INFO, "CRC: %04x\n", crc1); // should be 0x0708
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define CHECK(condition) \
|
||||||
|
if (!(condition)) \
|
||||||
|
{ \
|
||||||
|
printf("FAIL! Condition \'" #condition "' failed\n\n"); \
|
||||||
|
return; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CHECK_EQ_VAL(this, that) \
|
||||||
|
if ((this) != (that)) \
|
||||||
|
{ \
|
||||||
|
printf("FAIL! Expected " #this " (%d) == " #that " (%d)\n\n", \
|
||||||
|
(this), (that)); \
|
||||||
|
return; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define TEST_END printf("Test OK\n\n")
|
||||||
|
|
||||||
|
#define CALLSIGN_HASHTABLE_SIZE 256
|
||||||
|
|
||||||
|
struct
|
||||||
|
{
|
||||||
|
char callsign[12];
|
||||||
|
uint32_t hash;
|
||||||
|
} callsign_hashtable[CALLSIGN_HASHTABLE_SIZE];
|
||||||
|
|
||||||
|
void hashtable_init(void)
|
||||||
|
{
|
||||||
|
// for (int idx = 0; idx < CALLSIGN_HASHTABLE_SIZE; ++idx)
|
||||||
|
// {
|
||||||
|
// callsign_hashtable[idx]->callsign[0] = '\0';
|
||||||
|
// }
|
||||||
|
memset(callsign_hashtable, 0, sizeof(callsign_hashtable));
|
||||||
|
}
|
||||||
|
|
||||||
|
void hashtable_add(const char* callsign, uint32_t hash)
|
||||||
|
{
|
||||||
|
int idx_hash = (hash * 23) % CALLSIGN_HASHTABLE_SIZE;
|
||||||
|
while (callsign_hashtable[idx_hash].callsign[0] != '\0')
|
||||||
|
{
|
||||||
|
if ((callsign_hashtable[idx_hash].hash == hash) && (0 == strcmp(callsign_hashtable[idx_hash].callsign, callsign)))
|
||||||
|
{
|
||||||
|
LOG(LOG_DEBUG, "Found a duplicate [%s]\n", callsign);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG(LOG_DEBUG, "Hash table clash!\n");
|
||||||
|
// Move on to check the next entry in hash table
|
||||||
|
idx_hash = (idx_hash + 1) % CALLSIGN_HASHTABLE_SIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strncpy(callsign_hashtable[idx_hash].callsign, callsign, 11);
|
||||||
|
callsign_hashtable[idx_hash].callsign[11] = '\0';
|
||||||
|
callsign_hashtable[idx_hash].hash = hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hashtable_lookup(ftx_callsign_hash_type_t hash_type, uint32_t hash, char* callsign)
|
||||||
|
{
|
||||||
|
uint32_t hash_mask = (hash_type == FTX_CALLSIGN_HASH_10_BITS) ? 0x3FFu : (hash_type == FTX_CALLSIGN_HASH_12_BITS ? 0xFFFu : 0x3FFFFFu);
|
||||||
|
int idx_hash = (hash * 23) % CALLSIGN_HASHTABLE_SIZE;
|
||||||
|
while (callsign_hashtable[idx_hash].callsign[0] != '\0')
|
||||||
|
{
|
||||||
|
if ((callsign_hashtable[idx_hash].hash & hash_mask) == hash)
|
||||||
|
{
|
||||||
|
strcpy(callsign, callsign_hashtable[idx_hash].callsign);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Move on to check the next entry in hash table
|
||||||
|
idx_hash = (idx_hash + 1) % CALLSIGN_HASHTABLE_SIZE;
|
||||||
|
}
|
||||||
|
callsign[0] = '\0';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ftx_callsign_hash_interface_t hash_if = {
|
||||||
|
.lookup_hash = hashtable_lookup,
|
||||||
|
.save_hash = hashtable_add
|
||||||
|
};
|
||||||
|
|
||||||
|
void test_std_msg(const char* call_to_tx, ftx_field_t to_field, const char* call_de_tx, ftx_field_t de_field, const char* extra_tx, ftx_field_t extra_field)
|
||||||
|
{
|
||||||
|
ftx_message_t msg;
|
||||||
|
ftx_message_init(&msg);
|
||||||
|
|
||||||
|
ftx_message_rc_t rc_encode = ftx_message_encode_std(&msg, &hash_if, call_to_tx, call_de_tx, extra_tx);
|
||||||
|
printf("Encoded [%s] [%s] [%s]\n", call_to_tx, call_de_tx, extra_tx);
|
||||||
|
CHECK_EQ_VAL(rc_encode, FTX_MESSAGE_RC_OK);
|
||||||
|
|
||||||
|
char call_to_arr[14];
|
||||||
|
char call_de_arr[14];
|
||||||
|
char extra[14];
|
||||||
|
char *call_to = call_to_arr;
|
||||||
|
char *call_de = call_de_arr;
|
||||||
|
ftx_field_t types[FTX_MAX_MESSAGE_FIELDS];
|
||||||
|
ftx_message_rc_t rc_decode = ftx_message_decode_std(&msg, &hash_if, call_to, call_de, extra, types);
|
||||||
|
CHECK_EQ_VAL(rc_decode, FTX_MESSAGE_RC_OK);
|
||||||
|
printf("Decoded [%s] [%s] [%s]\n", call_to, call_de, extra);
|
||||||
|
call_to = trim_brackets(call_to);
|
||||||
|
call_de = trim_brackets(call_de);
|
||||||
|
CHECK_EQ_VAL(0, strcmp(call_to, call_to_tx));
|
||||||
|
CHECK_EQ_VAL(0, strcmp(call_de, call_de_tx));
|
||||||
|
CHECK_EQ_VAL(0, strcmp(extra, extra_tx));
|
||||||
|
CHECK_EQ_VAL(to_field, types[0]);
|
||||||
|
CHECK_EQ_VAL(de_field, types[1]);
|
||||||
|
CHECK_EQ_VAL(extra_field, types[2]);
|
||||||
|
TEST_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_msg(const char* message_text, ftx_message_type_t expected_type, const char* expected, ftx_callsign_hash_interface_t* hash_if)
|
||||||
|
{
|
||||||
|
printf("Testing [%s]\n", message_text);
|
||||||
|
|
||||||
|
ftx_message_t msg;
|
||||||
|
ftx_message_init(&msg);
|
||||||
|
|
||||||
|
ftx_message_rc_t rc_encode = ftx_message_encode(&msg, hash_if, message_text);
|
||||||
|
CHECK_EQ_VAL(rc_encode, FTX_MESSAGE_RC_OK);
|
||||||
|
CHECK_EQ_VAL(expected_type, ftx_message_get_type(&msg));
|
||||||
|
|
||||||
|
char message_decoded[12 + 12 + 20];
|
||||||
|
ftx_message_offsets_t offsets;
|
||||||
|
ftx_message_rc_t rc_decode = ftx_message_decode(&msg, hash_if, message_decoded, &offsets);
|
||||||
|
CHECK_EQ_VAL(rc_decode, FTX_MESSAGE_RC_OK);
|
||||||
|
printf("Decoded [%s]; offsets %d:%d %d:%d %d:%d\n", message_decoded,
|
||||||
|
offsets.offsets[0], offsets.types[0], offsets.offsets[1], offsets.types[1], offsets.offsets[2], offsets.types[2]);
|
||||||
|
CHECK_EQ_VAL(0, strcmp(expected, message_decoded));
|
||||||
|
// TODO check offsets
|
||||||
|
TEST_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define SIZEOF_ARRAY(x) (sizeof(x) / sizeof((x)[0]))
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
// test1();
|
||||||
|
// test4();
|
||||||
|
const char* callsigns[] = { "YL3JG", "W1A", "W1A/R", "W5AB", "W8ABC", "DE6ABC", "DE6ABC/R", "DE7AB", "DE9A", "3DA0X", "3DA0XYZ", "3DA0XYZ/R", "3XZ0AB", "3XZ0A", "CQ1CQ" };
|
||||||
|
const char* tokens[] = { "CQ", "QRZ", "CQ 123", "CQ 000", "CQ POTA", "CQ SA", "CQ O", "CQ ASD" };
|
||||||
|
const ftx_field_t token_types[] = { FTX_FIELD_TOKEN, FTX_FIELD_TOKEN, FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_TOKEN_WITH_ARG };
|
||||||
|
const char* grids[] = { "KO26", "RR99", "AA00", "RR09", "AA01", "RRR", "RR73", "73", "R+10", "R+05", "R-12", "R-02", "+10", "+05", "-02", "-02", "" };
|
||||||
|
const ftx_field_t grid_types[] = { FTX_FIELD_GRID, FTX_FIELD_GRID, FTX_FIELD_GRID, FTX_FIELD_GRID, FTX_FIELD_GRID, FTX_FIELD_TOKEN, FTX_FIELD_TOKEN, FTX_FIELD_TOKEN, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_NONE };
|
||||||
|
|
||||||
|
for (int idx_grid = 0; idx_grid < SIZEOF_ARRAY(grids); ++idx_grid)
|
||||||
|
{
|
||||||
|
for (int idx_callsign = 0; idx_callsign < SIZEOF_ARRAY(callsigns); ++idx_callsign)
|
||||||
|
{
|
||||||
|
for (int idx_callsign2 = 0; idx_callsign2 < SIZEOF_ARRAY(callsigns); ++idx_callsign2)
|
||||||
|
{
|
||||||
|
test_std_msg(callsigns[idx_callsign], FTX_FIELD_CALL, callsigns[idx_callsign2], FTX_FIELD_CALL, grids[idx_grid], grid_types[idx_grid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int idx_token = 0; idx_token < SIZEOF_ARRAY(tokens); ++idx_token)
|
||||||
|
{
|
||||||
|
for (int idx_callsign2 = 0; idx_callsign2 < SIZEOF_ARRAY(callsigns); ++idx_callsign2)
|
||||||
|
{
|
||||||
|
test_std_msg(tokens[idx_token], token_types[idx_token], callsigns[idx_callsign2], FTX_FIELD_CALL, grids[idx_grid], grid_types[idx_grid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test_msg("CQ K7IHZ DM43", FTX_MESSAGE_TYPE_STANDARD,
|
||||||
|
"CQ K7IHZ DM43", &hash_if);
|
||||||
|
test_msg("CQ EA8/G5LSI", FTX_MESSAGE_TYPE_NONSTD_CALL,
|
||||||
|
"CQ EA8/G5LSI", &hash_if);
|
||||||
|
test_msg("EA8/G5LSI R2RFE RR73", FTX_MESSAGE_TYPE_STANDARD,
|
||||||
|
"<EA8/G5LSI> R2RFE RR73", &hash_if);
|
||||||
|
test_msg("R2RFE/P EA8/G5LSI R+12", FTX_MESSAGE_TYPE_STANDARD,
|
||||||
|
"R2RFE/P <EA8/G5LSI> R+12", &hash_if);
|
||||||
|
test_msg("TNX BOB 73 GL", FTX_MESSAGE_TYPE_FREE_TEXT,
|
||||||
|
"TNX BOB 73 GL", &hash_if); // message with 4 tokens must be free text
|
||||||
|
test_msg("TNX BOB 73", FTX_MESSAGE_TYPE_STANDARD,
|
||||||
|
"<TNX> <BOB> 73", &hash_if); // can't distinguish special callsigns from other tokens
|
||||||
|
test_msg("CQ YL/LB2JK KO16sw", FTX_MESSAGE_TYPE_NONSTD_CALL,
|
||||||
|
"CQ YL/LB2JK", &hash_if); // grid not allowed with nonstandard call
|
||||||
|
test_msg("CQ POTA YL/LB2JK KO16sw", FTX_MESSAGE_TYPE_NONSTD_CALL,
|
||||||
|
"CQ YL/LB2JK", &hash_if); // CQ modifier not allowed with nonstandard call
|
||||||
|
test_msg("CQ JA LB2JK JO59", FTX_MESSAGE_TYPE_STANDARD,
|
||||||
|
"CQ JA LB2JK JO59", &hash_if);
|
||||||
|
test_msg("CQ 123 LB2JK JO59", FTX_MESSAGE_TYPE_STANDARD,
|
||||||
|
"CQ 123 LB2JK JO59", &hash_if);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
110115 6 0.9 1234 ~ GJ0KYZ RK9AX MO05
|
||||||
BIN
Binary file not shown.
+5
@@ -0,0 +1,5 @@
|
|||||||
|
110130 -6 0.7 683 ~ CQ TA6CQ KN70 AS Turkey
|
||||||
|
110130 -16 1.0 989 ~ OH3NIV ZS6S -03
|
||||||
|
110130 -6 0.9 1291 ~ CQ R7IW LN35 EU Russia
|
||||||
|
110130 -4 0.9 2096 ~ CQ DX R6WA LN32 EU Russia
|
||||||
|
110130 -14 1.2 2479 ~ TK4LS YC1MRF 73
|
||||||
BIN
Binary file not shown.
+2
@@ -0,0 +1,2 @@
|
|||||||
|
110145 -4 1.0 322 ~ <...> RY8CAA
|
||||||
|
110145 7 1.0 1234 ~ GJ0KYZ RK9AX MO05
|
||||||
BIN
Binary file not shown.
+5
@@ -0,0 +1,5 @@
|
|||||||
|
110200 -4 0.7 683 ~ CQ TA6CQ KN70 AS Turkey
|
||||||
|
110200 -16 1.0 990 ~ OH3NIV ZS6S RR73
|
||||||
|
110200 -17 0.6 1031 ~ CQ LZ1JZ KN22 Bulgaria
|
||||||
|
110200 -12 0.9 1292 ~ CQ R7IW LN35 EU Russia
|
||||||
|
110200 -7 0.9 2097 ~ CQ DX R6WA LN32 EU Russia
|
||||||
BIN
Binary file not shown.
+4
@@ -0,0 +1,4 @@
|
|||||||
|
110215 3 1.0 323 ~ <...> RY8CAA R-10
|
||||||
|
110215 -12 0.1 996 ~ GJ0KYZ UA6HI -15
|
||||||
|
110215 2 0.9 1235 ~ GJ0KYZ RK9AX MO05
|
||||||
|
110215 -16 0.9 2059 ~ CQ DX Z33Z KN11 N. Macedonia
|
||||||
BIN
Binary file not shown.
+22
@@ -0,0 +1,22 @@
|
|||||||
|
110615 -2 1.0 431 ~ VK4BLE OH8JK R-17
|
||||||
|
110615 -14 0.9 539 ~ RK6AH JH1AJT -05
|
||||||
|
110615 -18 0.9 656 ~ PA3EPP SP8NFO KN09
|
||||||
|
110615 -10 1.8 700 ~ RV6K RU3XL -13
|
||||||
|
110615 -16 0.9 756 ~ PA3EPP SP8NFO KN09
|
||||||
|
110615 -11 1.3 810 ~ SQ8OHR UA9LL MO27
|
||||||
|
110615 15 0.9 906 ~ PA3EPP SP8NFO KN09
|
||||||
|
110615 10 0.9 1196 ~ ET3RFG/R IN3ADG -23
|
||||||
|
110615 3 0.9 1284 ~ CQ F4FSY JN25 France
|
||||||
|
110615 -8 0.9 1349 ~ JR5MJS OH8NW 73
|
||||||
|
110615 -12 1.0 1404 ~ SV1GN RK6AUV LN05
|
||||||
|
110615 -24 0.9 1617 ~ PB5DX EI3CTB IO63
|
||||||
|
110615 10 1.5 2191 ~ CQ IZ1ANK JN33 Italy
|
||||||
|
110615 0 0.9 2281 ~ NT6Q OH8GDU -17
|
||||||
|
110615 -7 0.9 2447 ~ CQ DL1UDO JO31 Germany
|
||||||
|
110615 4 0.8 2576 ~ VK4BLE OH1EDK -20
|
||||||
|
110615 8 1.0 2656 ~ CQ JA OH1LWZ KP11 Finland
|
||||||
|
110615 -11 1.0 297 ~ <...> ON7EE JO10
|
||||||
|
110615 -17 0.8 594 ~ CQ DG0OFT JO50 Germany
|
||||||
|
110615 -16 0.8 1049 ~ CQ UB3AQS KO85 EU Russia
|
||||||
|
110615 -3 1.0 1201 ~ G1XJM HA7JIV JN97
|
||||||
|
110615 -16 1.4 2727 ~ SP7XIF JA2GQT -15
|
||||||
BIN
Binary file not shown.
+15
@@ -0,0 +1,15 @@
|
|||||||
|
110630 -20 1.1 518 ~ CQ PC2J JO22
|
||||||
|
110630 4 1.2 809 ~ UA9LL SQ8OHR -10
|
||||||
|
110630 15 -0.5 973 ~ JA2GQT SP7XIF JO91
|
||||||
|
110630 -3 0.8 1034 ~ CQ EA3UV JN01
|
||||||
|
110630 -5 1.4 1405 ~ RK6AUV SV1GN -18
|
||||||
|
110630 -15 1.0 1485 ~ SP8NFO PA3EPP +04
|
||||||
|
110630 -6 0.9 1670 ~ CQ PB5DX JO22
|
||||||
|
110630 -9 0.9 1722 ~ CQ SM7HZK JO76
|
||||||
|
110630 5 0.8 1954 ~ JH1AJT RK6AH R+07
|
||||||
|
110630 -2 0.9 2030 ~ JL1TZQ R3BV R-18
|
||||||
|
110630 -16 0.9 2110 ~ <...> DF1XG JO53
|
||||||
|
110630 19 1.3 2728 ~ CQ DX IK0YVV JN62
|
||||||
|
110630 -10 0.9 840 ~ CQ OR18RSX
|
||||||
|
110630 -24 0.3 1114 ~ CQ JR5MJS PM74
|
||||||
|
110630 -21 1.0 1695 ~ JA2GQT F8NHF -10
|
||||||
BIN
Binary file not shown.
+20
@@ -0,0 +1,20 @@
|
|||||||
|
110645 0 0.9 430 ~ VK4BLE OH8JK R-17
|
||||||
|
110645 -18 1.8 699 ~ CQ RU3XL KO84
|
||||||
|
110645 -23 0.7 756 ~ PA3EPP SP8NFO R+01
|
||||||
|
110645 9 0.7 906 ~ PA3EPP SP8NFO R+01
|
||||||
|
110645 -20 0.9 1049 ~ CQ UB3AQS KO85
|
||||||
|
110645 10 0.9 1196 ~ ET3RFG/R IN3ADG -23
|
||||||
|
110645 -1 0.9 1283 ~ CQ F4FSY JN25
|
||||||
|
110645 -16 1.0 1404 ~ SV1GN RK6AUV R-03
|
||||||
|
110645 -24 0.9 1617 ~ PB5DX EI3CTB IO63
|
||||||
|
110645 -10 1.0 2111 ~ CQ OR18TRA
|
||||||
|
110645 3 1.5 2191 ~ PC2J IZ1ANK +01
|
||||||
|
110645 -4 0.9 2281 ~ CQ OH8GDU KP24
|
||||||
|
110645 -10 0.9 2447 ~ CQ DL1UDO JO31
|
||||||
|
110645 6 0.7 2576 ~ VK4BLE OH1EDK -20
|
||||||
|
110645 5 1.0 2656 ~ CQ JA OH1LWZ KP11
|
||||||
|
110645 -21 0.8 594 ~ CQ DG0OFT JO50
|
||||||
|
110645 -21 0.7 1114 ~ <...> DA0FONTANE
|
||||||
|
110645 -6 1.0 1201 ~ G1XJM HA7JIV JN97
|
||||||
|
110645 -21 0.9 2092 ~ WB2QJ ES3AT KO18
|
||||||
|
110645 -15 1.4 2726 ~ SP7XIF JA2GQT -13
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user