PyFT8 2.8.0__tar.gz → 2.10.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.
- {pyft8-2.8.0 → pyft8-2.10.0}/PKG-INFO +39 -2
- pyft8-2.10.0/PyFT8/databases.py +311 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/gui.py +4 -4
- pyft8-2.8.0/PyFT8/pskr_upload.py → pyft8-2.10.0/PyFT8/pskreporter.py +30 -5
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/pyft8.py +74 -138
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/receiver.py +1 -1
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/rigctrl.py +33 -3
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/transmitter.py +1 -1
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/PKG-INFO +39 -2
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/SOURCES.txt +2 -5
- {pyft8-2.8.0 → pyft8-2.10.0}/README.md +38 -1
- {pyft8-2.8.0 → pyft8-2.10.0}/pyproject.toml +1 -1
- pyft8-2.8.0/PyFT8/callhashes.py +0 -19
- pyft8-2.8.0/PyFT8/hamlib.py +0 -36
- pyft8-2.8.0/PyFT8/maidenhead.py +0 -32
- pyft8-2.8.0/PyFT8/mqtt.py +0 -141
- {pyft8-2.8.0 → pyft8-2.10.0}/LICENSE +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/MANIFEST.in +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/__init__.py +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/time_utils.py +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/setup.cfg +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/tests/dev/CQ AAAA.py +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/tests/dev/osd.py +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/tests/dev/test_generate_wav.py +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/tests/dev/test_loopback_performance.py +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/tests/dev/view_worked_before.py +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/tests/plot_baseline.py +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/tests/spare.py +0 -0
- {pyft8-2.8.0 → pyft8-2.10.0}/tests/test_batch_and_live.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.10.0
|
|
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
|
|
@@ -43,6 +43,7 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
|
|
|
43
43
|
- Messages overlaid on waterfall signals that produce them
|
|
44
44
|
- Worked-before info and fine grid locators / distance and bearing in the message boxes
|
|
45
45
|
- List of stations hearing your transmissions on the selected band
|
|
46
|
+
- Marker on message if 2nd call is hearing you
|
|
46
47
|
- Band activity in your level 4 square live updated next to band select buttons
|
|
47
48
|
- Number of remote stations hearing your Tx, number of remote Txs that you're hearing, plus the same info for the 'best' station in your level 4 square
|
|
48
49
|
- Data used for the above is cached to disk so is not lost when restarting the program
|
|
@@ -79,12 +80,48 @@ Once installed, you can use the following commands to run it. Otherwise, please
|
|
|
79
80
|
| Launch configured GUI|pyft8 -i "Keyword1, Keyword2" -o "Keyword1, Keyword2" -c {config folder}| Config folder stores PyFT8.ini (your callsign, grid, buttons) and PyFT8.adi log file. Run this once to create default PyFT8.ini file.|
|
|
80
81
|
|
|
81
82
|
### Rig control
|
|
82
|
-
I've included the Python code that I use with my Icom IC-7100 in the file 'rigctrl.py', and believe I've moved sufficient 'specification' for the rig protocol into the .ini file so that you can paste in your own rig specification (see for e.g. the Omnirig .ini file for your rig) and get it working with PyFT8 controlling PTT and frequency. I designed this code to drop the serial connection when it's not required, so that the rig's serial port can be accessed by other software at the same time.
|
|
83
|
+
I've included the Python code that I use with my Icom IC-7100 in the file 'rigctrl.py', and believe I've moved sufficient 'specification' for the rig protocol into the .ini file so that you can paste in your own rig specification (see for e.g. the Omnirig .ini file for your rig) and get it working with PyFT8 controlling PTT and frequency. I designed this code to drop the serial connection when it's not required, so that the rig's serial port can be accessed by other software at the same time (assuming that the other software returns the favour and doesn't permanently hog the serial port).
|
|
83
84
|
|
|
84
85
|
I've also included a basic Hamlib interface which launches rigctld and uses that to control the rig. To use this, make sure that the hamlib section of the ini file is populated; this will then take precedence over the direct CAT control section.
|
|
85
86
|
|
|
86
87
|
Alternatively, you can run PyFT8 without rig control; if there is no rig found, PyFT8 defaults to running without a rig connected. In this case, you need to provide your own PTT method and note that the band buttons will only set the information used for logging QSOs to the PyFT8.adi file. Or you can use PyFT8 as Rx-only.
|
|
87
88
|
|
|
89
|
+
## Customising
|
|
90
|
+
Even if you don't want to edit the code, there's quite a bit that you can change in the .ini file. I haven't written a user guide yet, so here's my own .ini file for reference. I use CAT control so my hamlib block is disabled by adding a _ to the name of the block (I keep it there for testing only). I need to add some more switches really to catch up with recent additions.
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
[station]
|
|
94
|
+
call = G1OJS
|
|
95
|
+
grid = IO90******
|
|
96
|
+
[gui]
|
|
97
|
+
loc = km_deg
|
|
98
|
+
wb = Y
|
|
99
|
+
[rig]
|
|
100
|
+
port = COM4
|
|
101
|
+
baud_rate = 9600
|
|
102
|
+
set_freq_command = FEFE88E0.05.0000000000.FD
|
|
103
|
+
set_freq_value = 5|5|vfBcdLU|1|0
|
|
104
|
+
ptt_on_command = FEFE88E0.1C00.01.FD
|
|
105
|
+
ptt_off_command = FEFE88E0.1C00.00.FD
|
|
106
|
+
[_hamlib_rig]
|
|
107
|
+
rigctld = C:/WSJT/wsjtx/bin/rigctld-wsjtx
|
|
108
|
+
port = COM4
|
|
109
|
+
baud_rate = 9600
|
|
110
|
+
model = 3070
|
|
111
|
+
[pskreporter]
|
|
112
|
+
upload = Y
|
|
113
|
+
[bands]
|
|
114
|
+
160m = 1.840
|
|
115
|
+
80m = 3.573
|
|
116
|
+
60m = 5.357
|
|
117
|
+
40m = 7.074
|
|
118
|
+
20m = 14.074
|
|
119
|
+
15m = 21.074
|
|
120
|
+
10m = 28.074
|
|
121
|
+
6m = 50.313
|
|
122
|
+
2m = 144.174
|
|
123
|
+
```
|
|
124
|
+
|
|
88
125
|
## Performance Compared with FT8_lib and WSJT-x
|
|
89
126
|
|
|
90
127
|
The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, and FT8_lib, using the same 10 minutes of busy 20m audio that is used to test ft8_lib.
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
from PyFT8.pskreporter import PSKR_MQTT_listener
|
|
2
|
+
import threading, time, os, pickle
|
|
3
|
+
|
|
4
|
+
call_hashes = {}
|
|
5
|
+
def add_call_hashes(call):
|
|
6
|
+
global call_hashes
|
|
7
|
+
chars = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"
|
|
8
|
+
call_padded = (call + " ")[:11]
|
|
9
|
+
hashes = []
|
|
10
|
+
for m in [10,12,22]:
|
|
11
|
+
x = 0
|
|
12
|
+
for c in call_padded:
|
|
13
|
+
x = 38*x + chars.find(c)
|
|
14
|
+
x = x & ((int(1) << 64) - 1)
|
|
15
|
+
x = x & ((1 << 64) - 1)
|
|
16
|
+
x = x * 47055833459
|
|
17
|
+
x = x & ((1 << 64) - 1)
|
|
18
|
+
x = x >> (64 - m)
|
|
19
|
+
hashes.append(x)
|
|
20
|
+
call_hashes[(x, m)] = call
|
|
21
|
+
return hashes
|
|
22
|
+
|
|
23
|
+
def grid_to_latlong(grid, centre = True):
|
|
24
|
+
lat, lon = -90, -180
|
|
25
|
+
grid = grid.upper()
|
|
26
|
+
if centre:
|
|
27
|
+
grid = grid + "LL44LL44LL44"[len(grid):]
|
|
28
|
+
mults = [20, 2, 2/24, 0.2/24, 0.2/(24*24), 0.02/(24*24)]
|
|
29
|
+
grid = grid[:2*len(mults)]
|
|
30
|
+
pairs = [grid[i:i+2] for i in range(0,len(grid),2)]
|
|
31
|
+
for i, p in enumerate(pairs):
|
|
32
|
+
zero = [ord('A'),ord('0')][i % 2]
|
|
33
|
+
lon += mults[i] * (ord(p[0]) - zero)
|
|
34
|
+
lat += mults[i] * (ord(p[1]) - zero) / 2
|
|
35
|
+
return (lat, lon)
|
|
36
|
+
|
|
37
|
+
def grids_to_dist_brg(sq1, sq2, units):
|
|
38
|
+
from numpy import sin, cos, asin, atan2, sqrt, radians, degrees
|
|
39
|
+
ll1, ll2 = grid_to_latlong(sq1), grid_to_latlong(sq2)
|
|
40
|
+
lats = [radians(ll1[0]), radians(ll2[0])]
|
|
41
|
+
dlat, dlon = radians(ll2[0] - ll1[0]), radians(ll2[1] - ll1[1])
|
|
42
|
+
s_lats, c_lats = sin(lats), cos(lats)
|
|
43
|
+
a = sin(dlat/2)**2 + c_lats[0] * c_lats[1] * sin(dlon/2)**2
|
|
44
|
+
r = 6371 * 2 * asin(sqrt(a))
|
|
45
|
+
b = atan2(c_lats[1] * sin(dlon), c_lats[0] * s_lats[1] - s_lats[0] * c_lats[1] * cos(dlon))
|
|
46
|
+
b *= (1.0 if 'km' in units else 0.621371)
|
|
47
|
+
return (r, degrees(b) % 360)
|
|
48
|
+
|
|
49
|
+
class DiskDict:
|
|
50
|
+
def __init__(self, file):
|
|
51
|
+
self.lock = threading.Lock()
|
|
52
|
+
self.file = file
|
|
53
|
+
self.data = {}
|
|
54
|
+
self.load()
|
|
55
|
+
threading.Thread(target = self._autosave, daemon = True).start()
|
|
56
|
+
|
|
57
|
+
def _autosave(self, autosave_period = 15):
|
|
58
|
+
while True:
|
|
59
|
+
time.sleep(autosave_period)
|
|
60
|
+
self.save()
|
|
61
|
+
|
|
62
|
+
def load(self):
|
|
63
|
+
with self.lock:
|
|
64
|
+
if(os.path.exists(self.file)):
|
|
65
|
+
with open(f"{self.file}","rb") as f:
|
|
66
|
+
self.data = pickle.load(f)
|
|
67
|
+
|
|
68
|
+
def save(self):
|
|
69
|
+
with self.lock:
|
|
70
|
+
tmp_file = f"{self.file}.tmp"
|
|
71
|
+
with open(tmp_file, "wb") as f:
|
|
72
|
+
pickle.dump(self.data, f)
|
|
73
|
+
f.flush()
|
|
74
|
+
os.fsync(f.fileno())
|
|
75
|
+
os.replace(tmp_file, self.file)
|
|
76
|
+
|
|
77
|
+
class History:
|
|
78
|
+
def __init__(self, config_folder, my_call, home_square, pskr_refresh_mins, parse_all_file):
|
|
79
|
+
self.pskr_refresh_mins = pskr_refresh_mins
|
|
80
|
+
self.config_folder = config_folder
|
|
81
|
+
self.my_call = my_call
|
|
82
|
+
self.home_square = home_square
|
|
83
|
+
self.home_square_lev4 = home_square[:4]
|
|
84
|
+
self.dist_brg_cache = {}
|
|
85
|
+
self.hearing_me = DiskDict(f"{self.config_folder}/hearing_me.pkl") # all-time record of hearing me
|
|
86
|
+
self.heard_by_me = DiskDict(f"{self.config_folder}/heard_by_me.pkl") # all-time record of heard by me
|
|
87
|
+
self.hearing_me_new = {}
|
|
88
|
+
self.heard_by_me_new = {}
|
|
89
|
+
self.call_to_grid = DiskDict(f"{self.config_folder}/call_to_grid.pkl") # all time cache call -> fine locator
|
|
90
|
+
self.band_TxRx_homecall_report_times = DiskDict(f"{self.config_folder}/report_times.pkl") # last 20 mins data -> per band tx/rx & current band detail
|
|
91
|
+
self.home_activity = {}
|
|
92
|
+
self.home_most_remotes = {}
|
|
93
|
+
self.lock = threading.Lock()
|
|
94
|
+
if parse_all_file:
|
|
95
|
+
self.load_all_file(f"{self.config_folder}/ALL.txt")
|
|
96
|
+
self.hearing_me.save()
|
|
97
|
+
self.heard_by_me.save()
|
|
98
|
+
print("All file parsed and saved to hearing_me / heard_by_me files")
|
|
99
|
+
else:
|
|
100
|
+
mqtt = PSKR_MQTT_listener(self.home_square_lev4, self.add_mqtt_spot)
|
|
101
|
+
threading.Thread(target = self.count_activity, daemon = True).start()
|
|
102
|
+
|
|
103
|
+
def band_from_MHz(self, fMHz): # rewrite this to use the band button defs from ini file
|
|
104
|
+
f = int(fMHz)
|
|
105
|
+
if f > 0:
|
|
106
|
+
freqs = [1,3,5,7,10,14,18,21,24,28,50,144,433]
|
|
107
|
+
idx = freqs.index(f) if f in freqs else -1
|
|
108
|
+
if idx > -1:
|
|
109
|
+
return ['160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','2m','70cm'][idx]
|
|
110
|
+
|
|
111
|
+
def load_from_wb(self, log_cache):
|
|
112
|
+
for key in log_cache:
|
|
113
|
+
key_parts = key.split('_')
|
|
114
|
+
if len(key_parts) > 1:
|
|
115
|
+
c, b, m = key_parts
|
|
116
|
+
if m == 'FT8':
|
|
117
|
+
self.add_myspots_record(self.hearing_me.data, None, b, c, 0, 0)
|
|
118
|
+
self.add_myspots_record(self.heard_by_me.data, None, b, c, 0, 0)
|
|
119
|
+
|
|
120
|
+
def load_all_file(self, all_file):
|
|
121
|
+
recs = self.parse_all_txt(all_file)
|
|
122
|
+
if not any(recs): return
|
|
123
|
+
for r in recs:
|
|
124
|
+
if r['md'] == 'FT8':
|
|
125
|
+
band = self.band_from_MHz(r['fMHz'])
|
|
126
|
+
if band is not None:
|
|
127
|
+
TxRx = 'Tx' if (r['TxRx'] == 'Tx' or r['call_b'] == self.my_call) else 'Rx'
|
|
128
|
+
call = r['call_b'] if TxRx == 'Rx' else r['call_a']
|
|
129
|
+
if len(call) > 3:
|
|
130
|
+
data = self.heard_by_me.data if TxRx == 'Rx' else self.hearing_me.data
|
|
131
|
+
self.add_myspots_record(data, None, band, call, 0, 0)
|
|
132
|
+
|
|
133
|
+
def parse_all_txt(self, all_file):
|
|
134
|
+
rows, recs = None, []
|
|
135
|
+
if os.path.exists(all_file):
|
|
136
|
+
with open(all_file, 'r') as f:
|
|
137
|
+
rows = f.readlines()
|
|
138
|
+
if rows is not None:
|
|
139
|
+
for r in rows:
|
|
140
|
+
fields = r.strip().split()
|
|
141
|
+
if len(fields) > 8:
|
|
142
|
+
call_a_pos = 7 if fields[7] != '~' else 8
|
|
143
|
+
recs.append({'fMHz':float(fields[1]), 'TxRx':fields[2], 'md':fields[3],
|
|
144
|
+
'call_a':fields[call_a_pos], 'call_b':fields[call_a_pos + 1]} )
|
|
145
|
+
if not any(recs):
|
|
146
|
+
print("Didn't find any records in an ALL.txt file in the config folder")
|
|
147
|
+
return recs
|
|
148
|
+
|
|
149
|
+
def write_all_txt_row(self, cyclestart_string, fMHz, TxRx, mode, snr, dt, fHz, msg):
|
|
150
|
+
all_file = f"{self.config_folder}/ALL.txt"
|
|
151
|
+
filemode = 'w' if not os.path.exists(all_file) else 'a'
|
|
152
|
+
row = f"{cyclestart_string} {fMHz:8.3f} {TxRx} {mode} {snr:+03d} {dt:4.1f} {fHz:4.0f} {msg}"
|
|
153
|
+
with open(all_file, filemode) as f:
|
|
154
|
+
f.write(f"{row}\n")
|
|
155
|
+
|
|
156
|
+
def add_mqtt_spot(self, d):
|
|
157
|
+
tnow = int(time.time())
|
|
158
|
+
sc, rc = (d['sc'], d['sl']), (d['rc'], d['rl'])
|
|
159
|
+
for iTxRx, call_grid in enumerate([sc, rc]):
|
|
160
|
+
call, grid = call_grid
|
|
161
|
+
self.store_best_grid(call, grid)
|
|
162
|
+
if self.home_square_lev4 in grid:
|
|
163
|
+
self.add_homespots_record((d['b'], iTxRx, call), tnow)
|
|
164
|
+
if d['sc'] == self.my_call:
|
|
165
|
+
self.add_myspots_record(self.hearing_me.data, self.hearing_me_new, d['b'], d['rc'], tnow, d['rp'])
|
|
166
|
+
if d['rc'] == self.my_call:
|
|
167
|
+
self.add_myspots_record(self.heard_by_me.data, self.heard_by_me_new, d['b'], d['sc'], tnow, d['rp'])
|
|
168
|
+
|
|
169
|
+
def store_best_grid(self, call, grid):
|
|
170
|
+
if call.startswith('<'): return
|
|
171
|
+
existing_grid = self.call_to_grid.data.get(call, '')
|
|
172
|
+
if len(grid) > len(existing_grid):
|
|
173
|
+
self.call_to_grid.data[call] = grid
|
|
174
|
+
|
|
175
|
+
def add_homespots_record(self, key, t):
|
|
176
|
+
self.band_TxRx_homecall_report_times.data.setdefault(key, [])
|
|
177
|
+
self.band_TxRx_homecall_report_times.data[key].append(int(t))
|
|
178
|
+
|
|
179
|
+
def add_myspots_record(self, historic_data, new_alert_data, band, call, t, rp):
|
|
180
|
+
self._update_new_alert(band, call, historic_data, new_alert_data)
|
|
181
|
+
historic_data.setdefault(band, {})
|
|
182
|
+
if call in historic_data[band]:
|
|
183
|
+
if t < historic_data[band][call]['t']:
|
|
184
|
+
return
|
|
185
|
+
historic_data[band][call] = {'t': int(t),'rp':int(rp)}
|
|
186
|
+
|
|
187
|
+
def _update_new_alert(self, band, call, historic_data, new_alert_data):
|
|
188
|
+
if new_alert_data is None:
|
|
189
|
+
return
|
|
190
|
+
new = band not in historic_data
|
|
191
|
+
if not new:
|
|
192
|
+
new = call not in historic_data[band]
|
|
193
|
+
if new:
|
|
194
|
+
new_alert_data.setdefault(band, [])
|
|
195
|
+
new_alert_data[band].append(call)
|
|
196
|
+
|
|
197
|
+
def is_in_new_alert(self, band, call, new_alert_data):
|
|
198
|
+
result = False
|
|
199
|
+
if band in new_alert_data:
|
|
200
|
+
result = call in new_alert_data[band]
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
def is_hearing_me(self, band, call, since_seconds):
|
|
204
|
+
result = False
|
|
205
|
+
if band in self.hearing_me.data:
|
|
206
|
+
if call in self.hearing_me.data[band]:
|
|
207
|
+
result = int(self.hearing_me.data[band][call]['t']) > since_seconds
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
def count_activity(self):
|
|
211
|
+
import numpy as np
|
|
212
|
+
while True:
|
|
213
|
+
time.sleep(5)
|
|
214
|
+
self.home_activity = {}
|
|
215
|
+
self.home_most_remotes = {}
|
|
216
|
+
with self.lock:
|
|
217
|
+
# clear counters for each band
|
|
218
|
+
for b in self.home_activity:
|
|
219
|
+
self.home_activity[b] = [0, 0]
|
|
220
|
+
for b in self.home_most_remotes:
|
|
221
|
+
self.home_most_remotes[b] = [('',0), ('',0)]
|
|
222
|
+
|
|
223
|
+
# keep only the remote spots that happened in the self.pskr_refresh_mins window
|
|
224
|
+
for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
|
|
225
|
+
band_TxRx_homecall_report_times = self.band_TxRx_homecall_report_times.data[band_TxRx_homecall]
|
|
226
|
+
band_TxRx_homecall_report_times = [t for t in band_TxRx_homecall_report_times if (time.time() - t) < 60*self.pskr_refresh_mins]
|
|
227
|
+
self.band_TxRx_homecall_report_times.data[band_TxRx_homecall] = band_TxRx_homecall_report_times
|
|
228
|
+
|
|
229
|
+
# count number of local Tx and Rx, and identify the local Tx and Rx with most remote spots
|
|
230
|
+
for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
|
|
231
|
+
band_TxRx_homecall_report_times = self.band_TxRx_homecall_report_times.data[band_TxRx_homecall]
|
|
232
|
+
if len(band_TxRx_homecall_report_times):
|
|
233
|
+
b, iTxRx, c = band_TxRx_homecall
|
|
234
|
+
self.home_activity.setdefault(b, [0, 0])
|
|
235
|
+
self.home_activity[b][iTxRx] +=1
|
|
236
|
+
self.home_most_remotes.setdefault(b, [('',0), ('',0)])
|
|
237
|
+
nremotes = len(band_TxRx_homecall_report_times)
|
|
238
|
+
current_winner = self.home_most_remotes[b][iTxRx]
|
|
239
|
+
if nremotes > current_winner[1]:
|
|
240
|
+
if c != self.my_call:
|
|
241
|
+
self.home_most_remotes[b][iTxRx] = (c, nremotes)
|
|
242
|
+
|
|
243
|
+
def get_spot_counts(self, band, call):
|
|
244
|
+
tx_reports = self.band_TxRx_homecall_report_times.data.get((band, 0, call), [])
|
|
245
|
+
rx_reports = self.band_TxRx_homecall_report_times.data.get((band, 1, call), [])
|
|
246
|
+
n_spotting = len(tx_reports) if tx_reports else 0
|
|
247
|
+
n_spotted = len(rx_reports) if rx_reports else 0
|
|
248
|
+
return n_spotted, n_spotting
|
|
249
|
+
|
|
250
|
+
def get_dist_brg(self, grid, units):
|
|
251
|
+
self.dist_brg_cache.setdefault(grid, grids_to_dist_brg(self.home_square, grid, units))
|
|
252
|
+
return self.dist_brg_cache[grid]
|
|
253
|
+
|
|
254
|
+
def get_geo_text(self, call, units):
|
|
255
|
+
geo_text = ''
|
|
256
|
+
grid = self.call_to_grid.data.get(call, False)
|
|
257
|
+
if grid:
|
|
258
|
+
loc = grid if units == 'grid' else self.get_dist_brg(grid, units)
|
|
259
|
+
units_str = '' if units == 'grid' else ('km' if 'km' in units else 'mi')
|
|
260
|
+
geo_text = f"{int(loc[0]):5d}{units_str} {int(loc[1]):3d}°"
|
|
261
|
+
return geo_text
|
|
262
|
+
|
|
263
|
+
def ensure_file_exists(path, header = None):
|
|
264
|
+
try:
|
|
265
|
+
with open(path, "x") as f:
|
|
266
|
+
if header is not None:
|
|
267
|
+
f.write(header)
|
|
268
|
+
except FileExistsError:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
class ADIF:
|
|
272
|
+
def __init__(self, logfile):
|
|
273
|
+
self.adif_log_file = logfile
|
|
274
|
+
ensure_file_exists(self.adif_log_file, header = "header <eoh>\n")
|
|
275
|
+
self.cache = self._build_cache()
|
|
276
|
+
|
|
277
|
+
def log(self, times, band_info, mStation, oStation, rpts):
|
|
278
|
+
log_dict = {'call':oStation['c'], 'gridsquare':oStation['g'], 'mode':'FT8',
|
|
279
|
+
'operator':mStation['c'], 'station_callsign':mStation['c'], 'my_gridsquare':mStation['g'],
|
|
280
|
+
'rst_sent':rpts['sent'], 'rst_rcvd':rpts['rcvd'],
|
|
281
|
+
'qso_date':time.strftime("%Y%m%d", times['time_on']), 'qso_date_off':time.strftime("%Y%m%d", times['time_off']),
|
|
282
|
+
'time_on':time.strftime("%H%M%S", times['time_on']), 'time_off':time.strftime("%H%M%S", times['time_on']),
|
|
283
|
+
'band':band_info['b'], 'freq':band_info['fMHz']}
|
|
284
|
+
with open(self.adif_log_file,'a') as f:
|
|
285
|
+
for k, v in log_dict.items():
|
|
286
|
+
v = str(v)
|
|
287
|
+
f.write(f"<{k}:{len(v)}>{v} ")
|
|
288
|
+
f.write(f"<eor>\n")
|
|
289
|
+
cbm = log_dict['call'] + "_" + log_dict['band'] + "_FT8"
|
|
290
|
+
tm = time.time()
|
|
291
|
+
self.cache[log_dict['call']] = tm
|
|
292
|
+
self.cache[cbm] = tm
|
|
293
|
+
|
|
294
|
+
def _build_cache(self):
|
|
295
|
+
import calendar
|
|
296
|
+
def parse(rec, field):
|
|
297
|
+
p = rec.find(field)
|
|
298
|
+
if p > 0:
|
|
299
|
+
p1, p2 = rec.find(':',p), rec.find('>',p)
|
|
300
|
+
n = int(rec[p1+1:p2])
|
|
301
|
+
return rec[p2+1: p2+1+n]
|
|
302
|
+
cache = {}
|
|
303
|
+
with open(self.adif_log_file, 'r') as f:
|
|
304
|
+
for l in f.readlines():
|
|
305
|
+
if parse(l, 'mode') == "FT8":
|
|
306
|
+
c, b, d, t = parse(l, 'call'), parse(l, 'band'), parse(l, 'qso_date'), parse(l, 'time_on')
|
|
307
|
+
time_tuple = time.strptime(d+t, "%Y%m%d%H%M%S")
|
|
308
|
+
tm = calendar.timegm(time_tuple)
|
|
309
|
+
cache[c] = tm
|
|
310
|
+
cache[c + "_"+b+"_FT8"] = tm
|
|
311
|
+
return cache
|
|
@@ -14,7 +14,7 @@ HOVERCOLOR = 'darkgreen'
|
|
|
14
14
|
ACTIVE_BUTTON_COLOR = 'cyan'
|
|
15
15
|
INACTIVE_BUTTON_COLOR = '#edeef0'
|
|
16
16
|
MAX_FONT_SIZE_MAIN = 10
|
|
17
|
-
L = {'pmargin':0.04, 'sidebar_width': 0.
|
|
17
|
+
L = {'pmargin':0.04, 'sidebar_width': 0.17, 'banner_height':0.1, 'vsep1':0.01, 'hsep1':0.02}
|
|
18
18
|
|
|
19
19
|
# ================== WATERFALL ======================================================
|
|
20
20
|
|
|
@@ -31,7 +31,7 @@ class Scrollbox:
|
|
|
31
31
|
self.lineartists = []
|
|
32
32
|
for i in range(self.nlines):
|
|
33
33
|
self.lineartists.append(self.ax.text(0.03,1 - self.line_height * (i+1),
|
|
34
|
-
'', color = MAIN_TEXT_COLOR, fontsize = self.fontsize))
|
|
34
|
+
'', color = MAIN_TEXT_COLOR, fontsize = self.fontsize, clip_on = True))
|
|
35
35
|
if monospace:
|
|
36
36
|
self.lineartists[-1].set_fontfamily('monospace')
|
|
37
37
|
self.ax.set_xticks([])
|
|
@@ -67,7 +67,7 @@ class Msg_box:
|
|
|
67
67
|
self.onclick = onclick
|
|
68
68
|
rect = Rectangle((tbin, fbin), width=w, height=h, alpha=0.6, edgecolor='lime', lw=2)
|
|
69
69
|
self.patch = ax.add_patch(rect)
|
|
70
|
-
self.text_inst = ax.text(tbin, fbin+2, '', fontsize='small', fontweight='bold' )
|
|
70
|
+
self.text_inst = ax.text(tbin, fbin+2, '', fontsize='small', fontweight='bold', clip_on = True )
|
|
71
71
|
self.cid = fig.canvas.mpl_connect('button_press_event', self._onclick)
|
|
72
72
|
self.expire = 0
|
|
73
73
|
|
|
@@ -109,7 +109,7 @@ class ButtonBox:
|
|
|
109
109
|
self.info_axs.set_xticks([])
|
|
110
110
|
self.info_axs.set_yticks([])
|
|
111
111
|
self.info_axs.set_facecolor(TEXT_BACKGROUND_COLOR)
|
|
112
|
-
self.label2 = self.info_axs.text(0.03, 0.5, '', color = INFO_TEXT_COLOR, verticalalignment = 'center')
|
|
112
|
+
self.label2 = self.info_axs.text(0.03, 0.5, '', color = INFO_TEXT_COLOR, verticalalignment = 'center', clip_on = True)
|
|
113
113
|
self.btn_widg = Button(self.btn_axs, btn_label, color = BUTTONCOLOR, hovercolor = HOVERCOLOR)
|
|
114
114
|
self.label = self.btn_widg.label
|
|
115
115
|
self.label.set_color(MAIN_TEXT_COLOR)
|
|
@@ -1,11 +1,36 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import threading
|
|
5
|
-
import random
|
|
1
|
+
import paho.mqtt.client as mqtt
|
|
2
|
+
from ast import literal_eval
|
|
3
|
+
import socket, struct, random
|
|
4
|
+
import threading, time
|
|
6
5
|
|
|
7
6
|
MAX_REPORTS = 90
|
|
8
7
|
|
|
8
|
+
class PSKR_MQTT_listener:
|
|
9
|
+
def __init__(self, home_square, on_spot):
|
|
10
|
+
self.home_square = home_square
|
|
11
|
+
self.on_spot = on_spot
|
|
12
|
+
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
|
13
|
+
mqttc.on_connect = self.on_connect
|
|
14
|
+
mqttc.on_message = self.on_message
|
|
15
|
+
try:
|
|
16
|
+
mqttc.connect("mqtt.pskreporter.info", 1883, 60)
|
|
17
|
+
except:
|
|
18
|
+
print("[MQTT] connection error")
|
|
19
|
+
threading.Thread(target = mqttc.loop_forever, daemon = True).start()
|
|
20
|
+
|
|
21
|
+
def on_connect(self, client, userdata, flags, reason_code, properties):
|
|
22
|
+
#pskr/filter/v2/{band}/{mode}/{sendercall}/{receivercall}/{senderlocator}/{receiverlocator}/{sendercouniTxRxy}/{receivercouniTxRxy}
|
|
23
|
+
print(f"[MQTT] Requesting mqtt feed for {self.home_square}")
|
|
24
|
+
client.subscribe(f"pskr/filter/v2/+/FT8/+/+/{self.home_square}/#")
|
|
25
|
+
client.subscribe(f"pskr/filter/v2/+/FT8/+/+/+/{self.home_square}/#")
|
|
26
|
+
|
|
27
|
+
def on_message(self, client, userdata, msg):
|
|
28
|
+
try:
|
|
29
|
+
d = literal_eval(msg.payload.decode())
|
|
30
|
+
except:
|
|
31
|
+
return
|
|
32
|
+
self.on_spot(d)
|
|
33
|
+
|
|
9
34
|
class PSKR_upload:
|
|
10
35
|
# https://pskreporter.info/pskdev.html
|
|
11
36
|
# https://pskreporter.info/cgi-bin/psk-analysis.pl
|