PyFT8 3.7.4__tar.gz → 3.9.1__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.
- {pyft8-3.7.4 → pyft8-3.9.1}/PKG-INFO +25 -6
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8/databases.py +6 -5
- pyft8-3.9.1/PyFT8/decoders.py +246 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8/gui.py +47 -38
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8/pyft8.py +7 -5
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8/qso_manager.py +3 -5
- pyft8-3.9.1/PyFT8/receiver.py +429 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8/transmitter.py +41 -15
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8.egg-info/PKG-INFO +25 -6
- pyft8-3.9.1/PyFT8.egg-info/SOURCES.txt +20 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8.egg-info/requires.txt +1 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/README.md +23 -5
- {pyft8-3.7.4 → pyft8-3.9.1}/pyproject.toml +2 -1
- pyft8-3.7.4/MANIFEST.in +0 -2
- pyft8-3.7.4/PyFT8/osd.py +0 -88
- pyft8-3.7.4/PyFT8/receiver.py +0 -566
- pyft8-3.7.4/PyFT8.egg-info/SOURCES.txt +0 -36
- pyft8-3.7.4/tests/live test.py +0 -175
- pyft8-3.7.4/tests/misc/CQ AAAA.py +0 -30
- pyft8-3.7.4/tests/misc/animation.py +0 -36
- pyft8-3.7.4/tests/misc/bits_loopback.py +0 -204
- pyft8-3.7.4/tests/misc/bits_loopback_short.py +0 -169
- pyft8-3.7.4/tests/misc/osd.py +0 -89
- pyft8-3.7.4/tests/misc/symbols_loopback.py +0 -81
- pyft8-3.7.4/tests/misc/test_generate_wav.py +0 -58
- pyft8-3.7.4/tests/misc/test_loopback_performance.py +0 -220
- pyft8-3.7.4/tests/misc/text.py +0 -10
- pyft8-3.7.4/tests/misc/view_worked_before.py +0 -10
- pyft8-3.7.4/tests/pipeline/Fine tuning compare IOS_PC.py +0 -373
- pyft8-3.7.4/tests/pipeline/Targetted tests.py +0 -396
- pyft8-3.7.4/tests/pipeline/test_01.wav +0 -0
- pyft8-3.7.4/tests/plot_decode_times.py +0 -41
- {pyft8-3.7.4 → pyft8-3.9.1}/LICENSE +0 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8/__init__.py +0 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8/pskreporter.py +0 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8/rigctrl.py +0 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8/time_utils.py +0 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-3.7.4 → pyft8-3.9.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.9.1
|
|
4
4
|
Summary: FT8 Decoding and Encoding in Python with test/loopback code
|
|
5
5
|
Author-email: G1OJS <g1ojs@yahoo.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -13,6 +13,7 @@ Requires-Python: >=3.8
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: numpy>=1.24.0
|
|
16
|
+
Requires-Dist: scipy
|
|
16
17
|
Requires-Dist: matplotlib
|
|
17
18
|
Requires-Dist: pyaudio
|
|
18
19
|
Requires-Dist: pyserial
|
|
@@ -25,12 +26,11 @@ Dynamic: license-file
|
|
|
25
26
|
|
|
26
27
|
This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is very close (~90% of decodes) to that of WSJT-x 2.7.0 running in FAST mode.
|
|
27
28
|
|
|
28
|
-
PyFT8
|
|
29
|
-
|
|
30
|
-
If you're interested in how this works, maybe have a look at [MiniPyFT8](https://github.com/G1OJS/MiniPyFT8) which puts all of the receive code in a single 300 line Python file.
|
|
29
|
+
PyFT8 has a focus on demonstrating FT8 written in Python, but can be used as a standalone replacement for WSJT-x and other software.
|
|
31
30
|
|
|
32
31
|
## Features
|
|
33
32
|
- Rx and Tx of standard messages with optional /P and /R
|
|
33
|
+
- Tx uses GFSK waveform as defined in FT8 standard
|
|
34
34
|
- RX of nonstandard calls plus hashed calls
|
|
35
35
|
- Launches quickly (~2 seconds on my old Dell Optiplex 790)
|
|
36
36
|
- Use with or without gui (receive and send messages via command line commands)
|
|
@@ -112,9 +112,9 @@ upload = Y
|
|
|
112
112
|
|
|
113
113
|
## Performance Compared with WSJT-x
|
|
114
114
|
|
|
115
|
-
The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, using 5 minutes of busy 20m audio taken from the files used to test ft8_lib. On this dataset, WSJT-x performs almost the same on DEEP mode as it does on NORM, so the latter has been left off. Also, using AP with DEEP mode causes WSJT-x to be starved of CPU on my old PC (that's my theory), resulting in some cycles with hardly any decodes. PyFT8 gets about
|
|
115
|
+
The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, using 5 minutes of busy 20m audio taken from the files used to test ft8_lib. On this dataset, WSJT-x performs almost the same on DEEP mode as it does on NORM, so the latter has been left off. Also, using AP with DEEP mode causes WSJT-x to be starved of CPU on my old PC (that's my theory), resulting in some cycles with hardly any decodes. PyFT8 gets about 89% of the decodes that WSJT-x gets on FAST mode, and about 77% of those it gets on DEEP mode.
|
|
116
116
|
|
|
117
|
-
<img width="900" height="900" alt="
|
|
117
|
+
<img width="900" height="900" alt="decode_times" src="https://github.com/user-attachments/assets/6bdc3d97-5747-41d6-9c89-033f898102f2" />
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
## Decoding approach
|
|
@@ -166,6 +166,25 @@ and distributed under the GNU General Public License v3 (GPL-3.0), hence the use
|
|
|
166
166
|
|
|
167
167
|
Also thanks to [Robert Morris](https://github.com/rtmrtmrtmrtm) for [basicft8(*1)](https://github.com/rtmrtmrtmrtm/basicft8) - the first code I properly read when I was wondering whether to start this journey. (*1 note: applies to FT8 pre V2)
|
|
168
168
|
|
|
169
|
+
## My other FT8 repositories
|
|
170
|
+
|
|
171
|
+
If you're interested in how this works, maybe have a look at [MiniPyFT8](https://github.com/G1OJS/MiniPyFT8) which puts all of the receive code in a single 300 line Python file.
|
|
172
|
+
<html>
|
|
173
|
+
<table style = 'border: none; vertical-align:top;'>
|
|
174
|
+
<tr><td>
|
|
175
|
+
In October 2026 I started a new project which is a tidier version of PyFT8, called <a href='https://github.com/G1OJS/PyFT8m'>PyFT8m</a>, which has:
|
|
176
|
+
<ul>
|
|
177
|
+
<li>A Tkinter-based GUI</li>
|
|
178
|
+
<li>A separable receiver and transmitter that communicate with the GUI via UDP only</li>
|
|
179
|
+
</ul>
|
|
180
|
+
</td>
|
|
181
|
+
<td>
|
|
182
|
+
<img width="794" height="653" alt="673136630-e53ab21f-4e98-4dfa-b521-9fbeecc9917b" src="https://github.com/user-attachments/assets/50b3e35e-12bd-4b88-81a7-4318d821fedf" />
|
|
183
|
+
</td>
|
|
184
|
+
</tr>
|
|
185
|
+
</table>
|
|
186
|
+
</html>
|
|
187
|
+
|
|
169
188
|
## Useful resources:
|
|
170
189
|
**The QEX paper**
|
|
171
190
|
- [The FT4 and FT8 Communication Protocols](https://wsjt.sourceforge.io/FT4_FT8_QEX.pdf)
|
|
@@ -149,19 +149,20 @@ class History:
|
|
|
149
149
|
if data:
|
|
150
150
|
self._add_myspots_record(data, band, call, r['t'], r['rp'])
|
|
151
151
|
|
|
152
|
-
def process_message_for_history(self, message,
|
|
152
|
+
def process_message_for_history(self, message, myCall):
|
|
153
153
|
m = message
|
|
154
154
|
self._write_all_txt_row(m['all_txt_format'])
|
|
155
155
|
hail, their_call, grid_rpt = m['msg_tuple']
|
|
156
|
-
|
|
157
|
-
self._add_myspots_record(self.heard_by_me.data, current_band,
|
|
156
|
+
self._add_myspots_record(self.heard_by_me.data, m['band'],
|
|
158
157
|
their_call, int(time_utils.time()), m['their_snr'],
|
|
159
158
|
new_alert_data = self.heard_by_me_new)
|
|
160
159
|
if hail == myCall: # They're calling me, so they're hearing me
|
|
161
160
|
rpt = grid_rpt[-3:]
|
|
162
161
|
if rpt.isnumeric():
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
band = m['band']
|
|
163
|
+
if band:
|
|
164
|
+
self._add_myspots_record(self.hearing_me.data, band, their_call,
|
|
165
|
+
int(time_utils.time()), int(rpt), new_alert_data = self.heard_by_me_new)
|
|
165
166
|
|
|
166
167
|
|
|
167
168
|
# ----- internal storing / writing functions ----------------------------------------------------------
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from PyFT8.time_utils import time_utils
|
|
3
|
+
from PyFT8.databases import call_hashes, add_call_hashes
|
|
4
|
+
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"
|
|
5
|
+
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"
|
|
6
|
+
|
|
7
|
+
#=========== Unpacking functions ========================================
|
|
8
|
+
def get_bitfields(bits, lengths):
|
|
9
|
+
fields = []
|
|
10
|
+
for n in lengths:
|
|
11
|
+
mask = (1 << n) - 1
|
|
12
|
+
fields.append(bits & mask)
|
|
13
|
+
bits >>= n
|
|
14
|
+
return *fields, bits
|
|
15
|
+
|
|
16
|
+
def unpack(bits):
|
|
17
|
+
if not bits:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
i3, bits74 = get_bitfields(bits,[3])
|
|
21
|
+
if i3 == 0:
|
|
22
|
+
n3, bits71 = get_bitfields(bits74,[3])
|
|
23
|
+
if n3 <= 4:
|
|
24
|
+
#return (['Free text', 'DXpedition', 'Field Day', 'Field Day', 'Telemetry'][n3], 'not', 'implemented')
|
|
25
|
+
return None
|
|
26
|
+
else:
|
|
27
|
+
#return ('Unknown mode','not','implemented')
|
|
28
|
+
return None
|
|
29
|
+
elif i3 == 1 or i3 == 2: # 1 = Std Msg incl /R 2 = 'EU VHF' = Std Msg incl /P
|
|
30
|
+
return unpack_std(bits74, i3)
|
|
31
|
+
elif i3 == 3:
|
|
32
|
+
#return ('RTTY RU','not','implemented')
|
|
33
|
+
return None
|
|
34
|
+
elif i3 == 4:
|
|
35
|
+
cq, rrr, swp, c58, hsh, _ = get_bitfields(bits74, [1,2,1,58,12])
|
|
36
|
+
if cq and rrr or (not cq and not rrr):
|
|
37
|
+
return None
|
|
38
|
+
ca = "CQ" if cq else f"<{call_hashes.get((hsh,12), '...')}>"
|
|
39
|
+
cb = ""
|
|
40
|
+
for i in range(12):
|
|
41
|
+
cb = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"[c58 % 38] + cb
|
|
42
|
+
c58 = c58 // 38
|
|
43
|
+
cb = cb.strip()
|
|
44
|
+
add_call_hashes(cb)
|
|
45
|
+
(ca, cb) = (cb, ca) if swp else (ca, cb)
|
|
46
|
+
return (ca, cb, ('', 'RRR', 'RR73', '73')[rrr])
|
|
47
|
+
elif i3 == 5:
|
|
48
|
+
#return ('EU VHF','not','implemented')
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def unpack_std(bits74, i3):
|
|
52
|
+
g16, cb29, ca29, _ = get_bitfields(bits74,[16,29,29])
|
|
53
|
+
g15 = g16 & 0x7FFF
|
|
54
|
+
if g15 == 0:
|
|
55
|
+
return None
|
|
56
|
+
if g15 < 32400:
|
|
57
|
+
a, nn = divmod(g15, 1800)
|
|
58
|
+
b, nn = divmod(nn, 100)
|
|
59
|
+
c, d = divmod(nn, 10)
|
|
60
|
+
grid_rpt = chr(65+a) + chr(65+b) + str(c) + str(d)
|
|
61
|
+
elif g15 - 32400 <= 4:
|
|
62
|
+
grid_rpt = ('', '', 'RRR', 'RR73', '73')[g15 - 32400]
|
|
63
|
+
else:
|
|
64
|
+
prefix = 'R' if (g16 >> 15) else ''
|
|
65
|
+
grid_rpt = prefix + f"{(g15 - 32435):+03d}"
|
|
66
|
+
msg_tuple = (call_29(ca29, i3), call_29(cb29, i3), grid_rpt)
|
|
67
|
+
if not ('' in msg_tuple) and not (None in msg_tuple):
|
|
68
|
+
return msg_tuple
|
|
69
|
+
|
|
70
|
+
def call_29(call_int29, i3):
|
|
71
|
+
portable_rover = call_int29 & 1
|
|
72
|
+
call_int28 = call_int29>>1
|
|
73
|
+
if call_int28 < 3:
|
|
74
|
+
return ['DE', 'QRZ', 'CQ'][call_int28]
|
|
75
|
+
elif call_int28 < 1004:
|
|
76
|
+
return f"CQ {call_int28 - 3:03d}"
|
|
77
|
+
elif call_int28 < 21443:
|
|
78
|
+
x, txt = call_int28 - 1003, ''
|
|
79
|
+
for i in range(4):
|
|
80
|
+
txt = " ABCDEFGHIJKLMNOPQRSTUVWXYZ"[int(x % 27)] + txt
|
|
81
|
+
x //= 27
|
|
82
|
+
return f"CQ {txt.strip()}"
|
|
83
|
+
elif call_int28 < 2063592+4194303:
|
|
84
|
+
return f"<{call_hashes.get((call_int28 - 2063592, 22), '...')}>"
|
|
85
|
+
else:
|
|
86
|
+
call = standard_call28(call_int28, i3)
|
|
87
|
+
if call is not None:
|
|
88
|
+
if portable_rover:
|
|
89
|
+
call = call + ('/P' if i3 == 2 else '/R')
|
|
90
|
+
if call.endswith("/R") and not call[0] in ['A','K','N','W']:
|
|
91
|
+
return None
|
|
92
|
+
add_call_hashes(call)
|
|
93
|
+
return call
|
|
94
|
+
|
|
95
|
+
def standard_call28(call_int28, i3):
|
|
96
|
+
nn = call_int28 - (2063592 + 4194304)
|
|
97
|
+
from string import ascii_uppercase as ltrs, digits as digs
|
|
98
|
+
call_fields = [ (' ' + digs + ltrs, 36*10*27**3), (digs + ltrs, 10*27**3), (digs + ' ' * 17, 27**3),
|
|
99
|
+
(' ' + ltrs, 27**2), (' ' + ltrs, 27), (' ' + ltrs, 1) ]
|
|
100
|
+
chars = []
|
|
101
|
+
for alphabet, div in call_fields:
|
|
102
|
+
idx, nn = divmod(nn, div)
|
|
103
|
+
chars.append(alphabet[idx])
|
|
104
|
+
call = ''.join(chars).strip()
|
|
105
|
+
return simple_validate_call(call)
|
|
106
|
+
|
|
107
|
+
def simple_validate_call(call):
|
|
108
|
+
if not ' ' in call and len(call)>=3:
|
|
109
|
+
if call[0] in CALLSIGN_PREFIXES1 and call[1].isnumeric():
|
|
110
|
+
if not (call[0] in "B,F,G,I,K,M,N,R,W" and call[2].isnumeric()):
|
|
111
|
+
return call
|
|
112
|
+
if call[:2] in CALLSIGN_PREFIXES2 and call[2].isnumeric():
|
|
113
|
+
return call
|
|
114
|
+
|
|
115
|
+
def crc_unpack91(codeword91):
|
|
116
|
+
bits91_int = 0
|
|
117
|
+
for bit in (codeword91 > 0).astype(int).tolist():
|
|
118
|
+
bits91_int = (bits91_int << 1) | bit
|
|
119
|
+
bits77_int = bits91_int >> 14
|
|
120
|
+
msg = None
|
|
121
|
+
if(bits77_int > 0):
|
|
122
|
+
crc14_int = 0
|
|
123
|
+
for i in range(96):
|
|
124
|
+
inbit = ((bits77_int >> (76 - i)) & 1) if i < 77 else 0
|
|
125
|
+
bit14 = (crc14_int >> (14 - 1)) & 1
|
|
126
|
+
crc14_int = ((crc14_int << 1) & ((1 << 14) - 1)) | inbit
|
|
127
|
+
if bit14:
|
|
128
|
+
crc14_int ^= 0x2757
|
|
129
|
+
if crc14_int == bits91_int & 0b11111111111111:
|
|
130
|
+
msg = unpack(bits77_int)
|
|
131
|
+
return msg, bits77_int
|
|
132
|
+
|
|
133
|
+
#============== LDPC ===========================================================
|
|
134
|
+
CV6idx = np.array([[4,31,59,92,114,145],[5,23,60,93,121,150],[6,32,61,94,95,142],[5,31,63,96,125,137],[8,34,65,98,138,145],[9,35,66,99,106,125],[11,37,67,101,104,154],[12,38,68,102,148,161],[14,41,58,105,122,158],[0,32,71,105,106,156],[15,42,72,107,140,159],[10,43,74,109,120,165],[7,45,70,111,118,165],[18,37,76,103,115,162],[19,46,69,91,137,164],[1,47,73,112,127,159],[21,46,57,117,126,163],[15,38,61,111,133,157],[22,42,78,119,130,144],[19,35,62,93,135,160],[13,30,78,97,131,163],[2,43,79,123,126,168],[18,45,80,116,134,166],[11,49,60,117,118,143],[12,50,63,113,117,156],[23,51,75,128,147,148],[20,53,76,99,139,170],[34,81,132,141,170,173],[13,29,82,112,124,169],[3,28,67,119,133,172],[51,83,109,114,144,167],[6,49,80,98,131,172],[22,54,66,94,171,173],[25,40,76,108,140,147],[26,39,55,123,124,125],[17,48,54,123,140,166],[5,32,84,107,115,155],[8,53,62,130,146,154],[21,52,67,108,120,173],[2,12,47,77,94,122],[30,68,132,149,154,168],[4,38,74,101,135,166],[1,53,85,100,134,163],[14,55,86,107,118,170],[22,33,70,93,126,152],[10,48,87,91,141,156],[28,33,86,96,146,161],[21,56,84,92,139,158],[27,31,71,102,131,165],[0,25,44,79,127,146],[16,26,88,102,115,152],[50,56,97,162,164,171],[20,36,72,137,151,168],[15,46,75,129,136,153],[2,23,29,71,103,138],[8,39,89,105,133,150],[17,41,78,143,145,151],[24,37,64,98,121,159],[16,41,74,128,169,171]], dtype = np.int16)
|
|
135
|
+
CV7idx = np.array([[3,30,58,90,91,95,152],[7,24,62,82,92,95,147],[4,33,64,77,97,106,153],[10,36,66,86,100,138,157],[7,39,69,81,103,113,144],[13,40,70,87,101,122,155],[16,36,73,80,108,130,153],[44,54,63,110,129,160,172],[17,35,75,88,112,113,142],[20,44,77,82,116,120,150],[18,34,58,72,109,124,160],[6,48,57,89,99,104,167],[24,52,68,89,100,129,155],[19,45,64,79,119,139,169],[0,3,51,56,85,135,151],[25,50,55,90,121,136,167],[1,26,40,60,61,114,132],[27,47,69,84,104,128,157],[11,42,65,88,96,134,158],[9,43,81,90,110,143,148],[29,49,59,85,136,141,161],[9,52,65,83,111,127,164],[27,28,83,87,116,142,149],[14,57,59,73,110,149,162]], dtype = np.int16)
|
|
136
|
+
|
|
137
|
+
import warnings
|
|
138
|
+
warnings.filterwarnings("error")
|
|
139
|
+
|
|
140
|
+
def pass_ldpc_messages(llr, CVidx, mC2V_prev, update_collector):
|
|
141
|
+
mV2C = llr[CVidx] - mC2V_prev
|
|
142
|
+
tanh_mV2C = np.tanh(-mV2C)
|
|
143
|
+
tanh_mC2V = np.prod(tanh_mV2C, axis=1, keepdims=True)
|
|
144
|
+
orig_err = np.geterr()
|
|
145
|
+
np.seterr(all = 'ignore')
|
|
146
|
+
tanh_mC2V = np.divide(tanh_mC2V, tanh_mV2C)
|
|
147
|
+
np.seterr(**orig_err)
|
|
148
|
+
alpha_atanh_approx = 1.18
|
|
149
|
+
mC2V_curr = tanh_mC2V / ((tanh_mC2V - alpha_atanh_approx) * (alpha_atanh_approx + tanh_mC2V))
|
|
150
|
+
np.add.at(update_collector, CVidx, mC2V_curr - mC2V_prev)
|
|
151
|
+
return mC2V_curr
|
|
152
|
+
|
|
153
|
+
def ldpc_decode(llr, max_ncheck0, max_iters):
|
|
154
|
+
mC2V_prev6, mC2V_prev7 = np.zeros(CV6idx.shape, dtype=np.float32), np.zeros(CV7idx.shape, dtype=np.float32)
|
|
155
|
+
for n_its in range(max_iters):
|
|
156
|
+
bits6, bits7 = llr[CV6idx] > 0, llr[CV7idx] > 0
|
|
157
|
+
parity6, parity7 = np.sum(bits6, axis=1) & 1, np.sum(bits7, axis=1) & 1
|
|
158
|
+
ncheck = int(np.sum(parity7) + np.sum(parity6))
|
|
159
|
+
if n_its == 0 and ncheck > max_ncheck0:
|
|
160
|
+
return None, -1, []
|
|
161
|
+
if ncheck == 0:
|
|
162
|
+
msg_tuple, bits77_int = crc_unpack91(llr[:91])
|
|
163
|
+
if msg_tuple:
|
|
164
|
+
return msg_tuple, n_its, bits77_int
|
|
165
|
+
else:
|
|
166
|
+
update_collector = np.zeros_like(llr)
|
|
167
|
+
mC2V_prev6 = pass_ldpc_messages(llr, CV6idx, mC2V_prev6, update_collector)
|
|
168
|
+
mC2V_prev7 = pass_ldpc_messages(llr, CV7idx, mC2V_prev7, update_collector)
|
|
169
|
+
llr += update_collector
|
|
170
|
+
|
|
171
|
+
return None, -1, llr
|
|
172
|
+
|
|
173
|
+
#============== OSD ===========================================================
|
|
174
|
+
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"]
|
|
175
|
+
kGEN = np.array([int(row,16)>>1 for row in generator_matrix_rows])
|
|
176
|
+
A = np.zeros((83, 91), dtype=np.uint8)
|
|
177
|
+
for i, row in enumerate(kGEN):
|
|
178
|
+
for j in range(91):
|
|
179
|
+
A[i, 90 - j] = (row >> j) & 1
|
|
180
|
+
G0 = np.concatenate([np.eye(91, dtype=np.uint8), A.T],axis=1)
|
|
181
|
+
|
|
182
|
+
def osd(llr):
|
|
183
|
+
chbits174 = (llr>0).astype(np.uint8)
|
|
184
|
+
chvals174 = np.abs(llr)
|
|
185
|
+
|
|
186
|
+
rowperm = np.arange(91)
|
|
187
|
+
colperm = np.argsort(-np.abs(llr))
|
|
188
|
+
curr_row = 0
|
|
189
|
+
G = G0.copy()
|
|
190
|
+
for curr_col in range(174):
|
|
191
|
+
ones_below = np.where(G[rowperm[curr_row:], colperm[curr_col]] == 1)[0]
|
|
192
|
+
if ones_below.size > 0:
|
|
193
|
+
swap_row = curr_row + ones_below[0]
|
|
194
|
+
rowperm[[curr_row, swap_row]] = rowperm[[swap_row, curr_row]]
|
|
195
|
+
r_curr = rowperm[curr_row]
|
|
196
|
+
c_curr = colperm[curr_col]
|
|
197
|
+
g_c_curr = G[:, c_curr].copy()
|
|
198
|
+
g_c_curr[r_curr] = 0
|
|
199
|
+
rows_to_xor = np.where(g_c_curr == 1)[0]
|
|
200
|
+
G[rows_to_xor, :] ^= G[r_curr, :]
|
|
201
|
+
colperm[[curr_row, curr_col]] = colperm[[curr_col, curr_row]]
|
|
202
|
+
curr_row += 1
|
|
203
|
+
if curr_row > 90:
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
chbits91 = chbits174[colperm][:91]
|
|
207
|
+
chbits91[rowperm] = chbits91
|
|
208
|
+
|
|
209
|
+
base_cw = ((chbits91 @ G) & 1)
|
|
210
|
+
msg_tuple, bits77_int = crc_unpack91(base_cw[:91])
|
|
211
|
+
if msg_tuple:
|
|
212
|
+
return msg_tuple, bits77_int
|
|
213
|
+
|
|
214
|
+
fliplist = rowperm[::-1]
|
|
215
|
+
current_best_distance = 1e20
|
|
216
|
+
cw_out91 = None
|
|
217
|
+
for i in range(91):
|
|
218
|
+
# Single flip
|
|
219
|
+
cw = base_cw ^ G[fliplist[i]]
|
|
220
|
+
# Double flips with every j < i
|
|
221
|
+
if i:
|
|
222
|
+
cw2 = base_cw ^ G[fliplist[i]] ^ G[fliplist[:i]]
|
|
223
|
+
candidates = np.vstack((cw, cw2))
|
|
224
|
+
else:
|
|
225
|
+
candidates = cw[None, :]
|
|
226
|
+
|
|
227
|
+
# np.dot maybe?
|
|
228
|
+
distances = np.sum(np.abs(llr)[None, :] * (candidates != chbits174), axis=1)
|
|
229
|
+
best_idx = np.argmin(distances)
|
|
230
|
+
if distances[best_idx] < current_best_distance:
|
|
231
|
+
current_best_distance = distances[best_idx]
|
|
232
|
+
cw_out91 = candidates[best_idx, :91].copy()
|
|
233
|
+
|
|
234
|
+
if cw_out91 is not None:
|
|
235
|
+
msg_tuple, bits77_int = crc_unpack91(cw_out91)
|
|
236
|
+
if msg_tuple:
|
|
237
|
+
return msg_tuple, bits77_int
|
|
238
|
+
|
|
239
|
+
return None, None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
@@ -132,11 +132,12 @@ class Panel:
|
|
|
132
132
|
|
|
133
133
|
class Gui:
|
|
134
134
|
def __init__(self, myCall, myGrid, console_print, qso_manager, history,
|
|
135
|
-
band_frequencies, set_receiver_band, waterfall_data, hearing_me_since_mins, geo_units):
|
|
135
|
+
band_frequencies, set_receiver_band, waterfall_data, hearing_me_since_mins, geo_units, nodisplay = False):
|
|
136
136
|
self.hearing_me_since_mins = hearing_me_since_mins
|
|
137
137
|
self.waterfall_data = waterfall_data
|
|
138
138
|
self.qso_manager = qso_manager
|
|
139
139
|
self.history = history
|
|
140
|
+
self.nodisplay = nodisplay
|
|
140
141
|
self.set_receiver_band = set_receiver_band
|
|
141
142
|
|
|
142
143
|
self.myCall, self.myGrid = myCall, myGrid
|
|
@@ -187,11 +188,11 @@ class Gui:
|
|
|
187
188
|
threading.Thread(target = self._slow_loop, args = (testing,), daemon = True).start()
|
|
188
189
|
self._fast_loop()
|
|
189
190
|
|
|
190
|
-
def _fast_loop(self):
|
|
191
|
+
def _fast_loop(self, target_update_time = 0.15):
|
|
191
192
|
self.plt.show(block = False)
|
|
192
193
|
while True:
|
|
193
|
-
self.fig.canvas.flush_events() # for clicks
|
|
194
194
|
tstart_wf_redraw = time_utils.time()
|
|
195
|
+
self.fig.canvas.flush_events() # for clicks
|
|
195
196
|
if self.needs_redraw:
|
|
196
197
|
self.needs_redraw = False
|
|
197
198
|
plt.pause(0.01)
|
|
@@ -205,40 +206,35 @@ class Gui:
|
|
|
205
206
|
self.fig.canvas.flush_events() # for clicks
|
|
206
207
|
self.fig.canvas.blit(self.ax_wf.bbox) #self.fig.canvas.update()
|
|
207
208
|
self.fig.canvas.flush_events()
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if ct < self.cycle_time_prev:
|
|
213
|
-
self.new_cycle = True
|
|
214
|
-
self._refresh_home_panel()
|
|
215
|
-
self._refresh_band_buttons()
|
|
216
|
-
self._refresh_hearing()
|
|
217
|
-
self.needs_redraw = True
|
|
218
|
-
self.cycle_time_prev = ct
|
|
219
|
-
if ct > 10 and self.new_cycle:
|
|
220
|
-
for mb in self.msg_boxes:
|
|
221
|
-
mb.hide()
|
|
222
|
-
self.needs_redraw = True
|
|
223
|
-
self.new_cycle = False
|
|
224
|
-
tnow = time_utils.time()
|
|
225
|
-
tdelay = target_update_time - (tnow - tstart_wf_redraw)
|
|
226
|
-
if tdelay > 0.01:
|
|
227
|
-
time_utils.sleep(tdelay)
|
|
209
|
+
tnow = time_utils.time()
|
|
210
|
+
tdelay = target_update_time - (tnow - tstart_wf_redraw)
|
|
211
|
+
if tdelay > 0.01:
|
|
212
|
+
time_utils.sleep(tdelay)
|
|
228
213
|
|
|
229
214
|
def _slow_loop(self, testing):
|
|
230
215
|
while True:
|
|
231
216
|
time_utils.sleep(0.25)
|
|
217
|
+
|
|
218
|
+
ct = time_utils.cycle_time()
|
|
219
|
+
if ct < self.cycle_time_prev:
|
|
220
|
+
self._refresh_panels()
|
|
221
|
+
self.new_cycle = True
|
|
222
|
+
self.cycle_time_prev = ct
|
|
223
|
+
|
|
224
|
+
if ct > 10 and self.new_cycle:
|
|
225
|
+
self._hide_msg_boxes()
|
|
226
|
+
self.new_cycle = False
|
|
227
|
+
|
|
232
228
|
while not self.message_queue_non_time_critical.empty():
|
|
233
|
-
time_utils.sleep(0.
|
|
229
|
+
time_utils.sleep(0.05)
|
|
234
230
|
m = self.message_queue_non_time_critical.get()
|
|
235
231
|
if not testing and m['msg_tuple'][1] != 'not':
|
|
236
|
-
self.history.process_message_for_history(m, self.
|
|
232
|
+
self.history.process_message_for_history(m, self.myCall)
|
|
237
233
|
|
|
238
234
|
if not m['priority']:
|
|
239
235
|
self._display_message(m)
|
|
240
236
|
|
|
241
|
-
if m['message_type'] == 'CQ':
|
|
237
|
+
if m['message_type'] == 'CQ' and self.history:
|
|
242
238
|
for mb in self.msg_boxes:
|
|
243
239
|
if mb.patch.get_visible():
|
|
244
240
|
if mb.message['msg_tuple'] == m['msg_tuple']:
|
|
@@ -251,7 +247,17 @@ class Gui:
|
|
|
251
247
|
new_text = f"{' '.join(m['msg_tuple'])} {hearing_me} {wb_text} {geo_text}"
|
|
252
248
|
mb.set_text(new_text)
|
|
253
249
|
|
|
250
|
+
def _refresh_panels(self):
|
|
251
|
+
self._refresh_home_panel()
|
|
252
|
+
self._refresh_band_buttons()
|
|
253
|
+
self._refresh_hearing()
|
|
254
|
+
self.needs_redraw = True
|
|
254
255
|
|
|
256
|
+
def _hide_msg_boxes(self):
|
|
257
|
+
for mb in self.msg_boxes:
|
|
258
|
+
mb.hide()
|
|
259
|
+
self.needs_redraw = True
|
|
260
|
+
|
|
255
261
|
def process_message(self, m):
|
|
256
262
|
hail, their_call, _ = m['msg_tuple']
|
|
257
263
|
message_type_val = 0 + 1*(their_call == self.myCall) + 2*(hail == self.myCall) + 3*(their_call != self.myCall and hail.startswith('CQ'))
|
|
@@ -260,14 +266,16 @@ class Gui:
|
|
|
260
266
|
m.update( {'message_type':message_type, 'priority':priority} )
|
|
261
267
|
if priority:
|
|
262
268
|
self._display_message(m)
|
|
263
|
-
self.qso_manager
|
|
269
|
+
if self.qso_manager:
|
|
270
|
+
self.qso_manager.process_message(m)
|
|
264
271
|
self.message_queue_non_time_critical.put(m)
|
|
265
272
|
|
|
266
273
|
def _display_message(self, m):
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
274
|
+
if not self.nodisplay:
|
|
275
|
+
mb = self._get_message_box()
|
|
276
|
+
x = int(m['tsec'] / self.waterfall_data['dt'] + m['their_tx_cycle'] * self.waterfall_data['pixels_per_cycle'])
|
|
277
|
+
y = int(m['fHz'] / self.waterfall_data['df'])
|
|
278
|
+
mb.set_props(x, y, m)
|
|
271
279
|
|
|
272
280
|
def set_bandstats_title(self, txt):
|
|
273
281
|
self.home_panel.ax.set_title(txt, fontsize = 10)
|
|
@@ -329,13 +337,14 @@ class Gui:
|
|
|
329
337
|
self.home_panel.print_row(f"{n_spotted:<7} {rx_lead[1]:<7}", 4, color = '#b6f0c6' )
|
|
330
338
|
|
|
331
339
|
def _refresh_band_buttons(self):
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
bb
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
340
|
+
if self.history is not None:
|
|
341
|
+
current_band = self.band_info['current_band']
|
|
342
|
+
grd = self.myGrid[:4]
|
|
343
|
+
for bb in self.button_boxes:
|
|
344
|
+
bb.set_state(bb.id == current_band)
|
|
345
|
+
if bb.id in self.history.home_activity:
|
|
346
|
+
cnts = self.history.home_activity[bb.id]
|
|
347
|
+
bb.set_info_text(f"{cnts[0]}Tx, {cnts[1]}Rx")
|
|
339
348
|
|
|
340
349
|
def _refresh_hearing(self):
|
|
341
350
|
current_band = self.band_info['current_band']
|
|
@@ -10,7 +10,7 @@ from PyFT8.databases import History
|
|
|
10
10
|
from PyFT8.qso_manager import QSO_manager
|
|
11
11
|
from PyFT8.databases import History
|
|
12
12
|
|
|
13
|
-
VER = '3.
|
|
13
|
+
VER = '3.9.0'
|
|
14
14
|
PSKR_REFRESH_MINS = 20
|
|
15
15
|
|
|
16
16
|
def get_config(config_folder):
|
|
@@ -39,6 +39,7 @@ def get_config(config_folder):
|
|
|
39
39
|
return config
|
|
40
40
|
|
|
41
41
|
def cli():
|
|
42
|
+
n_decodes = 0
|
|
42
43
|
|
|
43
44
|
def console_print(text, color = 'white'):
|
|
44
45
|
text = f"{time_utils.cycle_time():4.1f} {text}"
|
|
@@ -48,6 +49,7 @@ def cli():
|
|
|
48
49
|
print(text)
|
|
49
50
|
|
|
50
51
|
def process_message(m):
|
|
52
|
+
nonlocal n_decodes
|
|
51
53
|
if gui:
|
|
52
54
|
gui.process_message(m)
|
|
53
55
|
|
|
@@ -58,8 +60,8 @@ def cli():
|
|
|
58
60
|
fHz = float(m['fHz'])
|
|
59
61
|
pskr_upload.add_report(their_call, int(1000000*fMHz + fHz),
|
|
60
62
|
m['their_snr'], 'FT8', 1, int(time_utils.time()))
|
|
61
|
-
|
|
62
|
-
print(f"{m['all_txt_format']:50s} decoded@ {m['decode_completed']%15 :5.1f}s, dec = {m['
|
|
63
|
+
n_decodes += 1
|
|
64
|
+
print(f"{n_decodes:4d} {m['all_txt_format']:50s} decoded@ {m['decode_completed']%15 :5.1f}s, dec = {m['decode_notes']}")
|
|
63
65
|
|
|
64
66
|
parser = argparse.ArgumentParser(prog='PyFT8rx', description = 'Command Line FT8 decoder')
|
|
65
67
|
parser.add_argument('-c', '--config_folder', help = 'Location of config folder e.g. C:/Users/drala/Documents/Projects/GitHub/G1OJS/PyFT8_cfg', default = './')
|
|
@@ -132,8 +134,8 @@ def cli():
|
|
|
132
134
|
pskr_upload = PSKR_upload(myCall, myGrid, software = f"PyFT8 v{VER}", console_print = console_print)
|
|
133
135
|
|
|
134
136
|
# Set up for receiving with or without Gui
|
|
135
|
-
receiver = Receiver(args.inputcard_keywords, process_message, sync_score_min =
|
|
136
|
-
search_freq_range = [100, 3000], search_timerange = [-
|
|
137
|
+
receiver = Receiver(args.inputcard_keywords, process_message, sync_score_min = 100, max_cands = 150,
|
|
138
|
+
search_freq_range = [100, 3000], search_timerange = [-2, 3])
|
|
137
139
|
if not receiver.audio_in.input_device_idx:
|
|
138
140
|
time_utils.tlog(f"[Audio] No input audio device found matching {input_device_keywords}", verbose = True)
|
|
139
141
|
sys.exit(1)
|
|
@@ -34,13 +34,10 @@ class QSO_manager:
|
|
|
34
34
|
history.add_bidirectional_hearing_heard_spot(band, their_call, time_on)
|
|
35
35
|
|
|
36
36
|
def _find_clear_freq(self, fmax):
|
|
37
|
-
from numpy.lib.stride_tricks import sliding_window_view
|
|
38
37
|
import numpy as np
|
|
39
|
-
fbin_sum = np.sum(self.waterfall_data['data'], axis =
|
|
40
|
-
windows = sliding_window_view(fbin_sum, self.waterfall_data['sig_h'])
|
|
41
|
-
busy_profile = windows.max(axis=1)
|
|
38
|
+
fbin_sum = np.sum(self.waterfall_data['data'], axis = 1)
|
|
42
39
|
f0_idx, fn_idx = int(500/self.waterfall_data['df']), int(fmax/self.waterfall_data['df'])
|
|
43
|
-
idx = np.argmin(
|
|
40
|
+
idx = np.argmin(fbin_sum[f0_idx:fn_idx])
|
|
44
41
|
clearest_frequency = (f0_idx + idx) * self.waterfall_data['df']
|
|
45
42
|
return clearest_frequency
|
|
46
43
|
|
|
@@ -122,6 +119,7 @@ class QSO_manager:
|
|
|
122
119
|
message_type = m['message_type']
|
|
123
120
|
if message_type in ['CQ','to_me'] and m['band'] == self.band_info['current_band']:
|
|
124
121
|
_ , their_call, grid_rpt = m['msg_tuple']
|
|
122
|
+
their_call = their_call.replace('<','').replace('>','')
|
|
125
123
|
their_snr = m['their_snr']
|
|
126
124
|
their_tx_cycle = m['their_tx_cycle']
|
|
127
125
|
if their_call != self.in_qso_with:
|