PyFT8 2.6.1__tar.gz → 2.7.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.6.1 → pyft8-2.7.0}/PKG-INFO +3 -3
- pyft8-2.7.0/PyFT8/gui.py +222 -0
- pyft8-2.7.0/PyFT8/maidenhead.py +32 -0
- pyft8-2.7.0/PyFT8/mqtt.py +133 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/pyft8.py +73 -45
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/PKG-INFO +3 -3
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/SOURCES.txt +1 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/README.md +2 -2
- {pyft8-2.6.1 → pyft8-2.7.0}/pyproject.toml +1 -1
- pyft8-2.6.1/PyFT8/gui.py +0 -153
- pyft8-2.6.1/PyFT8/mqtt.py +0 -84
- {pyft8-2.6.1 → pyft8-2.7.0}/LICENSE +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/MANIFEST.in +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/__init__.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/callhashes.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/hamlib.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/pskr_upload.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/receiver.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/rigctrl.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/time_utils.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/transmitter.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/setup.cfg +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/tests/dev/CQ AAAA.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/tests/dev/osd.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/tests/dev/test_generate_wav.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/tests/dev/test_loopback_performance.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/tests/dev/view_worked_before.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/tests/plot_baseline.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.0}/tests/spare.py +0 -0
- {pyft8-2.6.1 → pyft8-2.7.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.7.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
|
|
@@ -41,7 +41,7 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
|
|
|
41
41
|
The Gui shows:
|
|
42
42
|
- Simultaneous views of odd and even cycles
|
|
43
43
|
- Messages overlaid on waterfall signals that produce them
|
|
44
|
-
- Worked-before info and fine grid locators in the message boxes
|
|
44
|
+
- Worked-before info and fine grid locators / distance and bearing in the message boxes
|
|
45
45
|
- Band activity on band select buttons
|
|
46
46
|
- 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
|
|
47
47
|
|
|
@@ -50,8 +50,8 @@ To enable uploading of spots to pskreporter, make sure that your .ini file inclu
|
|
|
50
50
|
[pskreporter]
|
|
51
51
|
upload = Y
|
|
52
52
|
```
|
|
53
|
-
<img width="1078" height="436" alt="Capture" src="https://github.com/user-attachments/assets/5c4b64cb-e3e6-4a2f-aa77-93fd361a4e74" />
|
|
54
53
|
|
|
54
|
+
<img width="980" height="807" alt="screenshot" src="https://github.com/user-attachments/assets/ac393a05-277a-4d98-bd74-78bcb0ae8b03" />
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
## Motivation
|
pyft8-2.7.0/PyFT8/gui.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import time, queue
|
|
4
|
+
from matplotlib import rcParams
|
|
5
|
+
from matplotlib.animation import FuncAnimation
|
|
6
|
+
from matplotlib.widgets import Slider, Button
|
|
7
|
+
|
|
8
|
+
rcParams['toolbar'] = 'None'
|
|
9
|
+
MAIN_TEXT_COLOR = '#f0f9fa'
|
|
10
|
+
TEXT_BACKGROUND_COLOR = '#2a2b2b'
|
|
11
|
+
INFO_TEXT_COLOR = 'white'
|
|
12
|
+
BUTTONCOLOR = 'grey'
|
|
13
|
+
HOVERCOLOR = 'darkgreen'
|
|
14
|
+
ACTIVE_BUTTON_COLOR = 'cyan'
|
|
15
|
+
INACTIVE_BUTTON_COLOR = '#edeef0'
|
|
16
|
+
MAX_FONT_SIZE_MAIN = 10
|
|
17
|
+
L = {'pmargin':0.04, 'sidebar_width': 0.16, 'banner_height':0.1, 'vsep1':0.01, 'hsep1':0.02}
|
|
18
|
+
|
|
19
|
+
# ================== WATERFALL ======================================================
|
|
20
|
+
|
|
21
|
+
class Scrollbox:
|
|
22
|
+
def __init__(self, fig, box, nlines = 5, monospace = False, default_text = '', fontsize = None):
|
|
23
|
+
self.fig = fig
|
|
24
|
+
self.ax = fig.add_axes(box)
|
|
25
|
+
self.default_text = default_text
|
|
26
|
+
bbox = self.ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
|
|
27
|
+
self.fontsize = np.min([0.5 * bbox.height * fig.dpi / nlines, MAX_FONT_SIZE_MAIN]) if fontsize is None else fontsize
|
|
28
|
+
self.nlines = nlines
|
|
29
|
+
self.line_height = 0.9 / nlines
|
|
30
|
+
self.lines = []
|
|
31
|
+
self.lineartists = []
|
|
32
|
+
for i in range(self.nlines):
|
|
33
|
+
self.lineartists.append(self.ax.text(0.03,1 - self.line_height * (i+1),
|
|
34
|
+
'', color = MAIN_TEXT_COLOR, fontsize = self.fontsize))
|
|
35
|
+
if monospace:
|
|
36
|
+
self.lineartists[-1].set_fontfamily('monospace')
|
|
37
|
+
self.ax.set_xticks([])
|
|
38
|
+
self.ax.set_yticks([])
|
|
39
|
+
self.ax.set_facecolor(TEXT_BACKGROUND_COLOR)
|
|
40
|
+
|
|
41
|
+
def scroll_print(self, text, color = MAIN_TEXT_COLOR):
|
|
42
|
+
self.lines = self.lines[-(self.nlines-1):]
|
|
43
|
+
self.lines.append({'text':text, 'color':color})
|
|
44
|
+
for i, line in enumerate(self.lines):
|
|
45
|
+
self.lineartists[i].set_text(line['text'])
|
|
46
|
+
self.lineartists[i].set_color(line['color'])
|
|
47
|
+
|
|
48
|
+
def clear(self):
|
|
49
|
+
self.lines = []
|
|
50
|
+
for i in range(self.nlines):
|
|
51
|
+
self.lineartists[i].set_text(self.default_text)
|
|
52
|
+
|
|
53
|
+
def list_print(self, lst):
|
|
54
|
+
self.lines = [{'text':l, 'color':'white'} for l in lst[:self.nlines]]
|
|
55
|
+
for i, line in enumerate(self.lines):
|
|
56
|
+
if line['text'] != self.lineartists[i].get_text():
|
|
57
|
+
self.lineartists[i].set_text(line['text'])
|
|
58
|
+
self.lineartists[i].set_color(line['color'])
|
|
59
|
+
for i in range(len(lst), self.nlines):
|
|
60
|
+
if self.lineartists[i].get_text() != '':
|
|
61
|
+
self.lineartists[i].set_text('')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Msg_box:
|
|
65
|
+
def __init__(self, fig, ax, tbin, fbin, w, h, onclick):
|
|
66
|
+
from matplotlib.patches import Rectangle
|
|
67
|
+
self.onclick = onclick
|
|
68
|
+
rect = Rectangle((tbin, fbin), width=w, height=h, alpha=0.6, edgecolor='lime', lw=2)
|
|
69
|
+
self.patch = ax.add_patch(rect)
|
|
70
|
+
self.text_inst = ax.text(tbin, fbin+2, '', fontsize='small', fontweight='bold' )
|
|
71
|
+
self.cid = fig.canvas.mpl_connect('button_press_event', self._onclick)
|
|
72
|
+
self.expire = 0
|
|
73
|
+
|
|
74
|
+
def set_properties(self, message):
|
|
75
|
+
self.message = message
|
|
76
|
+
self.patch.set_x(message.h0_idx)
|
|
77
|
+
self.text_inst.set_x(message.h0_idx)
|
|
78
|
+
self.patch.set_visible(True)
|
|
79
|
+
self.text_inst.set_visible(True)
|
|
80
|
+
self.expire = message.expire
|
|
81
|
+
|
|
82
|
+
def set_appearance(self, message):
|
|
83
|
+
self.text_inst.set_text(message.gui_text)
|
|
84
|
+
colors = ['blue', 'white']
|
|
85
|
+
if message.is_cq: colors = ['green', 'white']
|
|
86
|
+
if message.is_from_me: colors = ['yellow', 'white']
|
|
87
|
+
if message.is_to_me: colors = ['red', 'white']
|
|
88
|
+
self.text_inst.set_color(colors[1])
|
|
89
|
+
self.patch.set_facecolor(colors[0])
|
|
90
|
+
|
|
91
|
+
def hide_if_expired(self):
|
|
92
|
+
if time.time() > self.expire > 0:
|
|
93
|
+
self.patch.set_visible(False)
|
|
94
|
+
self.text_inst.set_visible(False)
|
|
95
|
+
|
|
96
|
+
def _onclick(self, event):
|
|
97
|
+
b, _ = self.patch.contains(event)
|
|
98
|
+
if(b):
|
|
99
|
+
self.onclick(self.message)
|
|
100
|
+
|
|
101
|
+
class ButtonBox:
|
|
102
|
+
def __init__(self, fig, box, btn_pc = 30, onclick = None, clickargs=None, btn_label = ''):
|
|
103
|
+
btnbox, infobox = box.copy(), box.copy()
|
|
104
|
+
btnbox[2] = box[2] * btn_pc /100
|
|
105
|
+
infobox[2] = box[2] * (100-btn_pc) /100
|
|
106
|
+
infobox[0] = box[0] + box[2] * (btn_pc /100)
|
|
107
|
+
self.btn_axs = fig.add_axes(btnbox)
|
|
108
|
+
self.info_axs = fig.add_axes(infobox)
|
|
109
|
+
self.info_axs.set_xticks([])
|
|
110
|
+
self.info_axs.set_yticks([])
|
|
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')
|
|
113
|
+
self.btn_widg = Button(self.btn_axs, btn_label, color = BUTTONCOLOR, hovercolor = HOVERCOLOR)
|
|
114
|
+
self.label = self.btn_widg.label
|
|
115
|
+
self.label.set_color(MAIN_TEXT_COLOR)
|
|
116
|
+
self.clickargs = clickargs
|
|
117
|
+
self.active = False
|
|
118
|
+
self.btn_widg.on_clicked(lambda x: onclick(clickargs))
|
|
119
|
+
|
|
120
|
+
def set_active(self, active: bool):
|
|
121
|
+
if self.active != active:
|
|
122
|
+
self.active = active
|
|
123
|
+
self._update_appearance()
|
|
124
|
+
|
|
125
|
+
def set_info_text(self, text):
|
|
126
|
+
self.label2.set_text(text)
|
|
127
|
+
|
|
128
|
+
def get_info_text(self):
|
|
129
|
+
return self.label2
|
|
130
|
+
|
|
131
|
+
def _update_appearance(self):
|
|
132
|
+
color = ACTIVE_BUTTON_COLOR if self.active else INACTIVE_BUTTON_COLOR
|
|
133
|
+
self.label.set_color(color)
|
|
134
|
+
self.label2.set_color(color)
|
|
135
|
+
|
|
136
|
+
class Gui:
|
|
137
|
+
def __init__(self, dBgrid, hps, bpt, config, on_gui_sidebars_refresh, on_msg_click, on_control_click):
|
|
138
|
+
if config is not None:
|
|
139
|
+
self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
|
|
140
|
+
self.on_msg_click = on_msg_click
|
|
141
|
+
self.on_control_click = on_control_click
|
|
142
|
+
self.on_gui_sidebars_refresh = on_gui_sidebars_refresh
|
|
143
|
+
self.dBgrid = dBgrid
|
|
144
|
+
self.hps, self.bpt = hps, bpt
|
|
145
|
+
self.msg_boxes = {}
|
|
146
|
+
self.decode_queue = queue.Queue()
|
|
147
|
+
self.make_layout(config)
|
|
148
|
+
self.ani = FuncAnimation(self.fig, self._animate, interval = 40, frames=(100000), blit=True)
|
|
149
|
+
|
|
150
|
+
def make_layout(self, config):
|
|
151
|
+
# figure
|
|
152
|
+
self.plt = plt
|
|
153
|
+
self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
|
|
154
|
+
self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
|
|
155
|
+
wf_top = 1-L['pmargin']-L['banner_height']-L['vsep1']
|
|
156
|
+
wf_left = L['pmargin']+L['sidebar_width']+L['hsep1']
|
|
157
|
+
|
|
158
|
+
# waterfall
|
|
159
|
+
self.ax_wf = self.fig.add_axes([wf_left, L['pmargin'], 1-wf_left-L['pmargin'], wf_top-L['pmargin']])
|
|
160
|
+
self.image = self.ax_wf.imshow(self.dBgrid.T,vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
|
|
161
|
+
self.ax_wf.set_xticks([])
|
|
162
|
+
self.ax_wf.set_yticks([])
|
|
163
|
+
|
|
164
|
+
# band stats
|
|
165
|
+
self.band_stats = Scrollbox(self.fig, [L['pmargin'], wf_top+L['vsep1'], L['sidebar_width'], L['banner_height']], nlines = 4, monospace = True)
|
|
166
|
+
self.band_stats.ax.text(-0.2,0.75,'Tx')
|
|
167
|
+
self.band_stats.ax.text(-0.2,0.25,'Rx')
|
|
168
|
+
self.band_stats.ax.set_title(f"Spots to/from {config['station']['grid'][:4]}", fontsize = 10)
|
|
169
|
+
|
|
170
|
+
# console
|
|
171
|
+
self.console = Scrollbox(self.fig, [wf_left, wf_top+L['vsep1'], 1-wf_left-L['pmargin'], L['banner_height']])
|
|
172
|
+
|
|
173
|
+
# control buttons
|
|
174
|
+
self.button_boxes = []
|
|
175
|
+
bh, bs = 0.02, 0.002
|
|
176
|
+
bb = ButtonBox(self.fig, [L['pmargin'], wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
|
|
177
|
+
btn_label = "CQ", onclick = self.on_control_click, clickargs = {'action':'CQ'})
|
|
178
|
+
self.button_boxes.append(bb)
|
|
179
|
+
bb = ButtonBox(self.fig, [L['pmargin'], wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
|
|
180
|
+
btn_label = "Repeat last", onclick = self.on_control_click, clickargs = {'action':'RPT_LAST'})
|
|
181
|
+
self.button_boxes.append(bb)
|
|
182
|
+
bb = ButtonBox(self.fig, [L['pmargin'], wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
|
|
183
|
+
btn_label = "Tx off", onclick = self.on_control_click, clickargs = {'action':'TX_OFF'})
|
|
184
|
+
self.button_boxes.append(bb)
|
|
185
|
+
for band, freq in config['bands'].items():
|
|
186
|
+
bb = ButtonBox(self.fig, [L['pmargin'], wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 30,
|
|
187
|
+
btn_label = band, onclick = self.on_control_click, clickargs = {'action':'SET_BAND','band':band,'freq':freq})
|
|
188
|
+
self.button_boxes.append(bb)
|
|
189
|
+
|
|
190
|
+
# hearing me list
|
|
191
|
+
self.hm = Scrollbox(self.fig, [L['pmargin'], L['pmargin'], L['sidebar_width'], wf_top - (len(self.button_boxes)+2) * bh + bs - L['vsep1']],
|
|
192
|
+
nlines = 30, monospace = True, fontsize = 8)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def refresh_sidebars(self):
|
|
196
|
+
self.on_gui_sidebars_refresh(self)
|
|
197
|
+
|
|
198
|
+
def add_message_box(self, message):
|
|
199
|
+
self.decode_queue.put(message)
|
|
200
|
+
|
|
201
|
+
def _display_message_box(self, message):
|
|
202
|
+
h0_idx, f0_idx = message.h0_idx, message.f0_idx
|
|
203
|
+
if not f0_idx in self.msg_boxes:
|
|
204
|
+
self.msg_boxes[f0_idx] = Msg_box(self.fig, self.ax_wf, h0_idx, f0_idx, 79*self.hps, 8*self.bpt, onclick = self.on_msg_click)
|
|
205
|
+
self.msg_boxes[f0_idx].set_properties(message)
|
|
206
|
+
self.msg_boxes[f0_idx].set_appearance(message)
|
|
207
|
+
|
|
208
|
+
def _tidy_msg_boxes(self):
|
|
209
|
+
for fb in self.msg_boxes:
|
|
210
|
+
self.msg_boxes[fb].hide_if_expired()
|
|
211
|
+
|
|
212
|
+
def _animate(self, frame):
|
|
213
|
+
self.image.set_data(self.dBgrid.T)
|
|
214
|
+
while not self.decode_queue.empty():
|
|
215
|
+
self._display_message_box(self.decode_queue.get())
|
|
216
|
+
if (frame % 10 == 0):
|
|
217
|
+
self._tidy_msg_boxes()
|
|
218
|
+
self.refresh_sidebars()
|
|
219
|
+
return [self.image, *self.ax_wf.patches, *self.ax_wf.texts, *self.band_stats.lineartists, *self.console.lineartists, *self.hm.lineartists,
|
|
220
|
+
*[bb.label for bb in self.button_boxes], *[bb.label2 for bb in self.button_boxes]]
|
|
221
|
+
|
|
222
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def grid_to_latlong(grid, centre = True):
|
|
4
|
+
lat, lon = -90, -180
|
|
5
|
+
grid = grid.upper()
|
|
6
|
+
if centre:
|
|
7
|
+
grid = grid + "LL44LL44LL44"[len(grid):]
|
|
8
|
+
mults = [20, 2, 2/24, 0.2/24, 0.2/(24*24), 0.02/(24*24)]
|
|
9
|
+
grid = grid[:2*len(mults)]
|
|
10
|
+
pairs = [grid[i:i+2] for i in range(0,len(grid),2)]
|
|
11
|
+
for i, p in enumerate(pairs):
|
|
12
|
+
zero = [ord('A'),ord('0')][i % 2]
|
|
13
|
+
lon += mults[i] * (ord(p[0]) - zero)
|
|
14
|
+
lat += mults[i] * (ord(p[1]) - zero) / 2
|
|
15
|
+
return (lat, lon)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def db(sq1, sq2):
|
|
19
|
+
ll1, ll2 = grid_to_latlong(sq1), grid_to_latlong(sq2)
|
|
20
|
+
lats = [np.radians(ll1[0]),np.radians(ll2[0])]
|
|
21
|
+
dlat, dlon = np.radians(ll2[0] - ll1[0]), np.radians(ll2[1] - ll1[1])
|
|
22
|
+
s_lats, c_lats = np.sin(lats), np.cos(lats)
|
|
23
|
+
a = np.sin(dlat/2)**2 + c_lats[0] * c_lats[1] * np.sin(dlon/2)**2
|
|
24
|
+
r = 6371 * 2 * np.asin(np.sqrt(a))
|
|
25
|
+
b = np.atan2(c_lats[1] * np.sin(dlon), c_lats[0] * s_lats[1] - s_lats[0] * c_lats[1] * np.cos(dlon))
|
|
26
|
+
return (r, np.degrees(b) % 360)
|
|
27
|
+
|
|
28
|
+
#grids = ['LL44LL44LL44', 'IO90', 'IO90JU', 'IO90JU44', 'IO90JU95LX', 'IO90JU96MA','IN79jx55']
|
|
29
|
+
#for g in grids:
|
|
30
|
+
# print(g, grid_to_latlong(g, centre = True))
|
|
31
|
+
|
|
32
|
+
#print(db('IO90','IO91'))
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import paho.mqtt.client as mqtt
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from ast import literal_eval
|
|
5
|
+
|
|
6
|
+
SPOTLIFE = 15*60
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import pickle
|
|
10
|
+
|
|
11
|
+
class DiskDict:
|
|
12
|
+
def __init__(self, file):
|
|
13
|
+
self.file = file
|
|
14
|
+
self.data = {}
|
|
15
|
+
self.load()
|
|
16
|
+
threading.Thread(target = self._autosave, daemon = True).start()
|
|
17
|
+
|
|
18
|
+
def _autosave(self, autosave_period = 15):
|
|
19
|
+
while True:
|
|
20
|
+
time.sleep(autosave_period)
|
|
21
|
+
self.save()
|
|
22
|
+
|
|
23
|
+
def load(self):
|
|
24
|
+
if(os.path.exists(self.file)):
|
|
25
|
+
with open(f"{self.file}","rb") as f:
|
|
26
|
+
self.data = pickle.load(f)
|
|
27
|
+
|
|
28
|
+
def save(self):
|
|
29
|
+
if self.data:
|
|
30
|
+
with open(f"{self.file}","wb") as f:
|
|
31
|
+
pickle.dump(self.data, f)
|
|
32
|
+
|
|
33
|
+
class PSKR_MQTT_listener:
|
|
34
|
+
def __init__(self, config_folder, my_call, home_square):
|
|
35
|
+
self.my_call = my_call
|
|
36
|
+
self.hearing_me = DiskDict(f"{config_folder}/hearing_me.pkl")
|
|
37
|
+
self.home_square = home_square
|
|
38
|
+
self.callsign_cache = DiskDict(f"{config_folder}/callsign_cache.pkl")
|
|
39
|
+
self.band_TxRx_homecall_report_times = DiskDict(f"{config_folder}/report_times.pkl")
|
|
40
|
+
self.band_TxRx_homecall_countremotes = {}
|
|
41
|
+
self.home_activity = {}
|
|
42
|
+
self.home_most_remotes = {}
|
|
43
|
+
self.lock = threading.Lock()
|
|
44
|
+
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
|
45
|
+
mqttc.on_connect = self.on_connect
|
|
46
|
+
mqttc.on_message = self.on_message
|
|
47
|
+
try:
|
|
48
|
+
mqttc.connect("mqtt.pskreporter.info", 1883, 60)
|
|
49
|
+
except:
|
|
50
|
+
print("[MQTT] connection error")
|
|
51
|
+
threading.Thread(target = mqttc.loop_forever, daemon = True).start()
|
|
52
|
+
threading.Thread(target = self.count_activity, daemon = True).start()
|
|
53
|
+
|
|
54
|
+
def on_connect(self, client, userdata, flags, reason_code, properties):
|
|
55
|
+
#pskr/filter/v2/{band}/{mode}/{sendercall}/{receivercall}/{senderlocator}/{receiverlocator}/{sendercountry}/{receivercountry}
|
|
56
|
+
print(f"[MQTT] Requesting mqtt feed for {self.home_square}")
|
|
57
|
+
client.subscribe(f"pskr/filter/v2/+/FT8/+/+/{self.home_square}/#")
|
|
58
|
+
client.subscribe(f"pskr/filter/v2/+/FT8/+/+/+/{self.home_square}/#")
|
|
59
|
+
|
|
60
|
+
def on_message(self, client, userdata, msg):
|
|
61
|
+
try:
|
|
62
|
+
d = literal_eval(msg.payload.decode())
|
|
63
|
+
except:
|
|
64
|
+
return
|
|
65
|
+
sc, rc = (d['sc'], d['sl']), (d['rc'], d['rl'])
|
|
66
|
+
for i, c in enumerate([sc, rc]):
|
|
67
|
+
call, loc = c
|
|
68
|
+
if call not in self.callsign_cache.data:
|
|
69
|
+
self.callsign_cache.data[call] = loc
|
|
70
|
+
if self.home_square in loc:
|
|
71
|
+
key = f"{d['b']}_{['Tx','Rx'][i]}_{call}"
|
|
72
|
+
if not key in self.band_TxRx_homecall_report_times.data:
|
|
73
|
+
with self.lock:
|
|
74
|
+
self.band_TxRx_homecall_report_times.data[key] = []
|
|
75
|
+
self.band_TxRx_homecall_report_times.data[key].append(time.time())
|
|
76
|
+
if d['sc'] == self.my_call:
|
|
77
|
+
if d['b'] not in self.hearing_me.data:
|
|
78
|
+
self.hearing_me.data[d['b']] = {}
|
|
79
|
+
if d['rc'] not in self.hearing_me.data[d['b']]:
|
|
80
|
+
self.hearing_me.data[d['b']][d['rc']] = {'t':time.time(), 'rp':d['rp'], 'c':d['rc']}
|
|
81
|
+
|
|
82
|
+
def count_activity(self):
|
|
83
|
+
import numpy as np
|
|
84
|
+
while True:
|
|
85
|
+
time.sleep(5)
|
|
86
|
+
self.home_activity = {}
|
|
87
|
+
self.band_TxRx_homecall_countremotes = {}
|
|
88
|
+
self.home_most_remotes = {}
|
|
89
|
+
with self.lock:
|
|
90
|
+
for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
|
|
91
|
+
b = band_TxRx_homecall.split("_")[0]
|
|
92
|
+
self.home_activity[b] = [0, 0]
|
|
93
|
+
|
|
94
|
+
for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
|
|
95
|
+
band_TxRx_homecall_report_times = self.band_TxRx_homecall_report_times.data[band_TxRx_homecall]
|
|
96
|
+
band_TxRx_homecall_report_times = [t for t in band_TxRx_homecall_report_times if (time.time() - t) < SPOTLIFE]
|
|
97
|
+
self.band_TxRx_homecall_report_times.data[band_TxRx_homecall] = band_TxRx_homecall_report_times
|
|
98
|
+
|
|
99
|
+
for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
|
|
100
|
+
band_TxRx_homecall_report_times = self.band_TxRx_homecall_report_times.data[band_TxRx_homecall]
|
|
101
|
+
if len(band_TxRx_homecall_report_times):
|
|
102
|
+
b, tr, c = band_TxRx_homecall.split("_")
|
|
103
|
+
self.home_activity[b][['Tx','Rx'].index(tr)] +=1
|
|
104
|
+
nremotes = len(band_TxRx_homecall_report_times)
|
|
105
|
+
if not b in self.home_most_remotes:
|
|
106
|
+
self.home_most_remotes[b] = [('',0), ('',0)]
|
|
107
|
+
if nremotes>self.home_most_remotes[b][['Tx','Rx'].index(tr)][1]:
|
|
108
|
+
self.home_most_remotes[b][['Tx','Rx'].index(tr)] = (c, nremotes)
|
|
109
|
+
|
|
110
|
+
for b in self.hearing_me.data:
|
|
111
|
+
newdict = {}
|
|
112
|
+
for c in self.hearing_me.data[b]:
|
|
113
|
+
if (time.time() - self.hearing_me.data[b][c]['t']) < SPOTLIFE:
|
|
114
|
+
newdict[c] = self.hearing_me.data[b][c]
|
|
115
|
+
self.hearing_me.data[b] = newdict
|
|
116
|
+
|
|
117
|
+
def get_spot_counts(self, band, call):
|
|
118
|
+
tx_reports = self.band_TxRx_homecall_report_times.data.get(f"{band}_Tx_{call}", [])
|
|
119
|
+
rx_reports = self.band_TxRx_homecall_report_times.data.get(f"{band}_Rx_{call}", [])
|
|
120
|
+
n_spotting = len(tx_reports) if tx_reports else 0
|
|
121
|
+
n_spotted = len(rx_reports) if rx_reports else 0
|
|
122
|
+
return n_spotted, n_spotting
|
|
123
|
+
|
|
124
|
+
if __name__ == '__main__':
|
|
125
|
+
pskr = PSKR_MQTT_listener("IO90")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
@@ -12,11 +12,13 @@ from PyFT8.time_utils import global_time_utils
|
|
|
12
12
|
from PyFT8.rigctrl import Rig
|
|
13
13
|
from PyFT8.hamlib import Rig_hamlib
|
|
14
14
|
from PyFT8.mqtt import PSKR_MQTT_listener
|
|
15
|
+
import PyFT8.maidenhead as maidenhead
|
|
15
16
|
|
|
16
|
-
VER = '2.
|
|
17
|
+
VER = '2.7.0'
|
|
17
18
|
|
|
18
19
|
MAX_TX_START_SECONDS = 2.5
|
|
19
20
|
rig, gui, qso, adif_logging, pskr_info, pskr_upload = None, None, None, None, None, None
|
|
21
|
+
busy_profile, hearing_me = None, None
|
|
20
22
|
|
|
21
23
|
def get_config():
|
|
22
24
|
import configparser
|
|
@@ -88,6 +90,16 @@ class ADIF:
|
|
|
88
90
|
cache[c + "_"+b+"_FT8"] = tm
|
|
89
91
|
return cache
|
|
90
92
|
|
|
93
|
+
def get_geo_text(call):
|
|
94
|
+
geo_text = ''
|
|
95
|
+
loc = pskr_info.callsign_cache.data.get(call,'')
|
|
96
|
+
if loc and config['gui']['loc'] == 'km_deg':
|
|
97
|
+
loc = maidenhead.db(config['station']['grid'], loc)
|
|
98
|
+
geo_text = f"{int(loc[0]):5d}k {int(loc[1]):3d}°"
|
|
99
|
+
if loc and config['gui']['loc'] == 'loc':
|
|
100
|
+
geo_text = f"loc: {loc}"
|
|
101
|
+
return geo_text
|
|
102
|
+
|
|
91
103
|
class Message:
|
|
92
104
|
def __init__(self, candidate):
|
|
93
105
|
c = candidate
|
|
@@ -100,12 +112,10 @@ class Message:
|
|
|
100
112
|
self.is_from_me = c.msg_tuple[1] == mycall
|
|
101
113
|
self.is_to_me = c.msg_tuple[0] == mycall
|
|
102
114
|
self.is_cq = c.msg_tuple[0].startswith('CQ')
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
gui_wb_text = f"wb: {global_time_utils.format_duration(time.time() - float(wb))}" if wb else ''
|
|
108
|
-
self.gui_text = f"{c.msg} {gui_wb_text} {qui_loc_text}"
|
|
115
|
+
geo_text = get_geo_text(c.msg_tuple[1])
|
|
116
|
+
wb_time = adif_logging.cache.get(c.msg_tuple[1],'')
|
|
117
|
+
wb_text = f"wb: {global_time_utils.format_duration(time.time() - float(wb_time))}" if wb_time else ''
|
|
118
|
+
self.gui_text = f"{c.msg} {wb_text} {geo_text}"
|
|
109
119
|
|
|
110
120
|
def wsjtx_screen_format(self):
|
|
111
121
|
return f"{self.cyclestart['string']} {self.snr:+03d} {self.dt:4.1f} {self.fHz:4.0f} ~ {self.msg}"
|
|
@@ -152,7 +162,7 @@ class FT8_QSO:
|
|
|
152
162
|
time.sleep(delay)
|
|
153
163
|
if self.tx_cycle is None:
|
|
154
164
|
self.tx_cycle = global_time_utils.curr_cycle_from_time()
|
|
155
|
-
self.tx_freq =
|
|
165
|
+
self.tx_freq = clearest_frequency
|
|
156
166
|
console_print(f"[PyFT8] Set tx cycle = {self.tx_cycle} f = {self.tx_freq:5.1f}")
|
|
157
167
|
symbols = audio_out.create_ft8_symbols(self.message_to_transmit)
|
|
158
168
|
if any(symbols):
|
|
@@ -252,42 +262,59 @@ def on_rx_decode(c):
|
|
|
252
262
|
print(message.wsjtx_screen_format())
|
|
253
263
|
write_all_txt_row(message)
|
|
254
264
|
|
|
255
|
-
def on_rx_busy_profile(
|
|
265
|
+
def on_rx_busy_profile(busy_profile_new, cycle):
|
|
266
|
+
global busy_profile
|
|
256
267
|
if output_device_idx is None:
|
|
257
268
|
return
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
if busy_profile is not None:
|
|
270
|
+
busy_profile += busy_profile_new
|
|
271
|
+
fmax = 950 if qso.band_info['b']=='60m' else 2000
|
|
272
|
+
f0_idx, fn_idx = int(500/audio_in.df), int(fmax/audio_in.df)
|
|
273
|
+
idx = np.argmin(busy_profile[f0_idx:fn_idx])
|
|
274
|
+
clearest_frequency = (f0_idx + idx) * audio_in.df
|
|
275
|
+
busy_profile = busy_profile_new
|
|
276
|
+
#console_print(f"[on_busy] Set Tx freq to {clearest_frequency:6.1f}")
|
|
263
277
|
|
|
264
278
|
#============= Callbacks for GUI ==========================================================
|
|
265
|
-
def
|
|
279
|
+
def on_gui_sidebars_refresh(gui):
|
|
266
280
|
if qso.band_info['b'] is None:
|
|
267
281
|
console_print(f"[PyFT8] Band not set; please select a band.", color = 'red')
|
|
268
|
-
if pskr_info is
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
call = config['station']['call']
|
|
283
|
-
n_spotted, n_spotting = pskr_info.get_spot_counts(b, call)
|
|
284
|
-
gui.band_stats.print(f"{call:<7} {tx_lead[0]:<7}", color = '#ff756b')
|
|
285
|
-
gui.band_stats.print(f"{n_spotting:<7} {tx_lead[1]:<7}", color = '#ff756b')
|
|
286
|
-
gui.band_stats.print(f"{call:<7} {rx_lead[0]:<7}", color = '#b6f0c6')
|
|
287
|
-
gui.band_stats.print(f"{n_spotted:<7} {rx_lead[1]:<7}", color = '#b6f0c6')
|
|
282
|
+
if pskr_info is None:
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
# refresh band stats
|
|
286
|
+
grd = config['station']['grid'][:4]
|
|
287
|
+
for bb in gui.button_boxes:
|
|
288
|
+
band = bb.clickargs.get('band','')
|
|
289
|
+
if band:
|
|
290
|
+
bb.set_active(band == qso.band_info.get('b',''))
|
|
291
|
+
if band in pskr_info.home_activity:
|
|
292
|
+
cnts = pskr_info.home_activity[band]
|
|
293
|
+
new_text = f"{cnts[0]}Tx, {cnts[1]}Rx"
|
|
294
|
+
if new_text != bb.get_info_text():
|
|
295
|
+
bb.set_info_text(new_text)
|
|
288
296
|
|
|
289
|
-
|
|
290
|
-
|
|
297
|
+
# refresh home square counts
|
|
298
|
+
b = qso.band_info['b']
|
|
299
|
+
if b is not None and b in pskr_info.home_most_remotes:
|
|
300
|
+
tx_lead, rx_lead = pskr_info.home_most_remotes[b]
|
|
301
|
+
call = config['station']['call']
|
|
302
|
+
n_spotted, n_spotting = pskr_info.get_spot_counts(b, call)
|
|
303
|
+
# add local count here for n_spotted prior to round trip to pskreporter?
|
|
304
|
+
gui.band_stats.scroll_print(f"{call:<7} {tx_lead[0]:<7}", color = '#ff756b')
|
|
305
|
+
gui.band_stats.scroll_print(f"{n_spotting:<7} {tx_lead[1]:<7}", color = '#ff756b')
|
|
306
|
+
gui.band_stats.scroll_print(f"{call:<7} {rx_lead[0]:<7}", color = '#b6f0c6')
|
|
307
|
+
gui.band_stats.scroll_print(f"{n_spotted:<7} {rx_lead[1]:<7}", color = '#b6f0c6')
|
|
308
|
+
|
|
309
|
+
#refresh hearing me
|
|
310
|
+
hearing_me_text = []
|
|
311
|
+
if b is not None and b in pskr_info.hearing_me.data:
|
|
312
|
+
for h in pskr_info.hearing_me.data[b].values():
|
|
313
|
+
geo_text = geo_text = get_geo_text(h['c'])
|
|
314
|
+
hearing_me_text.append(f"{h['c']:<7} {int(h['rp']):+03d} {geo_text:<12}")
|
|
315
|
+
gui.hm.list_print(['Hearing me:'] + hearing_me_text)
|
|
316
|
+
|
|
317
|
+
def on_gui_control_click(btn_def):
|
|
291
318
|
btn_action = btn_def['action']
|
|
292
319
|
if btn_action == "CQ":
|
|
293
320
|
mc, mg = config['station']['call'], config['station']['grid'][:4]
|
|
@@ -298,25 +325,26 @@ def on_gui_control_click(btn_widg):
|
|
|
298
325
|
console_print("[PyFT8] Set PTT Off")
|
|
299
326
|
rig.ptt_off()
|
|
300
327
|
qso.tx_cycle = None
|
|
301
|
-
if(btn_action == '
|
|
302
|
-
band, freqMHz = btn_def['
|
|
328
|
+
if(btn_action == 'SET_BAND'):
|
|
329
|
+
band, freqMHz = btn_def['band'], btn_def['freq']
|
|
303
330
|
qso.band_info = {'b':band, 'fMHz':freqMHz}
|
|
304
331
|
rig.set_freq_Hz(int(1000000*float(qso.band_info['fMHz'])))
|
|
305
332
|
console_print(f"[PyFT8] Set band: {qso.band_info['b']} {qso.band_info['fMHz']}")
|
|
306
333
|
gui.band_stats.clear()
|
|
307
|
-
|
|
334
|
+
gui.refresh_sidebars()
|
|
335
|
+
|
|
308
336
|
def on_gui_msg_click(message):
|
|
309
337
|
progress_qso(message)
|
|
310
338
|
|
|
311
339
|
#=============== CLI ========================================================================
|
|
312
340
|
def console_print(text, color = 'white'):
|
|
313
341
|
if gui is not None:
|
|
314
|
-
gui.console.
|
|
342
|
+
gui.console.scroll_print(text, color)
|
|
315
343
|
else:
|
|
316
344
|
print(text)
|
|
317
345
|
|
|
318
346
|
def cli():
|
|
319
|
-
global audio_in, audio_out, output_device_idx, rig, gui, qso, config, config_folder,
|
|
347
|
+
global audio_in, audio_out, output_device_idx, rig, gui, qso, config, config_folder, clearest_frequency, adif_logging, pskr_upload, pskr_info
|
|
320
348
|
import time
|
|
321
349
|
parser = argparse.ArgumentParser(prog='PyFT8rx', description = 'Command Line FT8 decoder')
|
|
322
350
|
parser.add_argument('-c', '--config_folder', help = 'Location of config folder e.g. C:/Users/drala/Documents/Projects/GitHub/G1OJS/PyFT8_cfg', default = './')
|
|
@@ -336,7 +364,7 @@ def cli():
|
|
|
336
364
|
if mc is not None and 'pskreporter' in config.keys():
|
|
337
365
|
if config['pskreporter']['upload'] == 'Y':
|
|
338
366
|
pskr_upload = PSKR_upload(mc, mg, software = f"PyFT8 v{VER}", console_print = console_print) if not mc is None else None
|
|
339
|
-
pskr_info = PSKR_MQTT_listener(mg[:4])
|
|
367
|
+
pskr_info = PSKR_MQTT_listener(config_folder, mc, mg[:4])
|
|
340
368
|
qso = FT8_QSO()
|
|
341
369
|
if config.has_section('hamlib_rig'):
|
|
342
370
|
console_print("Connecting to rig via Hamlib")
|
|
@@ -347,7 +375,7 @@ def cli():
|
|
|
347
375
|
|
|
348
376
|
if args.transmit_message or args.outputcard_keywords:
|
|
349
377
|
audio_out = AudioOut()
|
|
350
|
-
|
|
378
|
+
clearest_frequency = 760
|
|
351
379
|
|
|
352
380
|
if args.outputcard_keywords:
|
|
353
381
|
outputcard_keywords = args.outputcard_keywords.replace(' ','').split(',')
|
|
@@ -364,7 +392,7 @@ def cli():
|
|
|
364
392
|
if not input_device_idx:
|
|
365
393
|
console_print("No input device")
|
|
366
394
|
else:
|
|
367
|
-
gui = None if args.no_gui else Gui(audio_in.dBgrid_main, 4, 2, config,
|
|
395
|
+
gui = None if args.no_gui else Gui(audio_in.dBgrid_main, 4, 2, config, on_gui_sidebars_refresh, on_gui_msg_click, on_gui_control_click)
|
|
368
396
|
rx = Receiver(audio_in, [200, 3100], on_rx_decode, on_rx_busy_profile)
|
|
369
397
|
audio_in.start_streamed_audio(input_device_idx)
|
|
370
398
|
if gui is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.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
|
|
@@ -41,7 +41,7 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
|
|
|
41
41
|
The Gui shows:
|
|
42
42
|
- Simultaneous views of odd and even cycles
|
|
43
43
|
- Messages overlaid on waterfall signals that produce them
|
|
44
|
-
- Worked-before info and fine grid locators in the message boxes
|
|
44
|
+
- Worked-before info and fine grid locators / distance and bearing in the message boxes
|
|
45
45
|
- Band activity on band select buttons
|
|
46
46
|
- 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
|
|
47
47
|
|
|
@@ -50,8 +50,8 @@ To enable uploading of spots to pskreporter, make sure that your .ini file inclu
|
|
|
50
50
|
[pskreporter]
|
|
51
51
|
upload = Y
|
|
52
52
|
```
|
|
53
|
-
<img width="1078" height="436" alt="Capture" src="https://github.com/user-attachments/assets/5c4b64cb-e3e6-4a2f-aa77-93fd361a4e74" />
|
|
54
53
|
|
|
54
|
+
<img width="980" height="807" alt="screenshot" src="https://github.com/user-attachments/assets/ac393a05-277a-4d98-bd74-78bcb0ae8b03" />
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
## Motivation
|
|
@@ -22,7 +22,7 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
|
|
|
22
22
|
The Gui shows:
|
|
23
23
|
- Simultaneous views of odd and even cycles
|
|
24
24
|
- Messages overlaid on waterfall signals that produce them
|
|
25
|
-
- Worked-before info and fine grid locators in the message boxes
|
|
25
|
+
- Worked-before info and fine grid locators / distance and bearing in the message boxes
|
|
26
26
|
- Band activity on band select buttons
|
|
27
27
|
- 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
|
|
28
28
|
|
|
@@ -31,8 +31,8 @@ To enable uploading of spots to pskreporter, make sure that your .ini file inclu
|
|
|
31
31
|
[pskreporter]
|
|
32
32
|
upload = Y
|
|
33
33
|
```
|
|
34
|
-
<img width="1078" height="436" alt="Capture" src="https://github.com/user-attachments/assets/5c4b64cb-e3e6-4a2f-aa77-93fd361a4e74" />
|
|
35
34
|
|
|
35
|
+
<img width="980" height="807" alt="screenshot" src="https://github.com/user-attachments/assets/ac393a05-277a-4d98-bd74-78bcb0ae8b03" />
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
## Motivation
|
pyft8-2.6.1/PyFT8/gui.py
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import matplotlib.pyplot as plt
|
|
3
|
-
import time, queue
|
|
4
|
-
from matplotlib import rcParams
|
|
5
|
-
from matplotlib.animation import FuncAnimation
|
|
6
|
-
from matplotlib.widgets import Slider, Button
|
|
7
|
-
|
|
8
|
-
rcParams['toolbar'] = 'None'
|
|
9
|
-
# ================== WATERFALL ======================================================
|
|
10
|
-
|
|
11
|
-
class Scrollbox:
|
|
12
|
-
def __init__(self, fig, ax, nlines = 5):
|
|
13
|
-
self.fig, self.ax = fig, ax
|
|
14
|
-
bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
|
|
15
|
-
self.fontsize = 0.5 * bbox.height * fig.dpi / nlines
|
|
16
|
-
self.nlines = nlines
|
|
17
|
-
self.line_height = 0.9 / nlines
|
|
18
|
-
self.lines = []
|
|
19
|
-
self.lineartists = []
|
|
20
|
-
for i in range(self.nlines):
|
|
21
|
-
self.lineartists.append(self.ax.text(0.03,1 - self.line_height * (i+1),
|
|
22
|
-
'', color = 'white', fontsize = self.fontsize, family="monospace"))
|
|
23
|
-
self.ax.set_xticks([])
|
|
24
|
-
self.ax.set_yticks([])
|
|
25
|
-
self.ax.set_facecolor('black')
|
|
26
|
-
|
|
27
|
-
def print(self, text, color = 'white'):
|
|
28
|
-
self.lines = self.lines[-(self.nlines-1):]
|
|
29
|
-
self.lines.append({'text':text, 'color':color})
|
|
30
|
-
for i, line in enumerate(self.lines):
|
|
31
|
-
self.lineartists[i].set_text(line['text'])
|
|
32
|
-
self.lineartists[i].set_color(line['color'])
|
|
33
|
-
|
|
34
|
-
def clear(self):
|
|
35
|
-
for i in range(self.nlines):
|
|
36
|
-
self.print("")
|
|
37
|
-
|
|
38
|
-
class Msg_box:
|
|
39
|
-
def __init__(self, fig, ax, tbin, fbin, w, h, onclick):
|
|
40
|
-
from matplotlib.patches import Rectangle
|
|
41
|
-
self.onclick = onclick
|
|
42
|
-
rect = Rectangle((tbin, fbin), width=w, height=h, alpha=0.6, edgecolor='lime', lw=2)
|
|
43
|
-
self.patch = ax.add_patch(rect)
|
|
44
|
-
self.text_inst = ax.text(tbin, fbin+2, '', fontsize='small', fontweight='bold' )
|
|
45
|
-
self.cid = fig.canvas.mpl_connect('button_press_event', self._onclick)
|
|
46
|
-
self.expire = 0
|
|
47
|
-
|
|
48
|
-
def set_properties(self, message):
|
|
49
|
-
self.message = message
|
|
50
|
-
self.patch.set_x(message.h0_idx)
|
|
51
|
-
self.text_inst.set_x(message.h0_idx)
|
|
52
|
-
self.patch.set_visible(True)
|
|
53
|
-
self.text_inst.set_visible(True)
|
|
54
|
-
self.expire = message.expire
|
|
55
|
-
|
|
56
|
-
def set_appearance(self, message):
|
|
57
|
-
self.text_inst.set_text(message.gui_text)
|
|
58
|
-
colors = ['blue', 'white']
|
|
59
|
-
if message.is_cq: colors = ['green', 'white']
|
|
60
|
-
if message.is_from_me: colors = ['yellow', 'black']
|
|
61
|
-
if message.is_to_me: colors = ['red', 'white']
|
|
62
|
-
self.text_inst.set_color(colors[1])
|
|
63
|
-
self.patch.set_facecolor(colors[0])
|
|
64
|
-
|
|
65
|
-
def hide_if_expired(self):
|
|
66
|
-
if time.time() > self.expire > 0:
|
|
67
|
-
self.patch.set_visible(False)
|
|
68
|
-
self.text_inst.set_visible(False)
|
|
69
|
-
|
|
70
|
-
def _onclick(self, event):
|
|
71
|
-
b, _ = self.patch.contains(event)
|
|
72
|
-
if(b):
|
|
73
|
-
self.onclick(self.message)
|
|
74
|
-
|
|
75
|
-
class Gui:
|
|
76
|
-
def __init__(self, dBgrid, hps, bpt, config, update_usermessages, on_msg_click, on_control_click):
|
|
77
|
-
if config is not None:
|
|
78
|
-
self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
|
|
79
|
-
self.on_msg_click = on_msg_click
|
|
80
|
-
self.on_control_click = on_control_click
|
|
81
|
-
self.update_usermessages = update_usermessages
|
|
82
|
-
self.dBgrid = dBgrid
|
|
83
|
-
self.hps, self.bpt = hps, bpt
|
|
84
|
-
self.msg_boxes = {}
|
|
85
|
-
self.decode_queue = queue.Queue()
|
|
86
|
-
self.pmarg = 0.04
|
|
87
|
-
self.make_layout(config)
|
|
88
|
-
self.ani = FuncAnimation(self.fig, self._animate, interval = 40, frames=(100000), blit=True)
|
|
89
|
-
|
|
90
|
-
def make_layout(self, config, wf_left = 0.15, wf_top = 0.87, left_width = 0.13):
|
|
91
|
-
self.plt = plt
|
|
92
|
-
self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
|
|
93
|
-
self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
|
|
94
|
-
self.ax_wf = self.fig.add_axes([self.pmarg + wf_left, self.pmarg, 1-2*self.pmarg-wf_left, wf_top-self.pmarg])
|
|
95
|
-
self.image = self.ax_wf.imshow(self.dBgrid.T,vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
|
|
96
|
-
self.ax_wf.set_xticks([])
|
|
97
|
-
self.ax_wf.set_yticks([])
|
|
98
|
-
self.sep_h = 0.002
|
|
99
|
-
self.ax_band_stats = self.fig.add_axes([self.pmarg, wf_top + self.sep_h, left_width, 1-self.pmarg - (wf_top + self.sep_h)])
|
|
100
|
-
self.band_stats = Scrollbox(self.fig, self.ax_band_stats, nlines = 4)
|
|
101
|
-
self.ax_band_stats.text(-0.2,0.75,'Tx')
|
|
102
|
-
self.ax_band_stats.text(-0.2,0.25,'Rx')
|
|
103
|
-
if 'station' in config:
|
|
104
|
-
self.ax_band_stats.set_title(f"Spots to/from {config['station']['grid'][:4]}", fontsize = 10)
|
|
105
|
-
self.ax_console = self.fig.add_axes([self.pmarg + wf_left, wf_top, 1-2*self.pmarg - wf_left, 1-self.pmarg-wf_top])
|
|
106
|
-
self.console = Scrollbox(self.fig, self.ax_console)
|
|
107
|
-
|
|
108
|
-
if config is not None:
|
|
109
|
-
styles = {'ctrl':{'fc':'grey','c':'black'}, 'band':{'fc':'green','c':'white'}}
|
|
110
|
-
button_defs = [{'label':'CQ','style':'ctrl','action':'CQ', 'data':None}, {'label':'Repeat last','style':'ctrl','action':'RPT_LAST','data':None},
|
|
111
|
-
{'label':'Tx off','style':'ctrl','action':'TX_OFF', 'data':None}]
|
|
112
|
-
#{'label':'Averaging','style':'ctrl','data':None}]
|
|
113
|
-
for band, freq in config['bands'].items():
|
|
114
|
-
button_defs.append({'label':band,'style':'band','action':'SET_FREQ','data':freq})
|
|
115
|
-
self._make_buttons(button_defs, styles, wf_top, 0.02, left_width, 0.002)
|
|
116
|
-
|
|
117
|
-
def _make_buttons(self, btn_defs, styles, btns_top, btn_h, btn_w, sep_h):
|
|
118
|
-
self.buttons = []
|
|
119
|
-
for i, btn_def in enumerate(btn_defs):
|
|
120
|
-
btn_axs = plt.axes([self.pmarg, btns_top - (i+1) * btn_h, btn_w, btn_h-sep_h])
|
|
121
|
-
style = styles[btn_def['style']]
|
|
122
|
-
btn_widg = Button(btn_axs, '', color=style['fc'], hovercolor='skyblue')
|
|
123
|
-
btn_widg.user_data = btn_def
|
|
124
|
-
btn_widg.label = btn_axs.text(0.05, 0.5, btn_def['label'], verticalalignment='center', horizontalalignment='left',
|
|
125
|
-
color = 'white', fontweight = 'bold', transform=btn_axs.transAxes)
|
|
126
|
-
btn_widg.on_clicked(lambda event, btn_widg=btn_widg: self.on_control_click(btn_widg))
|
|
127
|
-
self.buttons.append(btn_widg)
|
|
128
|
-
|
|
129
|
-
def add_message_box(self, message):
|
|
130
|
-
self.decode_queue.put(message)
|
|
131
|
-
|
|
132
|
-
def _display_message_box(self, message):
|
|
133
|
-
h0_idx, f0_idx = message.h0_idx, message.f0_idx
|
|
134
|
-
if not f0_idx in self.msg_boxes:
|
|
135
|
-
self.msg_boxes[f0_idx] = Msg_box(self.fig, self.ax_wf, h0_idx, f0_idx, 79*self.hps, 8*self.bpt, onclick = self.on_msg_click)
|
|
136
|
-
self.msg_boxes[f0_idx].set_properties(message)
|
|
137
|
-
self.msg_boxes[f0_idx].set_appearance(message)
|
|
138
|
-
|
|
139
|
-
def _tidy_msg_boxes(self):
|
|
140
|
-
for fb in self.msg_boxes:
|
|
141
|
-
self.msg_boxes[fb].hide_if_expired()
|
|
142
|
-
|
|
143
|
-
def _animate(self, frame):
|
|
144
|
-
self.image.set_data(self.dBgrid.T)
|
|
145
|
-
while not self.decode_queue.empty():
|
|
146
|
-
self._display_message_box(self.decode_queue.get())
|
|
147
|
-
if (frame % 10 == 0):
|
|
148
|
-
self._tidy_msg_boxes()
|
|
149
|
-
if (frame % 50 == 0):
|
|
150
|
-
self.update_usermessages()
|
|
151
|
-
return [self.image, *self.ax_wf.patches, *self.ax_wf.texts, *self.band_stats.lineartists, *self.console.lineartists, *[btn.label for btn in self.buttons]]
|
|
152
|
-
|
|
153
|
-
|
pyft8-2.6.1/PyFT8/mqtt.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import paho.mqtt.client as mqtt
|
|
2
|
-
import threading
|
|
3
|
-
import time
|
|
4
|
-
from ast import literal_eval
|
|
5
|
-
|
|
6
|
-
class PSKR_MQTT_listener:
|
|
7
|
-
|
|
8
|
-
def __init__(self, home_square):
|
|
9
|
-
self.home_square = home_square
|
|
10
|
-
self.cache = {}
|
|
11
|
-
self.band_TxRx_homecall_report_times = {}
|
|
12
|
-
self.band_TxRx_homecall_countremotes = {}
|
|
13
|
-
self.home_activity = {}
|
|
14
|
-
self.home_most_remotes = {}
|
|
15
|
-
self.lock = threading.Lock()
|
|
16
|
-
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
|
17
|
-
mqttc.on_connect = self.on_connect
|
|
18
|
-
mqttc.on_message = self.on_message
|
|
19
|
-
try:
|
|
20
|
-
mqttc.connect("mqtt.pskreporter.info", 1883, 60)
|
|
21
|
-
except:
|
|
22
|
-
print("[MQTT] connection error")
|
|
23
|
-
threading.Thread(target = mqttc.loop_forever, daemon = True).start()
|
|
24
|
-
threading.Thread(target = self.count_activity, daemon = True).start()
|
|
25
|
-
|
|
26
|
-
def on_connect(self, client, userdata, flags, reason_code, properties):
|
|
27
|
-
#pskr/filter/v2/{band}/{mode}/{sendercall}/{receivercall}/{senderlocator}/{receiverlocator}/{sendercountry}/{receivercountry}
|
|
28
|
-
print(f"[MQTT] Requesting mqtt feed for {self.home_square}")
|
|
29
|
-
client.subscribe(f"pskr/filter/v2/+/FT8/+/+/{self.home_square}/#")
|
|
30
|
-
client.subscribe(f"pskr/filter/v2/+/FT8/+/+/+/{self.home_square}/#")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def on_message(self, client, userdata, msg):
|
|
34
|
-
try:
|
|
35
|
-
d = literal_eval(msg.payload.decode())
|
|
36
|
-
except:
|
|
37
|
-
return
|
|
38
|
-
sc, rc = (d['sc'], d['sl']), (d['rc'], d['rl'])
|
|
39
|
-
for i, c in enumerate([sc, rc]):
|
|
40
|
-
call, loc = c
|
|
41
|
-
if call not in self.cache:
|
|
42
|
-
self.cache[call] = loc
|
|
43
|
-
if self.home_square in loc:
|
|
44
|
-
key = f"{d['b']}_{['Tx','Rx'][i]}_{call}"
|
|
45
|
-
if not key in self.band_TxRx_homecall_report_times:
|
|
46
|
-
with self.lock:
|
|
47
|
-
self.band_TxRx_homecall_report_times[key] = []
|
|
48
|
-
self.band_TxRx_homecall_report_times[key].append(time.time())
|
|
49
|
-
|
|
50
|
-
def count_activity(self):
|
|
51
|
-
while True:
|
|
52
|
-
time.sleep(5)
|
|
53
|
-
self.band_TxRx_homecall_countremotes = {}
|
|
54
|
-
with self.lock:
|
|
55
|
-
for band_TxRx_homecall in self.band_TxRx_homecall_report_times:
|
|
56
|
-
b = band_TxRx_homecall.split("_")[0]
|
|
57
|
-
self.home_activity[b] = [0,0]
|
|
58
|
-
for band_TxRx_homecall in self.band_TxRx_homecall_report_times:
|
|
59
|
-
b, tr, c = band_TxRx_homecall.split("_")
|
|
60
|
-
report_times = self.band_TxRx_homecall_report_times[band_TxRx_homecall]
|
|
61
|
-
report_times = [t for t in report_times if (time.time() - t) < 15*60]
|
|
62
|
-
self.band_TxRx_homecall_report_times[band_TxRx_homecall] = report_times
|
|
63
|
-
nremotes = len(report_times)
|
|
64
|
-
self.home_activity[b][['Tx','Rx'].index(tr)] +=1
|
|
65
|
-
if not b in self.home_most_remotes:
|
|
66
|
-
self.home_most_remotes[b] = [('',0), ('',0)]
|
|
67
|
-
if nremotes>self.home_most_remotes[b][['Tx','Rx'].index(tr)][1]:
|
|
68
|
-
self.home_most_remotes[b][['Tx','Rx'].index(tr)] = (c, nremotes)
|
|
69
|
-
|
|
70
|
-
def get_spot_counts(self, band, call):
|
|
71
|
-
n_spotting = len(self.band_TxRx_homecall_report_times.get(f"{band}_Tx_{call}", []))
|
|
72
|
-
n_spotted = len(self.band_TxRx_homecall_report_times.get(f"{band}_Rx_{call}", []))
|
|
73
|
-
return n_spotted, n_spotting
|
|
74
|
-
|
|
75
|
-
if __name__ == '__main__':
|
|
76
|
-
pskr = PSKR_MQTT_listener("IO90")
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|