PyFT8 3.7.1__tar.gz → 3.7.4__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.1 → pyft8-3.7.4}/PKG-INFO +5 -4
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8/databases.py +148 -127
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8/gui.py +151 -120
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8/pyft8.py +48 -38
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8/qso_manager.py +67 -29
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8/receiver.py +55 -114
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8/time_utils.py +3 -4
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8.egg-info/PKG-INFO +5 -4
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8.egg-info/SOURCES.txt +1 -5
- {pyft8-3.7.1 → pyft8-3.7.4}/README.md +4 -3
- {pyft8-3.7.1 → pyft8-3.7.4}/pyproject.toml +1 -1
- pyft8-3.7.4/tests/live test.py +175 -0
- pyft8-3.7.4/tests/plot_decode_times.py +41 -0
- pyft8-3.7.1/PyFT8/message_broker.py +0 -69
- pyft8-3.7.1/tests/play_wavs.py +0 -54
- pyft8-3.7.1/tests/plot_decode_times.py +0 -26
- pyft8-3.7.1/tests/spare.py +0 -52
- pyft8-3.7.1/tests/spare2.py +0 -37
- pyft8-3.7.1/tests/test_batch_and_live.py +0 -160
- {pyft8-3.7.1 → pyft8-3.7.4}/LICENSE +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/MANIFEST.in +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8/__init__.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8/osd.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8/pskreporter.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8/rigctrl.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8/transmitter.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/setup.cfg +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/misc/CQ AAAA.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/misc/animation.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/misc/bits_loopback.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/misc/bits_loopback_short.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/misc/osd.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/misc/symbols_loopback.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/misc/test_generate_wav.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/misc/test_loopback_performance.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/misc/text.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/misc/view_worked_before.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/pipeline/Fine tuning compare IOS_PC.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/pipeline/Targetted tests.py +0 -0
- {pyft8-3.7.1 → pyft8-3.7.4}/tests/pipeline/test_01.wav +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 3.7.
|
|
3
|
+
Version: 3.7.4
|
|
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
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
# PyFT8 [](https://pepy.tech/projects/pyft8) [](https://pepy.tech/projects/pyft8) [PyPI link](https://pypi.org/project/PyFT8/)
|
|
24
24
|
# All-Python FT8 Transceiver GUI / Command Line Modem
|
|
25
25
|
|
|
26
|
-
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 to that of WSJT-x 2.7.0 running in FAST mode.
|
|
26
|
+
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
27
|
|
|
28
28
|
PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Python, but can be used as a standalone replacement for WSJT-x and other software.
|
|
29
29
|
|
|
@@ -112,9 +112,10 @@ 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
|
|
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 90% of the decodes that WSJT-x gets on FAST mode, and about 78% of those it gets on DEEP mode.
|
|
116
|
+
|
|
117
|
+
<img width="900" height="900" alt="live wav playback vs wsjtx_v2 7 0" src="https://github.com/user-attachments/assets/d05755c9-07d2-4e2f-98ff-c15f64b320fe" />
|
|
116
118
|
|
|
117
|
-
<img width="900" height="900" alt="live playback vs wsjtx_v2 7 0" src="https://github.com/user-attachments/assets/853e98b2-5b9b-4241-b5f3-181fe193642e" />
|
|
118
119
|
|
|
119
120
|
## Decoding approach
|
|
120
121
|
The approach to decoding, as at v3.7.0, is as follows.
|
|
@@ -2,6 +2,9 @@ import threading, os, pickle, json
|
|
|
2
2
|
from PyFT8.pskreporter import PSKR_MQTT_listener
|
|
3
3
|
from PyFT8.time_utils import time_utils
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
# call hash calculator for receiver ==============================================================
|
|
7
|
+
|
|
5
8
|
call_hashes, hashes_for_calls = {}, {}
|
|
6
9
|
|
|
7
10
|
def add_call_hashes(call):
|
|
@@ -22,31 +25,8 @@ def add_call_hashes(call):
|
|
|
22
25
|
call_hashes[(x, m)] = call
|
|
23
26
|
hashes_for_calls[call] = hashes
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
grid = grid.upper()
|
|
28
|
-
if centre:
|
|
29
|
-
grid = grid + "LL44LL44LL44"[len(grid):]
|
|
30
|
-
mults = [20, 2, 2/24, 0.2/24, 0.2/(24*24), 0.02/(24*24)]
|
|
31
|
-
grid = grid[:2*len(mults)]
|
|
32
|
-
pairs = [grid[i:i+2] for i in range(0,len(grid),2)]
|
|
33
|
-
for i, p in enumerate(pairs):
|
|
34
|
-
zero = [ord('A'),ord('0')][i % 2]
|
|
35
|
-
lon += mults[i] * (ord(p[0]) - zero)
|
|
36
|
-
lat += mults[i] * (ord(p[1]) - zero) / 2
|
|
37
|
-
return (lat, lon)
|
|
38
|
-
|
|
39
|
-
def grids_to_dist_brg(sq1, sq2, units):
|
|
40
|
-
from numpy import sin, cos, asin, atan2, sqrt, radians, degrees
|
|
41
|
-
ll1, ll2 = grid_to_latlong(sq1), grid_to_latlong(sq2)
|
|
42
|
-
lats = [radians(ll1[0]), radians(ll2[0])]
|
|
43
|
-
dlat, dlon = radians(ll2[0] - ll1[0]), radians(ll2[1] - ll1[1])
|
|
44
|
-
s_lats, c_lats = sin(lats), cos(lats)
|
|
45
|
-
a = sin(dlat/2)**2 + c_lats[0] * c_lats[1] * sin(dlon/2)**2
|
|
46
|
-
r = 6371 * 2 * asin(sqrt(a))
|
|
47
|
-
b = atan2(c_lats[1] * sin(dlon), c_lats[0] * s_lats[1] - s_lats[0] * c_lats[1] * cos(dlon))
|
|
48
|
-
b *= (1.0 if 'km' in units else 0.621371)
|
|
49
|
-
return (r, degrees(b) % 360)
|
|
28
|
+
|
|
29
|
+
# Disk-synced dictionary class for History ======================================================
|
|
50
30
|
|
|
51
31
|
class DiskDict:
|
|
52
32
|
def __init__(self, file, autosave_t0):
|
|
@@ -84,11 +64,12 @@ class DiskDict:
|
|
|
84
64
|
except:
|
|
85
65
|
return
|
|
86
66
|
|
|
67
|
+
# History class ==============================================================================================
|
|
68
|
+
|
|
87
69
|
class History:
|
|
88
|
-
def __init__(self, config_folder, my_call, home_square
|
|
70
|
+
def __init__(self, config_folder, my_call, home_square, geo_units, pskr_refresh_mins = 20):
|
|
89
71
|
self.pskr_refresh_mins = pskr_refresh_mins
|
|
90
72
|
self.geo_units = geo_units
|
|
91
|
-
self.log_cache = None
|
|
92
73
|
self.my_call = my_call
|
|
93
74
|
self.home_square = home_square
|
|
94
75
|
self.home_square_lev4 = home_square[:4]
|
|
@@ -104,26 +85,52 @@ class History:
|
|
|
104
85
|
self.heard_by_me = DiskDict(f"{config_folder}/heard_by_me.json", 5) # all-time record of heard by me
|
|
105
86
|
self.call_to_grid = DiskDict(f"{config_folder}/call_to_grid.json", 7) # all time cache call -> fine locator
|
|
106
87
|
self.band_TxRx_homecall_recent_L4grid = DiskDict(f"{config_folder}/recent_l4grid.json", 9) # last 20 mins data -> per band tx/rx & current band detail
|
|
88
|
+
self.start()
|
|
89
|
+
|
|
90
|
+
def start(self):
|
|
91
|
+
mqtt = PSKR_MQTT_listener(self.home_square_lev4, self._add_mqtt_spot)
|
|
92
|
+
threading.Thread(target = self._count_activity, daemon = True).start()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# external query-type functions --------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def is_in_new_alert(self, band, call, new_alert_data):
|
|
98
|
+
result = False
|
|
99
|
+
if band in new_alert_data:
|
|
100
|
+
result = call in new_alert_data[band]
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
def is_hearing_me(self, band, call, in_last_n_minutes):
|
|
104
|
+
result = False
|
|
105
|
+
if band in self.hearing_me.data:
|
|
106
|
+
if call in self.hearing_me.data[band]:
|
|
107
|
+
result = int(self.hearing_me.data[band][call]['t']) > (time_utils.time() - 60*in_last_n_minutes)
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def get_spot_counts(self, band, call):
|
|
111
|
+
tx_reports = self.band_TxRx_homecall_recent_L4grid.data.get(f"{band}_0_{call}", [])
|
|
112
|
+
rx_reports = self.band_TxRx_homecall_recent_L4grid.data.get(f"{band}_1_{call}", [])
|
|
113
|
+
n_spotting = len(tx_reports) if tx_reports else 0
|
|
114
|
+
n_spotted = len(rx_reports) if rx_reports else 0
|
|
115
|
+
return n_spotted, n_spotting
|
|
116
|
+
|
|
117
|
+
def get_geo_text(self, call):
|
|
118
|
+
geo_text = ''
|
|
119
|
+
grid = self.call_to_grid.data.get(call, False)
|
|
120
|
+
if grid:
|
|
121
|
+
loc = grid if self.geo_units == 'grid' else self._get_dist_brg(grid, self.geo_units)
|
|
122
|
+
units_str = '' if self.geo_units == 'grid' else ('km' if 'km' in self.geo_units else 'mi')
|
|
123
|
+
geo_text = f"{int(loc[0]):5d}{units_str} {int(loc[1]):3d}°"
|
|
124
|
+
return geo_text
|
|
125
|
+
|
|
126
|
+
# external storage-like functions -------------------------------------------------------------------
|
|
107
127
|
|
|
108
|
-
def
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def incorporate_log_data(self, log_cache):
|
|
113
|
-
self.log_cache = log_cache
|
|
114
|
-
for key in log_cache:
|
|
115
|
-
key_parts = key.split('_')
|
|
116
|
-
if len(key_parts) > 1:
|
|
117
|
-
c, b, m = key_parts
|
|
118
|
-
if m == 'FT8':
|
|
119
|
-
#print(f"Parsing adif log: Add hearing & heard by {c} on {b}")
|
|
120
|
-
self._add_myspots_record(self.hearing_me.data, None, b, c, 0, 0)
|
|
121
|
-
self._add_myspots_record(self.heard_by_me.data, None, b, c, 0, 0)
|
|
122
|
-
else:
|
|
123
|
-
print(m)
|
|
128
|
+
def add_bidirectional_hearing_heard_spot(self, band, their_call, time_on):
|
|
129
|
+
self._add_myspots_record(self.hearing_me.data, band, their_call, time_on, 0)
|
|
130
|
+
self._add_myspots_record(self.heard_by_me.data, band, their_call, time_on, 0)
|
|
124
131
|
|
|
125
132
|
def load_hearing_heard_from_all_file(self, bands):
|
|
126
|
-
recs = self.
|
|
133
|
+
recs = self._parse_all_txt()
|
|
127
134
|
if not any(recs):
|
|
128
135
|
return
|
|
129
136
|
for b in bands:
|
|
@@ -133,79 +140,90 @@ class History:
|
|
|
133
140
|
if r['md'] == 'FT8':
|
|
134
141
|
band = self.freqs_to_bands.get(round(r['fMHz'], 1), None)
|
|
135
142
|
if band is not None:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
self.
|
|
143
|
+
data = None
|
|
144
|
+
if len(r['call_a']) > 3 and len(r['call_b']) > 3:
|
|
145
|
+
if r['call_a'] == self.my_call:
|
|
146
|
+
data, call = self.hearing_me.data, r['call_b'] # call b is calling me, so call b ishearing me
|
|
147
|
+
if r['call_b'] == self.my_call and not r['call_a'].startswith('CQ'):
|
|
148
|
+
data, call = self.heard_by_me.data, r['call_a'] # I'm calling call a, so I'm hearing call a
|
|
149
|
+
if data:
|
|
150
|
+
self._add_myspots_record(data, band, call, r['t'], r['rp'])
|
|
141
151
|
|
|
142
|
-
def
|
|
143
|
-
rows, recs = None, []
|
|
144
|
-
if os.path.exists(self.all_file):
|
|
145
|
-
with open(self.all_file, 'r') as f:
|
|
146
|
-
rows = f.readlines()
|
|
147
|
-
if rows is not None:
|
|
148
|
-
for r in rows:
|
|
149
|
-
fields = r.strip().split()
|
|
150
|
-
if len(fields) > 8:
|
|
151
|
-
call_a_pos = 7 if fields[7] != '~' else 8
|
|
152
|
-
recs.append({'fMHz':float(fields[1]), 'TxRx':fields[2], 'md':fields[3],
|
|
153
|
-
'call_a':fields[call_a_pos], 'call_b':fields[call_a_pos + 1]} )
|
|
154
|
-
if not any(recs):
|
|
155
|
-
print("Didn't find any records in an ALL.txt file in the config folder")
|
|
156
|
-
return recs
|
|
157
|
-
|
|
158
|
-
def process_message(self, message, band_info, myCall):
|
|
152
|
+
def process_message_for_history(self, message, band_info, myCall):
|
|
159
153
|
m = message
|
|
160
|
-
self._write_all_txt_row(m['
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
154
|
+
self._write_all_txt_row(m['all_txt_format'])
|
|
155
|
+
hail, their_call, grid_rpt = m['msg_tuple']
|
|
156
|
+
current_band = band_info['current_band']
|
|
157
|
+
self._add_myspots_record(self.heard_by_me.data, current_band,
|
|
158
|
+
their_call, int(time_utils.time()), m['their_snr'],
|
|
159
|
+
new_alert_data = self.heard_by_me_new)
|
|
160
|
+
if hail == myCall: # They're calling me, so they're hearing me
|
|
161
|
+
rpt = grid_rpt[-3:]
|
|
166
162
|
if rpt.isnumeric():
|
|
167
|
-
self._add_myspots_record(self.hearing_me.data,
|
|
168
|
-
|
|
163
|
+
self._add_myspots_record(self.hearing_me.data, current_band, their_call,
|
|
164
|
+
int(time_utils.time()), int(rpt), new_alert_data = self.heard_by_me_new)
|
|
165
|
+
|
|
169
166
|
|
|
167
|
+
# ----- internal storing / writing functions ----------------------------------------------------------
|
|
170
168
|
|
|
171
|
-
def _write_all_txt_row(self,
|
|
169
|
+
def _write_all_txt_row(self, row_text):
|
|
172
170
|
filemode = 'w' if not os.path.exists(self.all_file) else 'a'
|
|
173
|
-
row = f"{cyclestart_string} {fMHz:8.3f} {TxRx} {mode} {snr_str} {dt:4.1f} {fHz:4.0f} {hail} {their_call} {grid_rpt}"
|
|
174
171
|
with open(self.all_file, filemode) as f:
|
|
175
|
-
f.write(f"{
|
|
172
|
+
f.write(f"{row_text}\n")
|
|
176
173
|
|
|
177
|
-
def
|
|
174
|
+
def _add_mqtt_spot(self, d):
|
|
178
175
|
tnow = int(time_utils.time())
|
|
179
176
|
sc, rc = (d['sc'], d['sl']), (d['rc'], d['rl'])
|
|
180
177
|
for iTxRx, call_grid in enumerate([sc, rc]):
|
|
181
178
|
call, grid = call_grid
|
|
182
|
-
self.
|
|
179
|
+
self._store_best_grid(call, grid)
|
|
183
180
|
if self.home_square_lev4 in grid:
|
|
184
|
-
self.
|
|
181
|
+
self._add_homespots_record(f"{d['b']}_{iTxRx}_{call}", tnow)
|
|
185
182
|
if d['sc'] == self.my_call:
|
|
186
|
-
self._add_myspots_record(self.hearing_me.data,
|
|
183
|
+
self._add_myspots_record(self.hearing_me.data, d['b'], d['rc'], tnow, d['rp'], new_alert_data = self.hearing_me_new)
|
|
187
184
|
if d['rc'] == self.my_call:
|
|
188
|
-
self._add_myspots_record(self.heard_by_me.data,
|
|
185
|
+
self._add_myspots_record(self.heard_by_me.data, d['b'], d['sc'], tnow, d['rp'], new_alert_data = self.heard_by_me_new)
|
|
189
186
|
|
|
190
|
-
def
|
|
187
|
+
def _add_homespots_record(self, key, t):
|
|
188
|
+
self.band_TxRx_homecall_recent_L4grid.data.setdefault(key, [])
|
|
189
|
+
self.band_TxRx_homecall_recent_L4grid.data[key].append(int(t))
|
|
190
|
+
|
|
191
|
+
def _store_best_grid(self, call, grid):
|
|
191
192
|
if call.startswith('<'): return
|
|
192
193
|
existing_grid = self.call_to_grid.data.get(call, '')
|
|
193
194
|
if len(grid) > len(existing_grid):
|
|
194
195
|
self.call_to_grid.data[call] = grid
|
|
195
|
-
|
|
196
|
-
def add_homespots_record(self, key, t):
|
|
197
|
-
self.band_TxRx_homecall_recent_L4grid.data.setdefault(key, [])
|
|
198
|
-
self.band_TxRx_homecall_recent_L4grid.data[key].append(int(t))
|
|
199
196
|
|
|
200
|
-
def
|
|
201
|
-
|
|
197
|
+
def _parse_all_txt(self):
|
|
198
|
+
import time, calendar
|
|
199
|
+
rows, recs = None, []
|
|
200
|
+
if os.path.exists(self.all_file):
|
|
201
|
+
with open(self.all_file, 'r') as f:
|
|
202
|
+
rows = f.readlines()
|
|
203
|
+
if rows is not None:
|
|
204
|
+
for r in rows:
|
|
205
|
+
fields = r.strip().split()
|
|
206
|
+
if len(fields) > 8:
|
|
207
|
+
call_a_pos = 7 if fields[7] != '~' else 8
|
|
208
|
+
if len(fields[0]) == 13:
|
|
209
|
+
time_tuple = time.strptime(fields[0], "%y%m%d_%H%M%S")
|
|
210
|
+
t = calendar.timegm(time_tuple)
|
|
211
|
+
recs.append({'t':t , 'fMHz':float(fields[1]), 'TxRx':fields[2], 'md':fields[3],
|
|
212
|
+
'rp':fields[4], 'call_a':fields[call_a_pos], 'call_b':fields[call_a_pos + 1]} )
|
|
213
|
+
if not any(recs):
|
|
214
|
+
print("Didn't find any records in an ALL.txt file in the config folder")
|
|
215
|
+
return recs
|
|
216
|
+
|
|
217
|
+
def _add_myspots_record(self, historic_data, band, call, t, rp, new_alert_data = None):
|
|
218
|
+
self._update_new_alert_data(band, call, historic_data, new_alert_data)
|
|
202
219
|
historic_data.setdefault(band, {})
|
|
203
220
|
if call in historic_data[band]:
|
|
204
221
|
if int(t) < int(historic_data[band][call]['t']):
|
|
205
222
|
return
|
|
206
|
-
|
|
223
|
+
if t > 0:
|
|
224
|
+
historic_data[band][call] = {'t': int(t),'rp':int(rp)}
|
|
207
225
|
|
|
208
|
-
def
|
|
226
|
+
def _update_new_alert_data(self, band, call, historic_data, new_alert_data):
|
|
209
227
|
if new_alert_data is None:
|
|
210
228
|
return
|
|
211
229
|
new = band not in historic_data
|
|
@@ -215,20 +233,14 @@ class History:
|
|
|
215
233
|
new_alert_data.setdefault(band, [])
|
|
216
234
|
new_alert_data[band].append(call)
|
|
217
235
|
|
|
218
|
-
def
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
result = call in new_alert_data[band]
|
|
222
|
-
return result
|
|
236
|
+
def _get_dist_brg(self, grid, units):
|
|
237
|
+
self.dist_brg_cache.setdefault(grid, _grids_to_dist_brg(self.home_square, grid, units))
|
|
238
|
+
return self.dist_brg_cache[grid]
|
|
223
239
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
result = int(self.hearing_me.data[band][call]['t']) > since_seconds
|
|
229
|
-
return result
|
|
230
|
-
|
|
231
|
-
def count_activity(self):
|
|
240
|
+
|
|
241
|
+
# ---- Internal analysis functions -------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def _count_activity(self):
|
|
232
244
|
import numpy as np
|
|
233
245
|
while True:
|
|
234
246
|
time_utils.sleep(5)
|
|
@@ -261,33 +273,38 @@ class History:
|
|
|
261
273
|
if nremotes > current_winner[1]:
|
|
262
274
|
if c != self.my_call:
|
|
263
275
|
self.home_most_remotes[b][iTxRx] = (c, nremotes)
|
|
276
|
+
|
|
277
|
+
# Geo Helpers =================================================================
|
|
264
278
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
279
|
+
def _grids_to_dist_brg(sq1, sq2, units):
|
|
280
|
+
from numpy import sin, cos, asin, atan2, sqrt, radians, degrees
|
|
281
|
+
ll1, ll2 = _grid_to_latlong(sq1), _grid_to_latlong(sq2)
|
|
282
|
+
lats = [radians(ll1[0]), radians(ll2[0])]
|
|
283
|
+
dlat, dlon = radians(ll2[0] - ll1[0]), radians(ll2[1] - ll1[1])
|
|
284
|
+
s_lats, c_lats = sin(lats), cos(lats)
|
|
285
|
+
a = sin(dlat/2)**2 + c_lats[0] * c_lats[1] * sin(dlon/2)**2
|
|
286
|
+
r = 6371 * 2 * asin(sqrt(a))
|
|
287
|
+
b = atan2(c_lats[1] * sin(dlon), c_lats[0] * s_lats[1] - s_lats[0] * c_lats[1] * cos(dlon))
|
|
288
|
+
b *= (1.0 if 'km' in units else 0.621371)
|
|
289
|
+
return (r, degrees(b) % 360)
|
|
271
290
|
|
|
272
|
-
def get_dist_brg(self, grid, units):
|
|
273
|
-
self.dist_brg_cache.setdefault(grid, grids_to_dist_brg(self.home_square, grid, units))
|
|
274
|
-
return self.dist_brg_cache[grid]
|
|
275
291
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
292
|
+
def _grid_to_latlong(grid, centre = True):
|
|
293
|
+
lat, lon = -90, -180
|
|
294
|
+
grid = grid.upper()
|
|
295
|
+
if centre:
|
|
296
|
+
grid = grid + "LL44LL44LL44"[len(grid):]
|
|
297
|
+
mults = [20, 2, 2/24, 0.2/24, 0.2/(24*24), 0.02/(24*24)]
|
|
298
|
+
grid = grid[:2*len(mults)]
|
|
299
|
+
pairs = [grid[i:i+2] for i in range(0,len(grid),2)]
|
|
300
|
+
for i, p in enumerate(pairs):
|
|
301
|
+
zero = [ord('A'),ord('0')][i % 2]
|
|
302
|
+
lon += mults[i] * (ord(p[0]) - zero)
|
|
303
|
+
lat += mults[i] * (ord(p[1]) - zero) / 2
|
|
304
|
+
return (lat, lon)
|
|
305
|
+
|
|
282
306
|
|
|
283
|
-
|
|
284
|
-
geo_text = ''
|
|
285
|
-
grid = self.call_to_grid.data.get(call, False)
|
|
286
|
-
if grid:
|
|
287
|
-
loc = grid if self.geo_units == 'grid' else self.get_dist_brg(grid, self.geo_units)
|
|
288
|
-
units_str = '' if self.geo_units == 'grid' else ('km' if 'km' in self.geo_units else 'mi')
|
|
289
|
-
geo_text = f"{int(loc[0]):5d}{units_str} {int(loc[1]):3d}°"
|
|
290
|
-
return geo_text
|
|
307
|
+
# ==================ADIF LOGGING=========================================================================
|
|
291
308
|
|
|
292
309
|
def ensure_file_exists(path, header = None):
|
|
293
310
|
try:
|
|
@@ -314,6 +331,10 @@ class ADIF:
|
|
|
314
331
|
self.cache[log_dict['call']] = tm
|
|
315
332
|
self.cache[cbm] = tm
|
|
316
333
|
|
|
334
|
+
def get_worked_before_info(self, their_call):
|
|
335
|
+
wb_time = self.cache.get(their_call,'')
|
|
336
|
+
return f"wb: {time_utils.format_duration(time_utils.time() - float(wb_time))}" if wb_time else ''
|
|
337
|
+
|
|
317
338
|
def _build_cache(self):
|
|
318
339
|
import calendar, time
|
|
319
340
|
def parse(rec, field):
|