PyFT8 3.7.4__tar.gz → 4.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. pyft8-4.0.0/PKG-INFO +88 -0
  2. pyft8-4.0.0/PyFT8/__init__.py +3 -0
  3. pyft8-4.0.0/PyFT8/receiver.py +531 -0
  4. pyft8-4.0.0/PyFT8/tk_gui.py +583 -0
  5. {pyft8-3.7.4 → pyft8-4.0.0}/PyFT8/transmitter.py +123 -87
  6. pyft8-4.0.0/PyFT8/udp_comms.py +41 -0
  7. pyft8-4.0.0/PyFT8.egg-info/PKG-INFO +88 -0
  8. pyft8-4.0.0/PyFT8.egg-info/SOURCES.txt +14 -0
  9. pyft8-4.0.0/PyFT8.egg-info/entry_points.txt +2 -0
  10. {pyft8-3.7.4 → pyft8-4.0.0}/PyFT8.egg-info/requires.txt +0 -3
  11. pyft8-4.0.0/README.md +69 -0
  12. {pyft8-3.7.4 → pyft8-4.0.0}/pyproject.toml +6 -9
  13. pyft8-3.7.4/MANIFEST.in +0 -2
  14. pyft8-3.7.4/PKG-INFO +0 -203
  15. pyft8-3.7.4/PyFT8/__init__.py +0 -0
  16. pyft8-3.7.4/PyFT8/databases.py +0 -360
  17. pyft8-3.7.4/PyFT8/gui.py +0 -368
  18. pyft8-3.7.4/PyFT8/osd.py +0 -88
  19. pyft8-3.7.4/PyFT8/pskreporter.py +0 -116
  20. pyft8-3.7.4/PyFT8/pyft8.py +0 -180
  21. pyft8-3.7.4/PyFT8/qso_manager.py +0 -159
  22. pyft8-3.7.4/PyFT8/receiver.py +0 -566
  23. pyft8-3.7.4/PyFT8/rigctrl.py +0 -103
  24. pyft8-3.7.4/PyFT8/time_utils.py +0 -58
  25. pyft8-3.7.4/PyFT8.egg-info/PKG-INFO +0 -203
  26. pyft8-3.7.4/PyFT8.egg-info/SOURCES.txt +0 -36
  27. pyft8-3.7.4/PyFT8.egg-info/entry_points.txt +0 -2
  28. pyft8-3.7.4/README.md +0 -181
  29. pyft8-3.7.4/tests/live test.py +0 -175
  30. pyft8-3.7.4/tests/misc/CQ AAAA.py +0 -30
  31. pyft8-3.7.4/tests/misc/animation.py +0 -36
  32. pyft8-3.7.4/tests/misc/bits_loopback.py +0 -204
  33. pyft8-3.7.4/tests/misc/bits_loopback_short.py +0 -169
  34. pyft8-3.7.4/tests/misc/osd.py +0 -89
  35. pyft8-3.7.4/tests/misc/symbols_loopback.py +0 -81
  36. pyft8-3.7.4/tests/misc/test_generate_wav.py +0 -58
  37. pyft8-3.7.4/tests/misc/test_loopback_performance.py +0 -220
  38. pyft8-3.7.4/tests/misc/text.py +0 -10
  39. pyft8-3.7.4/tests/misc/view_worked_before.py +0 -10
  40. pyft8-3.7.4/tests/pipeline/Fine tuning compare IOS_PC.py +0 -373
  41. pyft8-3.7.4/tests/pipeline/Targetted tests.py +0 -396
  42. pyft8-3.7.4/tests/pipeline/test_01.wav +0 -0
  43. pyft8-3.7.4/tests/plot_decode_times.py +0 -41
  44. {pyft8-3.7.4 → pyft8-4.0.0}/LICENSE +0 -0
  45. {pyft8-3.7.4 → pyft8-4.0.0}/PyFT8.egg-info/dependency_links.txt +0 -0
  46. {pyft8-3.7.4 → pyft8-4.0.0}/PyFT8.egg-info/top_level.txt +0 -0
  47. {pyft8-3.7.4 → pyft8-4.0.0}/setup.cfg +0 -0
pyft8-4.0.0/PKG-INFO ADDED
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyFT8
3
+ Version: 4.0.0
4
+ Summary: All-Python FT8 Transceiver with separable Transmitter and Receiver
5
+ Author-email: G1OJS <g1ojs@yahoo.com>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/G1OJS/PyFT8m
8
+ Project-URL: Issues, https://github.com/G1OJS/PyFT8m/issues
9
+ Keywords: ham radio,radio,FT8,LDPC,CPMFSK
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: numpy>=1.24.0
16
+ Requires-Dist: pyaudio
17
+ Requires-Dist: paho-mqtt
18
+ Dynamic: license-file
19
+
20
+ # PyFT8m
21
+ An FT8 Receiver, FT8 Transmitter, and GUI, written in three single & short Python files plus a tiny udp_comms file. The Receiver and Transmitter can be run standalone.
22
+
23
+ <img width="794" height="653" alt="Capture" src="https://github.com/user-attachments/assets/e53ab21f-4e98-4dfa-b521-9fbeecc9917b" />
24
+
25
+ ## Features
26
+ As of version 1.1.1, the key features are:
27
+ * Written entirely in Python
28
+ * Implements LDPC and OSD decoding
29
+ * Demonstrated by a Tkinter GUI
30
+ * Receiver and Transmitter can be used standalone
31
+ * Internal connections are via UDP (so e.g. alternative Receivers / Transmitters could be easily swapped in)
32
+ * Rx and Tx of standard messages with optional /P and /R
33
+ * Tx uses GFSK waveform as defined in FT8 standard
34
+ * RX of nonstandard calls plus hashed calls
35
+ * Launches quickly (~2 seconds on my old Dell Optiplex 790)
36
+ * Finds sound cards by keywords so follows them if e.g. Windows moves them ...
37
+ * Logs QSOs to user-specified ADIF file
38
+ * Uploads spots to pskreporter
39
+ * Controls rigs via Hamlib's rigctl.exe, which doesn't hog the rig's COM port(*)
40
+ * Click on the spectrum display to set Tx frequency
41
+
42
+ (*) Interactions with the rig are brief and infrequent (get/set frequency, set/unset PTT), and for my own purposes I prefer not having the Hamlib rigctld running, and having to check its run status and launch it if it's not running. In fact I'd prefer not to use Hamlib at all, and simply use a direct CAT command for the radio I'm using. I included Hamlib-style control so that people who don't have the same Icom rig as me can use this software.
43
+
44
+ ## Design philosophy
45
+ This isn't meant to do anything 'better' than applications like WSJT-x and MSHV, but it illustrates what can be done in a very few lines of Python, and it is written to be free of bloat. Currently, the code comprises:
46
+ - Receiver ~ 300 lines, including LDPC and OSD decoding
47
+ - Transmitter ~ 300 lines
48
+ - Tkinter GUI ~ 500 lines
49
+
50
+ That's an FT8 transmitter / receiver *application* in about 1100 lines of Python. The Receiver and Transmitter are implemented in single Python files (apart from common use of a small UDP class) and communicate with the GUI *only* via UDP. The point of doing this is to keep the interfaces here minimal, so that in theory it's easy to swap out either transmitter, receiver, or GUI and replace it with something written in any other language, subject to meeting / adapting the UDP commands reference below.
51
+
52
+ ## Installation and use
53
+ To use it as a 'turnkey' transceiver, just install the project via pip:
54
+ ```
55
+ pip install PyFT8m
56
+ ```
57
+ ... and then run the tk_gui either in a Python environment or by using the command:
58
+ ```
59
+ pyft8mgui
60
+ ```
61
+ You might have to open a new command window after pip install so that the command 'pyft8mgui' is recognised. I haven't written too much of a step by step quide here, as the project is really intended for people who want to poke around and maybe modify the code. But hopefully even if you're new to Python or coding, you can have a look and work out how it's done.
62
+
63
+ To control a rig's PTT and frequency, you'll need a copy of Hamlib's rigctl.exe.
64
+
65
+ ## Hacking it
66
+ The transmitter and receiver may be run independently from the GUI, as they communicate only via UDP (see below). For example, to run the receiver from Python,
67
+ ```
68
+ from PyFT8m import Receiver
69
+ rx = Receiver(output_type = 'print', input_keywords = 'Mic, CODEC')
70
+ ```
71
+ Or of course copy the receiver.py file, add this line at the bottom (minus the import), and hack away.
72
+
73
+ ### UDP messages
74
+ The following shows the main points of the way that the UDP messages are constructed - please see the code for the absolute reference.
75
+ 1. All UDP messages apart from those sent to pskreporter.info or the waterfall data sent from the receiver to the GUI, are Python dictionaries serialised with JSON and encoded to bytes.
76
+ 2. All dictionaries start with a key called 'mtype' meaning 'messsage type'. The main ones are 'decode' (indicating that the dictionary contains various entries from a decoded message), 'shutdown', 'waterfall', 'rollover', 'test_info', 'set_tx_Hz', 'generate_tx_audio', 'send_tx_audio', 'stop_tx_audio'.
77
+
78
+ As the project grows, I might write a more formal spec for these messages.
79
+
80
+ ## Acknowledgements
81
+ This project implements a decoder for the FT8 digital mode. FT8 was developed by Joe Taylor, K1JT, Steve Franke, K9AN, and others as part of the WSJT-X project.
82
+ Protocol details are based on information publicly described by the WSJT-X authors and in related open documentation.
83
+
84
+ Some constants and tables (e.g. Costas synchronization sequence, LDPC structure, message packing scheme) are derived from
85
+ the publicly available WSJT-X source code and FT8 protocol descriptions. Original WSJT-X source is © the WSJT Development Group
86
+ and distributed under the GNU General Public License v3 (GPL-3.0), hence the use of GPL-3.0 in this repository.
87
+
88
+
@@ -0,0 +1,3 @@
1
+ from .receiver import Receiver
2
+ from .transmitter import Transmitter
3
+ from .udp_comms import UdpComms
@@ -0,0 +1,531 @@
1
+ import numpy as np
2
+ import time, pyaudio, threading
3
+ from PyFT8.udp_comms import UdpComms
4
+
5
+ HPS, BPT = 4, 2
6
+ TIME_WINDOW = [-1.5, 3.5]
7
+ SYM_RATE, SAMP_RATE = 6.25, 12000
8
+ MIN_SCORE = 0.5
9
+ MAX_CANDS = 400
10
+ MAX_LDPC = 25
11
+
12
+ #------------------------------
13
+ HPC = int(15 * SYM_RATE * HPS)
14
+ T_SEARCH = TIME_WINDOW[1]+(36+7)/SYM_RATE
15
+ H0_RANGE = [int(SYM_RATE * HPS * t) for t in TIME_WINDOW]
16
+
17
+ # =============== Call hashing ========================================
18
+ call_hashes = {}
19
+ def add_call_hash(call):
20
+ global call_hashes
21
+ chars = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"
22
+ call_padded = (call + " ")[:11]
23
+ hashes = []
24
+ for m in [10,12,22]:
25
+ x = 0
26
+ for c in call_padded:
27
+ x = 38*x + chars.find(c)
28
+ x = x & ((int(1) << 64) - 1)
29
+ x = x & ((1 << 64) - 1)
30
+ x = x * 47055833459
31
+ x = x & ((1 << 64) - 1)
32
+ x = x >> (64 - m)
33
+ hashes.append(x)
34
+ call_hashes[(x,m)] = call
35
+
36
+ #=========== Unpacking functions ========================================
37
+ CALLSIGN_PREFIXES1 = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,R,S,T,U,V,W,X,Y,Z"
38
+ CALLSIGN_PREFIXES2 = "2A,2B,2C,2D,2E,2F,2G,2H,2I,2J,2K,2L,2M,2N,2O,2P,2Q,2R,2S,2T,2U,2V,2W,2X,2Y,2Z,3A,3B,3C,3D,3D,3E,3F,3G,3H,3I,3J,3K,3L,3M,3N,3O,3P,3Q,3R,3S,3T,3U,3V,3W,3X,3Y,3Z,4A,4B,4C,4D,4E,4F,4G,4H,4I,4J,4K,4L,4M,4O,4P,4Q,4R,4S,4T,4U,4V,4W,4X,4Y,4Z,5A,5B,5C,5D,5E,5F,5G,5H,5I,5J,5K,5L,5M,5N,5O,5P,5Q,5R,5S,5T,5U,5V,5W,5X,5Y,5Z,6A,6B,6C,6D,6E,6F,6G,6H,6I,6J,6K,6L,6M,6N,6O,6P,6Q,6R,6S,6T,6U,6V,6W,6X,6Y,6Z,7A,7B,7C,7D,7E,7F,7G,7H,7I,7J,7K,7L,7M,7N,7O,7P,7Q,7R,7S,7T,7U,7V,7W,7X,7Y,7Z,8A,8B,8C,8D,8E,8F,8G,8H,8I,8J,8K,8L,8M,8N,8O,8P,8Q,8R,8S,8T,8U,8V,8W,8X,8Y,8Z,9A,9B,9C,9D,9E,9F,9G,9H,9I,9J,9K,9L,9M,9N,9O,9P,9Q,9R,9S,9T,9U,9V,9W,9X,9Y,9Z,A2,A3,A4,A5,A6,A7,A8,A9,AA,AB,AC,AD,AE,AF,AG,AH,AI,AJ,AK,AL,AM,AN,AO,AP,AQ,AR,AS,AT,AU,AV,AW,AX,AY,AZ,BA,BB,BC,BD,BE,BF,BG,BH,BI,BJ,BK,BL,BM,BN,BO,BP,BQ,BR,BS,BT,BU,BV,BW,BX,BY,BZ,C2,C3,C4,C5,C6,C7,C8,C9,CA,CB,CC,CD,CE,CF,CG,CH,CI,CJ,CK,CL,CM,CN,CO,CP,CQ,CR,CS,CT,CU,CV,CW,CX,CY,CZ,D2,D3,D4,D5,D6,D7,D8,D9,DA,DB,DC,DD,DE,DF,DG,DH,DI,DJ,DK,DL,DM,DN,DO,DP,DQ,DR,DS,DT,DU,DV,DW,DX,DY,DZ,E2,E3,E4,E5,E6,E7,EA,EB,EC,ED,EE,EF,EG,EH,EI,EJ,EK,EL,EM,EN,EO,EP,EQ,ER,ES,ET,EU,EV,EW,EX,EY,EZ,FA,FB,FC,FD,FE,FF,FG,FH,FI,FJ,FK,FL,FM,FN,FO,FP,FQ,FR,FS,FT,FU,FV,FW,FX,FY,FZ,GA,GB,GC,GD,GE,GF,GG,GH,GI,GJ,GK,GL,GM,GN,GO,GP,GQ,GR,GS,GT,GU,GV,GW,GX,GY,GZ,H2,H3,H4,H6,H7,H8,H9,HA,HB,HC,HD,HE,HF,HG,HH,HI,HJ,HK,HL,HM,HN,HO,HP,HQ,HR,HS,HT,HU,HV,HW,HX,HY,HZ,IA,IB,IC,ID,IE,IF,IG,IH,II,IJ,IK,IL,IM,IN,IO,IP,IQ,IR,IS,IT,IU,IV,IW,IX,IY,IZ,J2,J3,J4,J5,J6,J7,J8,JA,JB,JC,JD,JE,JF,JG,JH,JI,JJ,JK,JL,JM,JN,JO,JP,JQ,JR,JS,JT,JU,JV,JW,JX,JY,JZ,KA,KB,KC,KD,KE,KF,KG,KH,KI,KJ,KK,KL,KM,KN,KO,KP,KQ,KR,KS,KT,KU,KV,KW,KX,KY,KZ,L2,L3,L4,L5,L6,L7,L8,L9,LA,LB,LC,LD,LE,LF,LG,LH,LI,LJ,LK,LL,LM,LN,LO,LP,LQ,LR,LS,LT,LU,LV,LW,LX,LY,LZ,MA,MB,MC,MD,ME,MF,MG,MH,MI,MJ,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NB,NC,ND,NE,NF,NG,NH,NI,NJ,NK,NL,NM,NN,NO,NP,NQ,NR,NS,NT,NU,NV,NW,NX,NY,NZ,OA,OB,OC,OD,OE,OF,OG,OH,OI,OJ,OK,OL,OM,ON,OO,OP,OQ,OR,OS,OT,OU,OV,OW,OX,OY,OZ,P2,P3,P4,P5,P6,P7,P8,P9,PA,PB,PC,PD,PE,PF,PG,PH,PI,PJ,PJ,PJ,PK,PL,PM,PN,PO,PP,PQ,PR,PS,PT,PU,PV,PW,PX,PY,PZ,RA,RB,RC,RD,RE,RF,RG,RH,RI,RJ,RK,RL,RM,RN,RO,RP,RQ,RR,RS,RT,RU,RV,RW,RX,RY,RZ,S2,S3,S5,S6,S7,S8,S9,SA,SB,SC,SD,SE,SF,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SP,SQ,SR,SS,SS,ST,SU,SV,SW,SX,SY,SZ,T2,T3,T4,T5,T6,T7,T8,TA,TB,TC,TD,TE,TF,TG,TH,TI,TJ,TK,TL,TM,TN,TO,TP,TQ,TR,TS,TT,TU,TV,TW,TX,TY,TZ,UA,UB,UC,UD,UE,UF,UG,UH,UI,UJ,UK,UL,UM,UN,UO,UP,UQ,UR,US,UT,UU,UV,UW,UX,UY,UZ,V2,V3,V4,V5,V6,V7,V8,VA,VB,VC,VD,VE,VF,VG,VH,VI,VJ,VK,VL,VM,VN,VO,VP,VQ,VR,VS,VT,VU,VV,VW,VX,VY,VZ,WA,WB,WC,WD,WE,WF,WG,WH,WI,WJ,WK,WL,WM,WN,WO,WP,WQ,WR,WS,WT,WU,WV,WW,WX,WY,WZ,XA,XB,XC,XD,XE,XF,XG,XH,XI,XJ,XK,XL,XM,XN,XO,XP,XQ,XR,XS,XT,XU,XV,XW,XX,XY,XZ,Y2,Y3,Y4,Y5,Y6,Y7,Y8,Y9,YA,YB,YC,YD,YE,YF,YG,YH,YI,YJ,YK,YL,YM,YN,YO,YP,YQ,YR,YS,YT,YU,YV,YW,YX,YY,Z2,Z3,Z8,ZA,ZB,ZC,ZD,ZE,ZF,ZG,ZH,ZI,ZJ,ZK,ZL,ZM,ZN,ZO,ZP,ZQ,ZR,ZS,ZT,ZU,ZV,ZW,ZX,ZY,ZZ"
39
+
40
+ def get_bitfields(bits, lengths):
41
+ fields = []
42
+ for n in lengths:
43
+ mask = (1 << n) - 1
44
+ fields.append(bits & mask)
45
+ bits >>= n
46
+ return *fields, bits
47
+
48
+ def unpack(bits):
49
+ if not bits:
50
+ return None
51
+
52
+ i3, bits74 = get_bitfields(bits,[3])
53
+ if i3 == 0:
54
+ n3, bits71 = get_bitfields(bits74,[3])
55
+ if n3 <= 4:
56
+ #return (['Free text', 'DXpedition', 'Field Day', 'Field Day', 'Telemetry'][n3], 'not', 'implemented')
57
+ return None
58
+ else:
59
+ #return ('Unknown mode','not','implemented')
60
+ return None
61
+ elif i3 == 1 or i3 == 2: # 1 = Std Msg incl /R 2 = 'EU VHF' = Std Msg incl /P
62
+ return unpack_std(bits74, i3)
63
+ elif i3 == 3:
64
+ #return ('RTTY RU','not','implemented')
65
+ return None
66
+ elif i3 == 4:
67
+ cq, rrr, swp, c58, hsh, _ = get_bitfields(bits74, [1,2,1,58,12])
68
+ if cq and rrr or (not cq and not rrr):
69
+ return None
70
+ ca = "CQ" if cq else f"<{call_hashes.get((hsh,12), '...')}>"
71
+ cb = ""
72
+ for i in range(12):
73
+ cb = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"[c58 % 38] + cb
74
+ c58 = c58 // 38
75
+ cb = cb.strip()
76
+ add_call_hash(cb)
77
+ (ca, cb) = (cb, ca) if swp else (ca, cb)
78
+ return (ca, cb, ('', 'RRR', 'RR73', '73')[rrr])
79
+ elif i3 == 5:
80
+ #return ('EU VHF','not','implemented')
81
+ return None
82
+
83
+ def unpack_std(bits74, i3):
84
+ g16, cb29, ca29, _ = get_bitfields(bits74,[16,29,29])
85
+ g15 = g16 & 0x7FFF
86
+ if g15 == 0:
87
+ return None
88
+ if g15 < 32400:
89
+ a, nn = divmod(g15, 1800)
90
+ b, nn = divmod(nn, 100)
91
+ c, d = divmod(nn, 10)
92
+ grid_rpt = chr(65+a) + chr(65+b) + str(c) + str(d)
93
+ elif g15 - 32400 <= 4:
94
+ grid_rpt = ('', '', 'RRR', 'RR73', '73')[g15 - 32400]
95
+ else:
96
+ prefix = 'R' if (g16 >> 15) else ''
97
+ grid_rpt = prefix + f"{(g15 - 32435):+03d}"
98
+ msg_tuple = (call_29(ca29, i3), call_29(cb29, i3), grid_rpt)
99
+ if not ('' in msg_tuple) and not (None in msg_tuple) and not 'CQ' in msg_tuple[-1]:
100
+ return msg_tuple
101
+
102
+ def call_29(call_int29, i3):
103
+ portable_rover = call_int29 & 1
104
+ call_int28 = call_int29>>1
105
+ if call_int28 < 3:
106
+ return ['DE', 'QRZ', 'CQ'][call_int28]
107
+ elif call_int28 < 1004:
108
+ return f"CQ {call_int28 - 3:03d}"
109
+ elif call_int28 < 21443:
110
+ x, txt = call_int28 - 1003, ''
111
+ for i in range(4):
112
+ txt = " ABCDEFGHIJKLMNOPQRSTUVWXYZ"[int(x % 27)] + txt
113
+ x //= 27
114
+ return f"CQ {txt.strip()}"
115
+ elif call_int28 < 2063592+4194303:
116
+ return f"<{call_hashes.get((call_int28 - 2063592, 22), '...')}>"
117
+ else:
118
+ call = standard_call28(call_int28, i3)
119
+ if call is not None:
120
+ if portable_rover:
121
+ call = call + ('/P' if i3 == 2 else '/R')
122
+ if call.endswith("/R") and not call[0] in ['A','K','N','W']:
123
+ return None
124
+ add_call_hash(call)
125
+ return call
126
+
127
+ def standard_call28(call_int28, i3):
128
+ nn = call_int28 - (2063592 + 4194304)
129
+ from string import ascii_uppercase as ltrs, digits as digs
130
+ call_fields = [ (' ' + digs + ltrs, 36*10*27**3), (digs + ltrs, 10*27**3), (digs + ' ' * 17, 27**3),
131
+ (' ' + ltrs, 27**2), (' ' + ltrs, 27), (' ' + ltrs, 1) ]
132
+ chars = []
133
+ for alphabet, div in call_fields:
134
+ idx, nn = divmod(nn, div)
135
+ chars.append(alphabet[idx])
136
+ call = ''.join(chars).strip()
137
+ return simple_validate_call(call)
138
+
139
+ def simple_validate_call(call):
140
+ if not ' ' in call and len(call)>=3:
141
+ if call[0] in CALLSIGN_PREFIXES1 and call[1].isnumeric():
142
+ if not (call[0] in "B,F,G,I,K,M,N,R,W" and call[2].isnumeric()):
143
+ return call
144
+ if call[:2] in CALLSIGN_PREFIXES2 and call[2].isnumeric():
145
+ return call
146
+
147
+ def crc_unpack91(bits91_int):
148
+ bits77_int = bits91_int >> 14
149
+ msg = None
150
+ if(bits77_int > 0):
151
+ crc14_int = 0
152
+ for i in range(96):
153
+ inbit = ((bits77_int >> (76 - i)) & 1) if i < 77 else 0
154
+ bit14 = (crc14_int >> (14 - 1)) & 1
155
+ crc14_int = ((crc14_int << 1) & ((1 << 14) - 1)) | inbit
156
+ if bit14:
157
+ crc14_int ^= 0x2757
158
+ if crc14_int == bits91_int & 0b11111111111111:
159
+ msg = unpack(bits77_int)
160
+ return msg, bits77_int
161
+
162
+ #============== Early decode if message bits are sound ===============================================
163
+
164
+ def decode_raw91(p, i):
165
+ def syms_to_int(syms):
166
+ bint = 0
167
+ for s in syms:
168
+ bint = (bint << 3) | (s & 7)
169
+ return bint
170
+ ch_syms = np.argmax(p[:31, :], axis = 1)
171
+ de_gray = [0,1,3,2,6,4,5,7]
172
+ for j in range(2):
173
+ if j == 1:
174
+ ch_syms[:11] = 0
175
+ ch_syms[8] = 1
176
+ syms = [de_gray[s] for s in ch_syms]
177
+ bits91_int = syms_to_int(syms) >> 2
178
+ msg_tuple, bits77_int = crc_unpack91(bits91_int)
179
+ if msg_tuple:
180
+ return msg_tuple, f"GOOD91 t={i} {['','CQ'][j]}"
181
+ return None, None
182
+
183
+ def bits_to_int(llr):
184
+ bits_int = 0
185
+ for bit in (llr > 0).astype(int).tolist():
186
+ bits_int = (bits_int << 1) | bit
187
+ return bits_int
188
+
189
+ #============== LDPC ===========================================================
190
+ CVidx_all = np.array([[4,31,59,91,92,96,153],[8,25,63,83,93,96,148],[5,34,65,78,98,107,154],[11,37,67,87,101,139,158],[8,40,70,82,104,114,145],[14,41,71,88,102,123,156],[17,37,74,81,109,131,154],[45,55,64,111,130,161,173],[18,36,76,89,113,114,143],[21,45,78,83,117,121,151],[19,35,59,73,110,125,161],[7,49,58,90,100,105,168],[25,53,69,90,101,130,156],[20,46,65,80,120,140,170],[1,4,52,57,86,136,152],[26,51,56,91,122,137,168],[2,27,41,61,62,115,133],[28,48,70,85,105,129,158],[12,43,66,89,97,135,159],[10,44,82,91,111,144,149],[30,50,60,86,137,142,162],[10,53,66,84,112,128,165],[28,29,84,88,117,143,150],[15,58,60,74,111,150,163],[5,32,60,93,115,146,0],[6,24,61,94,122,151,0],[7,33,62,95,96,143,0],[6,32,64,97,126,138,0],[9,35,66,99,139,146,0],[10,36,67,100,107,126,0],[12,38,68,102,105,155,0],[13,39,69,103,149,162,0],[15,42,59,106,123,159,0],[1,33,72,106,107,157,0],[16,43,73,108,141,160,0],[11,44,75,110,121,166,0],[8,46,71,112,119,166,0],[19,38,77,104,116,163,0],[20,47,70,92,138,165,0],[2,48,74,113,128,160,0],[22,47,58,118,127,164,0],[16,39,62,112,134,158,0],[23,43,79,120,131,145,0],[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],[12,50,61,118,119,144,0],[13,51,64,114,118,157,0],[24,52,76,129,148,149,0],[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],[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],[27,40,56,124,125,126,0],[18,49,55,124,141,167,0],[6,33,85,108,116,156,0],[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],[5,39,75,102,136,167,0],[2,54,86,101,135,164,0],[15,56,87,108,119,171,0],[23,34,71,94,127,153,0],[11,49,88,92,142,157,0],[29,34,87,97,147,162,0],[22,57,85,93,140,159,0],[28,32,72,103,132,166,0],[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],[18,42,79,144,146,152,0],[25,38,65,99,122,160,0],[17,42,75,129,170,172,0]], dtype = np.int16)
191
+ llr175 = np.zeros(175, dtype=np.float32)
192
+ mC2V_prev = np.zeros(CVidx_all.shape, dtype=np.float32)
193
+ import warnings
194
+ warnings.filterwarnings("error")
195
+ def decode(p, i):
196
+ def atanh(x):
197
+ a = 1.03
198
+ y = -x / ((x-a)*(x+a))
199
+ y[(x > a * 0.93)] = 7
200
+ y[(x < -a * 0.93)] = -7
201
+ return y
202
+ global llr175, mC2V_prev
203
+ llra = np.max(p[:, [4,5,6,7]], axis=1) - np.max(p[:, [0,1,2,3]], axis=1)
204
+ llrb = np.max(p[:, [2,3,4,7]], axis=1) - np.max(p[:, [0,1,5,6]], axis=1)
205
+ llrc = np.max(p[:, [1,2,6,7]], axis=1) - np.max(p[:, [0,3,4,5]], axis=1)
206
+ llr = np.column_stack((llra, llrb, llrc))
207
+ llr = llr.ravel()
208
+ llr = 2.55 * llr / (np.std(llr) + 1e-12)
209
+ llr175[1:] = llr
210
+ llr_clip = 7
211
+ llr175 = np.clip(llr175, -llr_clip, llr_clip)
212
+ mC2V_prev[:, :] = 0
213
+ ch_llr_saved = llr175[1:]
214
+ llr_saved = None
215
+ for ldpc_it in range(MAX_LDPC):
216
+ mV2C = llr175[CVidx_all] - mC2V_prev
217
+ tanh_mV2C = np.tanh(-mV2C / 2)
218
+ tanh_mC2V = np.prod(tanh_mV2C, axis=1, keepdims=True)
219
+ tanh_mC2V[24:,:6] = np.prod(tanh_mV2C[24:,:6], axis=1, keepdims=True)
220
+ orig_err = np.geterr()
221
+ np.seterr(all = 'ignore')
222
+ tanh_mC2V = np.divide(tanh_mC2V, tanh_mV2C)
223
+ mC2V_curr = 2 * atanh(-tanh_mC2V)
224
+ np.seterr(**orig_err)
225
+ np.add.at(llr175, CVidx_all, mC2V_curr - mC2V_prev)
226
+ llr175[0]=0
227
+ llr175 = np.clip(llr175, -llr_clip, llr_clip)
228
+ mC2V_prev = mC2V_curr
229
+ bits = llr175[CVidx_all] > 0
230
+ parity = np.sum(bits, axis=1) & 1
231
+ ncheck = int(np.sum(parity))
232
+ if(ncheck == 0):
233
+ bits91_int = bits_to_int(llr175[1:92])
234
+ msg_tuple, bits77_int = crc_unpack91(bits91_int)
235
+ return msg_tuple, f"LDPC t={i} its={ldpc_it+1}", llr_saved, ch_llr_saved
236
+ if ldpc_it == 5:
237
+ llr_saved = llr175[1:]
238
+ return None, '', llr_saved, ch_llr_saved
239
+
240
+ #============== OSD ===========================================================
241
+ generator_matrix_rows = ["8329ce11bf31eaf509f27fc", "761c264e25c259335493132", "dc265902fb277c6410a1bdc", "1b3f417858cd2dd33ec7f62", "09fda4fee04195fd034783a", "077cccc11b8873ed5c3d48a", "29b62afe3ca036f4fe1a9da", "6054faf5f35d96d3b0c8c3e", "e20798e4310eed27884ae90", "775c9c08e80e26ddae56318", "b0b811028c2bf997213487c", "18a0c9231fc60adf5c5ea32", "76471e8302a0721e01b12b8", "ffbccb80ca8341fafb47b2e", "66a72a158f9325a2bf67170", "c4243689fe85b1c51363a18", "0dff739414d1a1b34b1c270", "15b48830636c8b99894972e", "29a89c0d3de81d665489b0e", "4f126f37fa51cbe61bd6b94", "99c47239d0d97d3c84e0940", "1919b75119765621bb4f1e8", "09db12d731faee0b86df6b8", "488fc33df43fbdeea4eafb4", "827423ee40b675f756eb5fe", "abe197c484cb74757144a9a", "2b500e4bc0ec5a6d2bdbdd0", "c474aa53d70218761669360", "8eba1a13db3390bd6718cec", "753844673a27782cc42012e", "06ff83a145c37035a5c1268", "3b37417858cc2dd33ec3f62", "9a4a5a28ee17ca9c324842c", "bc29f465309c977e89610a4", "2663ae6ddf8b5ce2bb29488", "46f231efe457034c1814418", "3fb2ce85abe9b0c72e06fbe", "de87481f282c153971a0a2e", "fcd7ccf23c69fa99bba1412", "f0261447e9490ca8e474cec", "4410115818196f95cdd7012", "088fc31df4bfbde2a4eafb4", "b8fef1b6307729fb0a078c0", "5afea7acccb77bbc9d99a90", "49a7016ac653f65ecdc9076", "1944d085be4e7da8d6cc7d0", "251f62adc4032f0ee714002", "56471f8702a0721e00b12b8", "2b8e4923f2dd51e2d537fa0", "6b550a40a66f4755de95c26", "a18ad28d4e27fe92a4f6c84", "10c2e586388cb82a3d80758", "ef34a41817ee02133db2eb0", "7e9c0c54325a9c15836e000", "3693e572d1fde4cdf079e86", "bfb2cec5abe1b0c72e07fbe", "7ee18230c583cccc57d4b08", "a066cb2fedafc9f52664126", "bb23725abc47cc5f4cc4cd2", "ded9dba3bee40c59b5609b4", "d9a7016ac653e6decdc9036", "9ad46aed5f707f280ab5fc4", "e5921c77822587316d7d3c2", "4f14da8242a8b86dca73352", "8b8b507ad467d4441df770e", "22831c9cf1169467ad04b68", "213b838fe2ae54c38ee7180", "5d926b6dd71f085181a4e12", "66ab79d4b29ee6e69509e56", "958148682d748a38dd68baa", "b8ce020cf069c32a723ab14", "f4331d6d461607e95752746", "6da23ba424b9596133cf9c8", "a636bcbc7b30c5fbeae67fe", "5cb0d86a07df654a9089a20", "f11f106848780fc9ecdd80a", "1fbb5364fb8d2c9d730d5ba", "fcb86bc70a50c9d02a5d034", "a534433029eac15f322e34c", "c989d9c7c3d3b8c55d75130", "7bb38b2f0186d46643ae962", "2644ebadeb44b9467d1f42c", "608cc857594bfbb55d69600"]
242
+ kGEN = np.array([int(row,16)>>1 for row in generator_matrix_rows])
243
+ A = np.zeros((83, 91), dtype=np.uint8)
244
+ for i, row in enumerate(kGEN):
245
+ for j in range(91):
246
+ A[i, 90 - j] = (row >> j) & 1
247
+ G0 = np.concatenate([np.eye(91, dtype=np.uint8), A.T],axis=1)
248
+
249
+ def osd(llr_pack):
250
+ source, llr = llr_pack
251
+ chbits174 = (llr>0).astype(np.uint8)
252
+ chvals174 = np.abs(llr)
253
+
254
+ rowperm = np.arange(91)
255
+ colperm = np.argsort(-np.abs(llr))
256
+ curr_row = 0
257
+ G = G0.copy()
258
+ for curr_col in range(174):
259
+ ones_below = np.where(G[rowperm[curr_row:], colperm[curr_col]] == 1)[0]
260
+ if ones_below.size > 0:
261
+ swap_row = curr_row + ones_below[0]
262
+ rowperm[[curr_row, swap_row]] = rowperm[[swap_row, curr_row]]
263
+ r_curr = rowperm[curr_row]
264
+ c_curr = colperm[curr_col]
265
+ g_c_curr = G[:, c_curr].copy()
266
+ g_c_curr[r_curr] = 0
267
+ rows_to_xor = np.where(g_c_curr == 1)[0]
268
+ G[rows_to_xor, :] ^= G[r_curr, :]
269
+ colperm[[curr_row, curr_col]] = colperm[[curr_col, curr_row]]
270
+ curr_row += 1
271
+ if curr_row > 90:
272
+ break
273
+
274
+ chbits91 = chbits174[colperm][:91]
275
+ chbits91[rowperm] = chbits91
276
+
277
+ base_cw = ((chbits91 @ G) & 1)
278
+
279
+ bits91_int = bits_to_int(base_cw[:91])
280
+ msg_tuple, bits77_int = crc_unpack91(bits91_int)
281
+ if msg_tuple:
282
+ return msg_tuple, f"OSD0{source}"
283
+
284
+ fliplist = rowperm[::-1]
285
+ current_best_distance = 1e20
286
+ cw_out91 = None
287
+ for i in range(91):
288
+ cw = base_cw ^ G[fliplist[i]] # Single flip
289
+ if i: # Double flips with every j < i
290
+ cw2 = base_cw ^ G[fliplist[i]] ^ G[fliplist[:i]]
291
+ candidates = np.vstack((cw, cw2))
292
+ else:
293
+ candidates = cw[None, :]
294
+ distances = np.sum(np.abs(llr)[None, :] * (candidates != chbits174), axis=1)
295
+ best_idx = np.argmin(distances)
296
+ if distances[best_idx] < current_best_distance:
297
+ current_best_distance = distances[best_idx]
298
+ cw_out91 = candidates[best_idx, :91].copy()
299
+ if cw_out91 is not None:
300
+ bits91_int = bits_to_int(cw_out91)
301
+ msg_tuple, bits77_int = crc_unpack91(bits91_int)
302
+ if msg_tuple:
303
+ return msg_tuple, f"OSD2{source}"
304
+ return None, ''
305
+
306
+ #============== AUDIO ========================================================
307
+ class AudioIn:
308
+ def __init__(self, input_keywords, max_freq):
309
+ samples_per_hop = int(SAMP_RATE / (SYM_RATE * HPS))
310
+ self.samples_per_half_hop = int(samples_per_hop / 2)
311
+ fft_len = int(BPT * SAMP_RATE // SYM_RATE)
312
+ fft_out_len = fft_len // 2 + 1
313
+ self.nFreqs = int(fft_out_len * 2 * max_freq / SAMP_RATE)
314
+ self.audio_buffer = np.zeros(fft_len + self.samples_per_half_hop, dtype=np.float32)
315
+ self.fft_in = np.zeros(fft_len, dtype=np.float32)
316
+ self.fft_window = np.hanning(fft_len).astype(np.float32)
317
+ self.tfgrid = np.ones((2, HPC, self.nFreqs), dtype = np.float32)
318
+ self.indev = self.find_device(input_keywords)
319
+ self.tfgrid_ptr = 0
320
+ self.check_pointer()
321
+ if self.indev:
322
+ self.stream = pyaudio.PyAudio().open(
323
+ format = pyaudio.paInt16, channels=1, rate = SAMP_RATE, input = True, input_device_index = self.indev,
324
+ frames_per_buffer = samples_per_hop, stream_callback=self._callback,)
325
+ self.stream.start_stream()
326
+
327
+ def find_device(self, device_str_contains):
328
+ pya = pyaudio.PyAudio()
329
+ for dev_idx in range(pya.get_device_count()):
330
+ name = pya.get_device_info_by_index(dev_idx)['name']
331
+ match = True
332
+ for pattern in device_str_contains.replace(' ','').split(','):
333
+ if (not pattern in name): match = False
334
+ if(match):
335
+ return dev_idx
336
+
337
+ def check_pointer(self):
338
+ ptr = int((time.time() % 15) * SYM_RATE * HPS)
339
+ if np.abs(self.tfgrid_ptr - ptr) > 3:
340
+ self.tfgrid_ptr = ptr
341
+
342
+ def _callback(self, in_data, frame_count, time_info, status_flags):
343
+ samples = np.frombuffer(in_data, dtype=np.int16).astype(np.float32)
344
+ ns = len(samples)
345
+ self.audio_buffer[:-ns] = self.audio_buffer[ns:]
346
+ self.audio_buffer[-ns:] = samples
347
+ self.tfgrid_ptr = (self.tfgrid_ptr + 1) % HPC
348
+
349
+ np.multiply(self.audio_buffer[self.samples_per_half_hop:], self.fft_window, out=self.fft_in)
350
+ z = np.fft.rfft(self.fft_in)[:self.nFreqs]
351
+ self.tfgrid[0, self.tfgrid_ptr, :] = 20*np.log10(np.abs(z))
352
+
353
+ np.multiply(self.audio_buffer[:-self.samples_per_half_hop], self.fft_window, out=self.fft_in)
354
+ z = np.fft.rfft(self.fft_in)[:self.nFreqs]
355
+ self.tfgrid[1, self.tfgrid_ptr, :] = 20*np.log10(np.abs(z))
356
+
357
+ return (None, pyaudio.paContinue)
358
+
359
+ class Receiver:
360
+ def __init__(self, max_freq = 3000, output_type = 'udp', latest_decode = 2,
361
+ ports = None, input_keywords = None):
362
+ self.latest_decode = latest_decode
363
+ self.input_keywords = input_keywords
364
+ self.max_freq = max_freq
365
+ self.output_type = output_type
366
+ self.candidates = []
367
+ self.duplicate_filter = []
368
+ payload_symb_idxs = list(range(7, 36)) + list(range(43, 72))
369
+ self.base_payload_hops = np.array([HPS * s for s in payload_symb_idxs])
370
+ self.hop_idxs_Costas = np.arange(7) * HPS
371
+ self.base_freq_idxs = np.array([BPT * t for t in range(8)])
372
+ csync = np.full((7, 8*BPT), -1/(8 * BPT - 1), np.float32)
373
+ for sym_idx, tone in enumerate([3,1,4,0,6,5,2]):
374
+ csync[sym_idx, tone * BPT] = 1
375
+ self.csync_flat = csync.ravel()
376
+ self.waterfall_max = 0
377
+ self.running = True
378
+ if self.output_type == 'udp':
379
+ self.udp_comms = UdpComms(ports = ports, rx_callback = self.udp_received)
380
+ self.audio_in = AudioIn(input_keywords, self.max_freq)
381
+ threading.Thread(target = self.manage_cycle, daemon=True ).start()
382
+ threading.Thread(target = self.send_waterfall_rows, daemon=True ).start()
383
+ if self.audio_in.indev:
384
+ self.send_output({'mtype':'info', 'info':'Receiver starting'})
385
+ self.send_output({'mtype':'STATUS', 'from':'receiver', 'value':'OK'})
386
+ else:
387
+ info = f"[Receiver] Couldn't find input device matching '{self.input_keywords}'"
388
+ self.send_output({'mtype':'info', 'info':info})
389
+ self.running = False
390
+
391
+ def send_output(self, msg_dict, noprint = False):
392
+ if self.output_type == 'print':
393
+ if not noprint:
394
+ print(msg_dict)
395
+ return
396
+ self.udp_comms.udp_send_dict(msg_dict)
397
+
398
+ def udp_received(self, msg_dict):
399
+ if msg_dict['mtype'] == 'shutdown':
400
+ self.running = False
401
+
402
+ def manage_cycle(self):
403
+ cycle_searched = False
404
+ t_cyc, t_cyc_prev = 0, 0
405
+ while self.running:
406
+ time.sleep(0.1)
407
+ t_cyc = time.time() % 15
408
+ if t_cyc < t_cyc_prev:
409
+ self.audio_in.check_pointer()
410
+ cycle_searched = False
411
+ if t_cyc > T_SEARCH and not cycle_searched:
412
+ cycle_searched = True
413
+ self.search_and_decode()
414
+ t_cyc_prev = t_cyc
415
+ self.udp_comms.udp_send_dict({'mtype':'info', 'info':'Receiver is shut down'})
416
+
417
+ def send_waterfall_rows(self):
418
+ while self.running:
419
+ time.sleep(0.25)
420
+ row = self.audio_in.tfgrid[0,self.audio_in.tfgrid_ptr,:]
421
+ self.waterfall_max = np.max([np.max(row), 0.999 * self.waterfall_max])
422
+ dBrng = 35
423
+ row = (dBrng + np.clip(row - self.waterfall_max, -dBrng, 0)) / dBrng
424
+ row = ','.join([f"{r:.2f}"[-2:] for i, r in enumerate(row) if i % 3 == 0])
425
+ self.send_output({'mtype':'waterfall', 'data':row}, noprint = True)
426
+
427
+ def search_and_decode(self):
428
+ t0_cyc = 15 * int(time.time() / 15)
429
+ cycle_start_str = time.strftime("%y%m%d_%H%M%S", time.gmtime(t0_cyc))
430
+ info = f"{cycle_start_str} ========================================"
431
+ self.send_output({'mtype':'rollover', 'info':info})
432
+ self.candidates = self.search(t0_cyc, cycle_start_str)
433
+ self.duplicate_filter = []
434
+ info = f"Search finished at {time.time() % 15:6.1f} with {len(self.candidates)} candidates"
435
+ self.send_output({'mtype':'test_info', 'info':info})
436
+ info = self.decode()
437
+ self.send_output({'mtype':'test_info', 'info':info})
438
+
439
+ def search(self, t0_cyc, cycle_start_str):
440
+ candidates = []
441
+ for f0_idx in range(int(100 / 3.125), self.audio_in.nFreqs - 8 * BPT, 1):
442
+ time.sleep(0)
443
+ freq_idxs = f0_idx + self.base_freq_idxs
444
+ p = self.audio_in.tfgrid[1, :, f0_idx:f0_idx+8*BPT]
445
+ new_origin = {'score':0}
446
+ for h0_idx in range(H0_RANGE[0], H0_RANGE[1]):
447
+ vals = p[h0_idx + self.hop_idxs_Costas + 36 * HPS, :].ravel()
448
+ sync_score = np.dot(vals, self.csync_flat) / np.max(vals)
449
+ test_origin = {'h0_idx':h0_idx, 'score':sync_score}
450
+ if test_origin['score'] > new_origin['score']:
451
+ new_origin = test_origin
452
+ if new_origin['score'] > MIN_SCORE:
453
+ freq_idxs = f0_idx + self.base_freq_idxs
454
+ hops = [(new_origin['h0_idx'] + h) % HPC for h in self.base_payload_hops]
455
+ p_idx = np.ix_(hops, freq_idxs)
456
+ tsec = new_origin['h0_idx'] / (SYM_RATE * HPS)
457
+ dt = tsec - 0.5
458
+ fHz = 3.125 * f0_idx
459
+ abs_t0 = t0_cyc + tsec
460
+ new_origin.update({'f0_idx': f0_idx, 'cs':cycle_start_str, 'p_idx':p_idx, 'fHz': fHz, 'dt': dt, 'abs_t0':abs_t0,
461
+ 'decode_info': None, 'decode_result':None, 'saved_llrs':[]})
462
+ candidates.append(new_origin)
463
+ candidates.sort(key = lambda c: -c['score'])
464
+ candidates = candidates[:MAX_CANDS]
465
+ candidates.sort(key = lambda c: int(c['h0_idx']))
466
+ return candidates
467
+
468
+ def decode(self):
469
+ n_cands = len(self.candidates)
470
+ self.last_attempt_stop = 0
471
+
472
+ def stop_decoding():
473
+ return self.latest_decode < time.time() % 15 < T_SEARCH
474
+
475
+ def wait_for_signal(cand, last_sym = 57):
476
+ t = time.time()
477
+ arriving = t < cand['abs_t0'] + self.base_payload_hops[last_sym] / (SYM_RATE * HPS)
478
+ overwritten = t > cand['abs_t0'] + 15 + 0.16*7
479
+ return arriving and not overwritten and not stop_decoding() and self.running
480
+
481
+ # good91 after message symbols arrive
482
+ for c in self.candidates:
483
+ if not stop_decoding():
484
+ while wait_for_signal(c, last_sym = 31):
485
+ time.sleep(0.05)
486
+ for itime in range(2):
487
+ p = self.audio_in.tfgrid[itime,:,:][c['p_idx']]
488
+ c['decode_result'], c['decode_info'] = decode_raw91(p, itime)
489
+ self.check_and_send(c)
490
+
491
+ # ldpc, saving input and output llrs ONLY if score < 1.2 (limits OSD load to typically-useful range)
492
+ for c in self.candidates:
493
+ if not stop_decoding():
494
+ if not c['decode_result']:
495
+ while wait_for_signal(c, last_sym = 57):
496
+ time.sleep(0.05)
497
+ for itime in range(2):
498
+ p = self.audio_in.tfgrid[itime,:,:][c['p_idx']]
499
+ c['decode_result'], c['decode_info'], llr_out, ch_llr = decode(p, itime)
500
+ if c['score'] < 1.2:
501
+ c['saved_llrs'].append((f"ch t={itime}", ch_llr))
502
+ if llr_out is not None:
503
+ c['saved_llrs'].append((f"op t={itime}", llr_out))
504
+ self.check_and_send(c)
505
+
506
+ # osd on all saved llrs
507
+ self.candidates.sort(key = lambda c: -c['score'])
508
+ for c in self.candidates:
509
+ if not stop_decoding():
510
+ if not c['decode_result']:
511
+ for llr in c['saved_llrs']:
512
+ c['decode_result'], c['decode_info'] = osd(llr)
513
+ self.check_and_send(c)
514
+ if not c['decode_result']:
515
+ c['decode_result'] = 'stop'
516
+
517
+ n_remaining_with_saved_llrs = len([c for c in self.candidates if any(c['saved_llrs']) and not c['decode_result']])
518
+ return (f"Last decode attempt {self.last_attempt_stop % 15:6.1f},"
519
+ + f" remaining_with_saved_llrs: {n_remaining_with_saved_llrs}")
520
+
521
+ def check_and_send(self, c):
522
+ self.last_attempt_stop = time.time()
523
+ if c['decode_result']:
524
+ if not c['decode_result'] in self.duplicate_filter:
525
+ p = self.audio_in.tfgrid[0,:,:][c['p_idx']]
526
+ c['decode_info'] = f" s={c['score']:5.2f} {c['decode_info']}"
527
+ their_snr = np.clip(int(np.max(p)-np.min(p)) - 58, -24, 24)
528
+ self.duplicate_filter.append(c['decode_result'])
529
+ self.send_output({'mtype':'decode', 'cyclestart_string': c['cs'], 't_decode':time.time(),
530
+ 'fHz':f"{c['fHz']:7.2f}", 'dt':f"{c['dt']:+04.2f}", 'decode_info':c['decode_info'],
531
+ 'their_snr':f"{their_snr:+03d}", 'msg_tuple':c['decode_result']})