PyFT8 2.4.3__tar.gz → 2.6.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.4.3 → pyft8-2.6.0}/PKG-INFO +1 -1
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/gui.py +15 -5
- pyft8-2.6.0/PyFT8/hamlib.py +36 -0
- pyft8-2.6.0/PyFT8/mqtt.py +84 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/pskr_upload.py +9 -3
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/pyft8.py +98 -82
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/transmitter.py +1 -1
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/PKG-INFO +1 -1
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/SOURCES.txt +2 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/pyproject.toml +1 -1
- {pyft8-2.4.3 → pyft8-2.6.0}/LICENSE +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/MANIFEST.in +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/__init__.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/callhashes.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/receiver.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/rigctrl.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/time_utils.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/README.md +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/setup.cfg +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/tests/dev/CQ AAAA.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/tests/dev/osd.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/tests/dev/test_generate_wav.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/tests/dev/test_loopback_performance.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/tests/dev/view_worked_before.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/tests/plot_baseline.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/tests/spare.py +0 -0
- {pyft8-2.4.3 → pyft8-2.6.0}/tests/test_batch_and_live.py +0 -0
|
@@ -19,7 +19,7 @@ class Scrollbox:
|
|
|
19
19
|
self.lineartists = []
|
|
20
20
|
for i in range(self.nlines):
|
|
21
21
|
self.lineartists.append(self.ax.text(0.03,1 - self.line_height * (i+1),
|
|
22
|
-
'', color = 'white', fontsize = self.fontsize))
|
|
22
|
+
'', color = 'white', fontsize = self.fontsize, family="monospace"))
|
|
23
23
|
self.ax.set_xticks([])
|
|
24
24
|
self.ax.set_yticks([])
|
|
25
25
|
self.ax.set_facecolor('black')
|
|
@@ -31,6 +31,10 @@ class Scrollbox:
|
|
|
31
31
|
self.lineartists[i].set_text(line['text'])
|
|
32
32
|
self.lineartists[i].set_color(line['color'])
|
|
33
33
|
|
|
34
|
+
def clear(self):
|
|
35
|
+
for i in range(self.nlines):
|
|
36
|
+
self.print("")
|
|
37
|
+
|
|
34
38
|
class Msg_box:
|
|
35
39
|
def __init__(self, fig, ax, tbin, fbin, w, h, onclick):
|
|
36
40
|
from matplotlib.patches import Rectangle
|
|
@@ -69,11 +73,12 @@ class Msg_box:
|
|
|
69
73
|
self.onclick(self.message)
|
|
70
74
|
|
|
71
75
|
class Gui:
|
|
72
|
-
def __init__(self, dBgrid, hps, bpt, config, on_msg_click, on_control_click):
|
|
76
|
+
def __init__(self, dBgrid, hps, bpt, config, update_usermessages, on_msg_click, on_control_click):
|
|
73
77
|
if config is not None:
|
|
74
78
|
self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
|
|
75
79
|
self.on_msg_click = on_msg_click
|
|
76
80
|
self.on_control_click = on_control_click
|
|
81
|
+
self.update_usermessages = update_usermessages
|
|
77
82
|
self.dBgrid = dBgrid
|
|
78
83
|
self.hps, self.bpt = hps, bpt
|
|
79
84
|
self.msg_boxes = {}
|
|
@@ -82,7 +87,7 @@ class Gui:
|
|
|
82
87
|
self.make_layout(config)
|
|
83
88
|
self.ani = FuncAnimation(self.fig, self._animate, interval = 40, frames=(100000), blit=True)
|
|
84
89
|
|
|
85
|
-
def make_layout(self, config, wf_left = 0.15, wf_top = 0.87):
|
|
90
|
+
def make_layout(self, config, wf_left = 0.15, wf_top = 0.87, left_width = 0.13):
|
|
86
91
|
self.plt = plt
|
|
87
92
|
self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
|
|
88
93
|
self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
|
|
@@ -90,6 +95,9 @@ class Gui:
|
|
|
90
95
|
self.image = self.ax_wf.imshow(self.dBgrid.T,vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
|
|
91
96
|
self.ax_wf.set_xticks([])
|
|
92
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)
|
|
93
101
|
self.ax_console = self.fig.add_axes([self.pmarg + wf_left, wf_top, 1-2*self.pmarg - wf_left, 1-self.pmarg-wf_top])
|
|
94
102
|
self.console = Scrollbox(self.fig, self.ax_console)
|
|
95
103
|
|
|
@@ -100,7 +108,7 @@ class Gui:
|
|
|
100
108
|
#{'label':'Averaging','style':'ctrl','data':None}]
|
|
101
109
|
for band, freq in config['bands'].items():
|
|
102
110
|
button_defs.append({'label':band,'style':'band','action':'SET_FREQ','data':freq})
|
|
103
|
-
self._make_buttons(button_defs, styles, wf_top, 0.02,
|
|
111
|
+
self._make_buttons(button_defs, styles, wf_top, 0.02, left_width, 0.002)
|
|
104
112
|
|
|
105
113
|
def _make_buttons(self, btn_defs, styles, btns_top, btn_h, btn_w, sep_h):
|
|
106
114
|
self.buttons = []
|
|
@@ -132,6 +140,8 @@ class Gui:
|
|
|
132
140
|
self._display_message_box(self.decode_queue.get())
|
|
133
141
|
if (frame % 10 == 0):
|
|
134
142
|
self._tidy_msg_boxes()
|
|
135
|
-
|
|
143
|
+
if (frame % 50 == 0):
|
|
144
|
+
self.update_usermessages()
|
|
145
|
+
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]]
|
|
136
146
|
|
|
137
147
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import subprocess
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
class Rig_hamlib:
|
|
7
|
+
def __init__(self, config):
|
|
8
|
+
com = config['hamlib_rig']['port']
|
|
9
|
+
s = config['hamlib_rig']['baud_rate']
|
|
10
|
+
rigctld = config['hamlib_rig']['rigctld']
|
|
11
|
+
rig = config['hamlib_rig']['model']
|
|
12
|
+
host, port ="localhost", 4532
|
|
13
|
+
cmd = f"{rigctld} -m {rig} -r /{com} -s {s}"
|
|
14
|
+
threading.Thread(target = subprocess.run, args = (cmd,)).start()
|
|
15
|
+
self.sock = socket.create_connection((host, port))
|
|
16
|
+
|
|
17
|
+
def cmd(self, command):
|
|
18
|
+
self.sock.sendall((command + "\n").encode())
|
|
19
|
+
return self.sock.recv(1024).decode()
|
|
20
|
+
|
|
21
|
+
def set_freq_Hz(self, hz):
|
|
22
|
+
self.cmd(f"F {hz}")
|
|
23
|
+
|
|
24
|
+
def ptt_on(self):
|
|
25
|
+
self.cmd(f"T 1")
|
|
26
|
+
|
|
27
|
+
def ptt_off(self):
|
|
28
|
+
self.cmd(f"T 0")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == '__main__':
|
|
32
|
+
rig = Rig_hamlib()
|
|
33
|
+
rig.set_freq_Hz(14074000)
|
|
34
|
+
rig.ptt_on()
|
|
35
|
+
time.sleep(0.1)
|
|
36
|
+
rig.ptt_off()
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
|
|
@@ -26,6 +26,7 @@ class PSKR_upload:
|
|
|
26
26
|
self.rx_block = self._block(b"\x99\x92", rx)
|
|
27
27
|
self.console_print = console_print
|
|
28
28
|
self.lock = threading.Lock()
|
|
29
|
+
print(f"[PSKR_upload] Spots will upload to pskreporter")
|
|
29
30
|
threading.Thread(target = self._check_for_send, daemon = True).start()
|
|
30
31
|
|
|
31
32
|
def _enc_str(self, s):
|
|
@@ -71,10 +72,15 @@ class PSKR_upload:
|
|
|
71
72
|
packet = bytearray(header + self.rx_block + self._block(b"\x99\x93", senders))
|
|
72
73
|
struct.pack_into("!H", packet, 2, len(packet))
|
|
73
74
|
self.seq += len(self.reports)
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
try:
|
|
76
|
+
self.sock.sendto(packet, self.addr)
|
|
77
|
+
txt = f"[pskr_upload] Sent packet with {len(self.reports)} reports"
|
|
78
|
+
col = 'green'
|
|
79
|
+
except:
|
|
80
|
+
txt = "[PSKR_UPLOAD] Connection error"
|
|
81
|
+
col = 'red'
|
|
76
82
|
print(txt)
|
|
77
|
-
self.console_print(txt)
|
|
83
|
+
self.console_print(txt, color = col)
|
|
78
84
|
self.reports = {}
|
|
79
85
|
self.last_report_time = time.time()
|
|
80
86
|
|
|
@@ -10,11 +10,13 @@ from PyFT8.gui import Gui
|
|
|
10
10
|
from PyFT8.transmitter import AudioOut
|
|
11
11
|
from PyFT8.time_utils import global_time_utils
|
|
12
12
|
from PyFT8.rigctrl import Rig
|
|
13
|
+
from PyFT8.hamlib import Rig_hamlib
|
|
14
|
+
from PyFT8.mqtt import PSKR_MQTT_listener
|
|
13
15
|
|
|
14
|
-
VER = '2.
|
|
16
|
+
VER = '2.6.0'
|
|
15
17
|
|
|
16
18
|
MAX_TX_START_SECONDS = 2.5
|
|
17
|
-
rig, gui, qso,
|
|
19
|
+
rig, gui, qso, adif_logging, pskr_info, pskr_upload = None, None, None, None, None, None
|
|
18
20
|
|
|
19
21
|
def get_config():
|
|
20
22
|
import configparser
|
|
@@ -24,6 +26,7 @@ def get_config():
|
|
|
24
26
|
if not os.path.exists(ini_file):
|
|
25
27
|
config['station'] = {'call':'station_callsign', 'grid':'station_grid'}
|
|
26
28
|
config['bands'] = {'20m':14.074}
|
|
29
|
+
config['hamlib_rig'] = {'rigctld':'C:/WSJT/wsjtx/bin/rigctld-wsjtx', 'port': 'COM4', 'baud_rate':9600, 'model':3070}
|
|
27
30
|
config['rig'] = {'port': 'COM4', 'baud_rate':9600,
|
|
28
31
|
'set_freq_command':'FEFE88E0.05.0000000000.FD', 'set_freq_value':'5|5|vfBcdLU|1|0',
|
|
29
32
|
'ptt_on_command':'FEFE88E0.1C00.01.FD', 'ptt_off_command':'FEFE88E0.1C00.00.FD'}
|
|
@@ -34,56 +37,21 @@ def get_config():
|
|
|
34
37
|
console_print(f"Reading config from {ini_file}")
|
|
35
38
|
config.read(ini_file)
|
|
36
39
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
self.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
with open(f"{self.worked_before_file}","wb") as f:
|
|
53
|
-
pickle.dump({'dummy':0}, f)
|
|
54
|
-
self.load_wb()
|
|
55
|
-
console_print(f"Logging to {self.adif_log_file}")
|
|
56
|
-
|
|
57
|
-
def load_wb(self):
|
|
58
|
-
global worked_before
|
|
59
|
-
with open(f"{self.worked_before_file}","rb") as f:
|
|
60
|
-
worked_before = pickle.load(f)
|
|
61
|
-
|
|
62
|
-
def merge_adif_to_wb_not_used(self, file = 'c:/users/drala/recent_log.adi'):
|
|
63
|
-
import datetime
|
|
64
|
-
with open(file, 'r') as f:
|
|
65
|
-
for l in f.readlines():
|
|
66
|
-
mode = parse_from_adif_rec(l, 'mode')
|
|
67
|
-
if mode == "FT8":
|
|
68
|
-
callsign = parse_from_adif_rec(l, 'call')
|
|
69
|
-
t = parse_from_adif_rec(l, 'time_on')
|
|
70
|
-
d = parse_from_adif_rec(l, 'qso_date')
|
|
71
|
-
tm = time.mktime(datetime.datetime.strptime(d+t, "%Y%m%d%H%M%S").timetuple())
|
|
72
|
-
if callsign in worked_before:
|
|
73
|
-
if tm < worked_before[callsign]:
|
|
74
|
-
continue
|
|
75
|
-
self.update_worked_before(callsign, tm)
|
|
76
|
-
|
|
77
|
-
def update_worked_before(self, callsign, band, mode, tm):
|
|
78
|
-
global worked_before
|
|
79
|
-
self.load_wb()
|
|
80
|
-
worked_before[callsign] = tm
|
|
81
|
-
cbm = callsign + "_"+band+"_"+mode
|
|
82
|
-
worked_before[callsign] = tm
|
|
83
|
-
worked_before[cbm] = tm
|
|
84
|
-
with open(f"{self.worked_before_file}","wb") as f:
|
|
85
|
-
pickle.dump(worked_before, f)
|
|
86
|
-
|
|
40
|
+
def ensure_file_exists(path, header = None):
|
|
41
|
+
try:
|
|
42
|
+
with open(path, "x") as f:
|
|
43
|
+
if header is not None:
|
|
44
|
+
f.write(header)
|
|
45
|
+
except FileExistsError:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
class ADIF:
|
|
49
|
+
def __init__(self, logfile):
|
|
50
|
+
self.adif_log_file = logfile
|
|
51
|
+
ensure_file_exists(self.adif_log_file, header = "header <eoh>\n")
|
|
52
|
+
console_print(f"ADIF to {self.adif_log_file}")
|
|
53
|
+
self.cache = self._build_cache()
|
|
54
|
+
|
|
87
55
|
def log(self, times, band_info, mStation, oStation, rpts):
|
|
88
56
|
log_dict = {'call':oStation['c'], 'gridsquare':oStation['g'], 'mode':'FT8',
|
|
89
57
|
'operator':mStation['c'], 'station_callsign':mStation['c'], 'my_gridsquare':mStation['g'],
|
|
@@ -92,14 +60,33 @@ class Logging:
|
|
|
92
60
|
'time_on':time.strftime("%H%M%S", times['time_on']), 'time_off':time.strftime("%H%M%S", times['time_on']),
|
|
93
61
|
'band':band_info['b'], 'freq':band_info['fMHz']}
|
|
94
62
|
with open(self.adif_log_file,'a') as f:
|
|
95
|
-
f.write(f"\n")
|
|
96
63
|
for k, v in log_dict.items():
|
|
97
64
|
v = str(v)
|
|
98
65
|
f.write(f"<{k}:{len(v)}>{v} ")
|
|
99
66
|
f.write(f"<eor>\n")
|
|
100
|
-
|
|
67
|
+
cbm = log_dict['call'] + "_" + log_dict['band'] + "_FT8"
|
|
68
|
+
tm = time.time()
|
|
69
|
+
self.cache[log_dict['call']] = tm
|
|
70
|
+
self.cache[cbm] = tm
|
|
101
71
|
console_print(f"Logged QSO with {oStation['c']}")
|
|
102
72
|
|
|
73
|
+
def _build_cache(self):
|
|
74
|
+
import datetime
|
|
75
|
+
def parse(rec, field):
|
|
76
|
+
p = rec.find(field)
|
|
77
|
+
if p > 0:
|
|
78
|
+
p1, p2 = rec.find(':',p), rec.find('>',p)
|
|
79
|
+
n = int(rec[p1+1:p2])
|
|
80
|
+
return rec[p2+1: p2+1+n]
|
|
81
|
+
cache = {}
|
|
82
|
+
with open(self.adif_log_file, 'r') as f:
|
|
83
|
+
for l in f.readlines():
|
|
84
|
+
if parse(l, 'mode') == "FT8":
|
|
85
|
+
c, b, d, t = parse(l, 'call'), parse(l, 'band'), parse(l, 'qso_date'), parse(l, 'time_on')
|
|
86
|
+
tm = time.mktime(datetime.datetime.strptime(d+t, "%Y%m%d%H%M%S").timetuple())
|
|
87
|
+
cache[c] = tm
|
|
88
|
+
cache[c + "_"+b+"_FT8"] = tm
|
|
89
|
+
return cache
|
|
103
90
|
|
|
104
91
|
class Message:
|
|
105
92
|
def __init__(self, candidate):
|
|
@@ -113,12 +100,13 @@ class Message:
|
|
|
113
100
|
self.is_from_me = c.msg_tuple[1] == mycall
|
|
114
101
|
self.is_to_me = c.msg_tuple[0] == mycall
|
|
115
102
|
self.is_cq = c.msg_tuple[0].startswith('CQ')
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
103
|
+
call = c.msg_tuple[1]
|
|
104
|
+
loc = pskr_info.cache.get(call,'')
|
|
105
|
+
qui_loc_text = f"loc: {loc}" if loc else ''
|
|
106
|
+
wb = adif_logging.cache.get(call,'')
|
|
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}"
|
|
109
|
+
|
|
122
110
|
def wsjtx_screen_format(self):
|
|
123
111
|
return f"{self.cyclestart['string']} {self.snr:+03d} {self.dt:4.1f} {self.fHz:4.0f} ~ {self.msg}"
|
|
124
112
|
|
|
@@ -126,10 +114,8 @@ class Message:
|
|
|
126
114
|
fMHz = float(qso.band_info['fMHz']) if qso.band_info['fMHz'] is not None else 0
|
|
127
115
|
return f"{self.cyclestart['string']} {fMHz:8.3f} Rx FT8 {self.snr:+03d} {self.dt:4.1f} {self.fHz:4.0f} ~ {self.msg}"
|
|
128
116
|
|
|
129
|
-
|
|
130
117
|
class FT8_QSO:
|
|
131
|
-
def __init__(self
|
|
132
|
-
self.logging = logging
|
|
118
|
+
def __init__(self):
|
|
133
119
|
if config is not None:
|
|
134
120
|
self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
|
|
135
121
|
self.band_info = {'b':None, 'fMHz':0}
|
|
@@ -181,9 +167,9 @@ class FT8_QSO:
|
|
|
181
167
|
self.message_to_transmit = None
|
|
182
168
|
|
|
183
169
|
def log(self):
|
|
184
|
-
if
|
|
170
|
+
if adif_logging is not None:
|
|
185
171
|
self.times['time_off'] = time.gmtime()
|
|
186
|
-
|
|
172
|
+
adif_logging.log(self.times, self.band_info, self.mStation, self.oStation, self.rpts)
|
|
187
173
|
|
|
188
174
|
def isReport(grid_rpt): return "+" in grid_rpt or "-" in grid_rpt
|
|
189
175
|
def isRReport(grid_rpt): return isReport(grid_rpt) and 'R' in grid_rpt
|
|
@@ -210,7 +196,7 @@ def progress_qso(clicked_message):
|
|
|
210
196
|
qso.times['time_on'] = time.gmtime()
|
|
211
197
|
qso.oStation = {'c': call_b, 'g': grid_rpt}
|
|
212
198
|
qso.rpts['sent'] = f"{clicked_message.snr:+03d}"
|
|
213
|
-
qso.set_tx_message(f"{qso.oStation['c']} {my_station['c']} {my_station['g']}")
|
|
199
|
+
qso.set_tx_message(f"{qso.oStation['c']} {my_station['c']} {my_station['g'][:4]}")
|
|
214
200
|
return
|
|
215
201
|
|
|
216
202
|
if call_a == my_station['c']:
|
|
@@ -254,19 +240,19 @@ def write_all_txt_row(message):
|
|
|
254
240
|
with open(all_file, mode) as f:
|
|
255
241
|
f.write(f"{row}\n")
|
|
256
242
|
|
|
257
|
-
#============= Callbacks for
|
|
258
|
-
def
|
|
243
|
+
#============= Callbacks for Receiver ==========================================================
|
|
244
|
+
def on_rx_decode(c):
|
|
259
245
|
message = Message(c)
|
|
260
246
|
if gui:
|
|
261
247
|
gui.add_message_box(message)
|
|
262
248
|
if qso.band_info['b'] is not None and pskr_upload is not None:
|
|
263
249
|
dx_call = c.msg_tuple[1]
|
|
264
|
-
if dx_call != 'not':
|
|
250
|
+
if dx_call != 'not' and dx_call != config['station']['call']:
|
|
265
251
|
pskr_upload.add_report(dx_call, int(1000000*float(qso.band_info['fMHz'])) + c.fHz, c.snr, 'FT8', 1, int(time.time()))
|
|
266
252
|
print(message.wsjtx_screen_format())
|
|
267
253
|
write_all_txt_row(message)
|
|
268
254
|
|
|
269
|
-
def
|
|
255
|
+
def on_rx_busy_profile(busy_profile, cycle):
|
|
270
256
|
if output_device_idx is None:
|
|
271
257
|
return
|
|
272
258
|
fmax = 950 if qso.band_info['b']=='60m' else 2000
|
|
@@ -274,14 +260,37 @@ def on_busy_profile(busy_profile, cycle):
|
|
|
274
260
|
idx = np.argmin(busy_profile[f0_idx:fn_idx])
|
|
275
261
|
clear_frequencies[cycle] = (f0_idx + idx) * audio_in.df
|
|
276
262
|
console_print(f"[on_busy] Set Tx freq to {clear_frequencies[cycle]:6.1f} for cycle {cycle}")
|
|
263
|
+
|
|
264
|
+
#============= Callbacks for GUI ==========================================================
|
|
265
|
+
def gui_update_usermessages():
|
|
277
266
|
if qso.band_info['b'] is None:
|
|
278
267
|
console_print(f"[PyFT8] Band not set; please select a band.", color = 'red')
|
|
279
|
-
|
|
280
|
-
|
|
268
|
+
if pskr_info is not None and gui is not None:
|
|
269
|
+
grd = config['station']['grid'][:4]
|
|
270
|
+
#s = [f"{b} {cnts[0]}/{cnts[1]} " for b, cnts in pskr_info.home_activity.items()]
|
|
271
|
+
#console_print(f"Tx/Rx calls in {grd}: {' '.join(s)}", color = 'yellow')
|
|
272
|
+
for bw in gui.buttons:
|
|
273
|
+
band_text = bw.user_data['label']
|
|
274
|
+
if band_text in pskr_info.home_activity:
|
|
275
|
+
cnts = pskr_info.home_activity[band_text]
|
|
276
|
+
activity_text = f"{cnts[0]}t/{cnts[1]}r"
|
|
277
|
+
bw.label.set_text(f"{band_text} {activity_text}")
|
|
278
|
+
|
|
279
|
+
b = qso.band_info['b']
|
|
280
|
+
if b is not None and b in pskr_info.home_most_remotes:
|
|
281
|
+
tx_lead, rx_lead = pskr_info.home_most_remotes[b]
|
|
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 = 'red')
|
|
285
|
+
gui.band_stats.print(f"{n_spotting:<7} {tx_lead[1]:<7}", color = 'red')
|
|
286
|
+
gui.band_stats.print(f"{call:<7} {rx_lead[0]:<7}", color = 'green')
|
|
287
|
+
gui.band_stats.print(f"{n_spotted:<7} {rx_lead[1]:<7}", color = 'green')
|
|
288
|
+
|
|
289
|
+
def on_gui_control_click(btn_widg):
|
|
281
290
|
btn_def = btn_widg.user_data
|
|
282
291
|
btn_action = btn_def['action']
|
|
283
292
|
if btn_action == "CQ":
|
|
284
|
-
mc, mg = config['station']['call'], config['station']['grid']
|
|
293
|
+
mc, mg = config['station']['call'], config['station']['grid'][:4]
|
|
285
294
|
qso.set_tx_message(f"CQ {mc} {mg}")
|
|
286
295
|
if btn_action == "RPT_LAST":
|
|
287
296
|
qso.set_tx_message(qso.last_tx)
|
|
@@ -290,12 +299,13 @@ def on_control_click(btn_widg):
|
|
|
290
299
|
rig.ptt_off()
|
|
291
300
|
qso.tx_cycle = None
|
|
292
301
|
if(btn_action == 'SET_FREQ'):
|
|
293
|
-
btn_text, freqMHz =
|
|
302
|
+
btn_text, freqMHz = btn_def['label'], btn_def['data']
|
|
294
303
|
qso.band_info = {'b':btn_text, 'fMHz':freqMHz}
|
|
295
304
|
rig.set_freq_Hz(int(1000000*float(qso.band_info['fMHz'])))
|
|
296
305
|
console_print(f"[PyFT8] Set band: {qso.band_info['b']} {qso.band_info['fMHz']}")
|
|
306
|
+
gui.band_stats.clear()
|
|
297
307
|
|
|
298
|
-
def
|
|
308
|
+
def on_gui_msg_click(message):
|
|
299
309
|
progress_qso(message)
|
|
300
310
|
|
|
301
311
|
#=============== CLI ========================================================================
|
|
@@ -306,7 +316,7 @@ def console_print(text, color = 'white'):
|
|
|
306
316
|
print(text)
|
|
307
317
|
|
|
308
318
|
def cli():
|
|
309
|
-
global audio_in, audio_out, output_device_idx, rig, gui, qso, config, config_folder, clear_frequencies, pskr_upload
|
|
319
|
+
global audio_in, audio_out, output_device_idx, rig, gui, qso, config, config_folder, clear_frequencies, adif_logging, pskr_upload, pskr_info
|
|
310
320
|
import time
|
|
311
321
|
parser = argparse.ArgumentParser(prog='PyFT8rx', description = 'Command Line FT8 decoder')
|
|
312
322
|
parser.add_argument('-c', '--config_folder', help = 'Location of config folder e.g. C:/Users/drala/Documents/Projects/GitHub/G1OJS/PyFT8_cfg', default = './')
|
|
@@ -321,14 +331,19 @@ def cli():
|
|
|
321
331
|
output_device_idx = None
|
|
322
332
|
config_folder = f"{args.config_folder}".strip()
|
|
323
333
|
get_config()
|
|
324
|
-
|
|
334
|
+
adif_logging = ADIF(f"{config_folder}/PyFT8.adi")
|
|
325
335
|
mc, mg = config['station']['call'], config['station']['grid']
|
|
326
336
|
if mc is not None and 'pskreporter' in config.keys():
|
|
327
337
|
if config['pskreporter']['upload'] == 'Y':
|
|
328
338
|
pskr_upload = PSKR_upload(mc, mg, software = f"PyFT8 v{VER}", console_print = console_print) if not mc is None else None
|
|
329
|
-
|
|
330
|
-
qso = FT8_QSO(
|
|
331
|
-
|
|
339
|
+
pskr_info = PSKR_MQTT_listener(mg[:4])
|
|
340
|
+
qso = FT8_QSO()
|
|
341
|
+
if config.has_section('hamlib_rig'):
|
|
342
|
+
console_print("Connecting to rig via Hamlib")
|
|
343
|
+
rig = Rig_hamlib(config)
|
|
344
|
+
else:
|
|
345
|
+
console_print("Connecting to rig via CAT")
|
|
346
|
+
rig = Rig(config)
|
|
332
347
|
|
|
333
348
|
if args.transmit_message or args.outputcard_keywords:
|
|
334
349
|
audio_out = AudioOut()
|
|
@@ -349,8 +364,8 @@ def cli():
|
|
|
349
364
|
if not input_device_idx:
|
|
350
365
|
console_print("No input device")
|
|
351
366
|
else:
|
|
352
|
-
gui = None if args.no_gui else Gui(audio_in.dBgrid_main, 4, 2, config,
|
|
353
|
-
rx = Receiver(audio_in, [200, 3100],
|
|
367
|
+
gui = None if args.no_gui else Gui(audio_in.dBgrid_main, 4, 2, config, gui_update_usermessages, on_gui_msg_click, on_gui_control_click)
|
|
368
|
+
rx = Receiver(audio_in, [200, 3100], on_rx_decode, on_rx_busy_profile)
|
|
354
369
|
audio_in.start_streamed_audio(input_device_idx)
|
|
355
370
|
if gui is not None:
|
|
356
371
|
gui.plt.show()
|
|
@@ -366,4 +381,5 @@ if __name__ == "__main__":
|
|
|
366
381
|
#with mock.patch('sys.argv', ['pyft8', '-i Mic, CODEC', '-n']):
|
|
367
382
|
#with mock.patch('sys.argv', ['pyft8', '-m', "CQ G1OJS IO90", '-cC:/Users/drala/Documents/Projects/GitHub/G1OJS/PyFT8_cfg']):
|
|
368
383
|
#with mock.patch('sys.argv', ['pyft8', '-m', "CQ G1OJS IO90", '-o', "Speak, CODEC"]):
|
|
384
|
+
#with mock.patch('sys.argv', ['pyft8', '-m', "CQ G1OJS IO90"]):
|
|
369
385
|
cli()
|
|
@@ -2,7 +2,7 @@ import numpy as np
|
|
|
2
2
|
import wave
|
|
3
3
|
import pyaudio
|
|
4
4
|
import time
|
|
5
|
-
from PyFT8.callhashes import
|
|
5
|
+
from PyFT8.callhashes import add_call_hashes
|
|
6
6
|
|
|
7
7
|
#==================== AUDIO OUT ================================================================
|
|
8
8
|
|
|
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
|