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.
Files changed (31) hide show
  1. {pyft8-2.4.3 → pyft8-2.6.0}/PKG-INFO +1 -1
  2. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/gui.py +15 -5
  3. pyft8-2.6.0/PyFT8/hamlib.py +36 -0
  4. pyft8-2.6.0/PyFT8/mqtt.py +84 -0
  5. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/pskr_upload.py +9 -3
  6. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/pyft8.py +98 -82
  7. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/transmitter.py +1 -1
  8. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/PKG-INFO +1 -1
  9. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/SOURCES.txt +2 -0
  10. {pyft8-2.4.3 → pyft8-2.6.0}/pyproject.toml +1 -1
  11. {pyft8-2.4.3 → pyft8-2.6.0}/LICENSE +0 -0
  12. {pyft8-2.4.3 → pyft8-2.6.0}/MANIFEST.in +0 -0
  13. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/__init__.py +0 -0
  14. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/callhashes.py +0 -0
  15. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/receiver.py +0 -0
  16. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/rigctrl.py +0 -0
  17. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8/time_utils.py +0 -0
  18. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/dependency_links.txt +0 -0
  19. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/entry_points.txt +0 -0
  20. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/requires.txt +0 -0
  21. {pyft8-2.4.3 → pyft8-2.6.0}/PyFT8.egg-info/top_level.txt +0 -0
  22. {pyft8-2.4.3 → pyft8-2.6.0}/README.md +0 -0
  23. {pyft8-2.4.3 → pyft8-2.6.0}/setup.cfg +0 -0
  24. {pyft8-2.4.3 → pyft8-2.6.0}/tests/dev/CQ AAAA.py +0 -0
  25. {pyft8-2.4.3 → pyft8-2.6.0}/tests/dev/osd.py +0 -0
  26. {pyft8-2.4.3 → pyft8-2.6.0}/tests/dev/test_generate_wav.py +0 -0
  27. {pyft8-2.4.3 → pyft8-2.6.0}/tests/dev/test_loopback_performance.py +0 -0
  28. {pyft8-2.4.3 → pyft8-2.6.0}/tests/dev/view_worked_before.py +0 -0
  29. {pyft8-2.4.3 → pyft8-2.6.0}/tests/plot_baseline.py +0 -0
  30. {pyft8-2.4.3 → pyft8-2.6.0}/tests/spare.py +0 -0
  31. {pyft8-2.4.3 → pyft8-2.6.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.4.3
3
+ Version: 2.6.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
@@ -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, 0.1, 0.002)
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
- return [self.image, *self.ax_wf.patches, *self.ax_wf.texts, *self.console.lineartists]
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
- self.sock.sendto(packet, self.addr)
75
- txt = f"[pskr_upload] Sent packet with {len(self.reports)} reports"
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.4.3'
16
+ VER = '2.6.0'
15
17
 
16
18
  MAX_TX_START_SECONDS = 2.5
17
- rig, gui, qso, worked_before, pskr_upload = None, None, None, None, None
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 parse_from_adif_rec(rec, field):
38
- p = rec.find(field)
39
- if p > 0:
40
- p1, p2 = rec.find(':',p), rec.find('>',p)
41
- n = int(rec[p1+1:p2])
42
- return rec[p2+1: p2+1+n]
43
-
44
- class Logging:
45
- def __init__(self):
46
- self.adif_log_file = f"{config_folder}/PyFT8.adi"
47
- self.worked_before_file = f"{config_folder}/PyFT8_wb.pkl"
48
- if(not os.path.exists(self.adif_log_file)):
49
- with open(self.adif_log_file, 'w') as f:
50
- f.write("header <eoh>")
51
- if(not os.path.exists(self.worked_before_file)):
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
- self.update_worked_before(oStation['c'], band_info['b'], 'FT8', time.time())
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
- gui_wb_text = ''
117
- if self.is_cq and worked_before is not None:
118
- if c.msg_tuple[1] in worked_before:
119
- gui_wb_text = f"wb: {global_time_utils.format_duration(time.time() - worked_before[c.msg_tuple[1]])}"
120
- self.gui_text = f"{c.msg} {gui_wb_text}"
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, logging):
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 self.logging is not None:
170
+ if adif_logging is not None:
185
171
  self.times['time_off'] = time.gmtime()
186
- self.logging.log(self.times, self.band_info, self.mStation, self.oStation, self.rpts)
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 GUI ==========================================================
258
- def on_decode(c):
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 on_busy_profile(busy_profile, cycle):
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
- def on_control_click(btn_widg):
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 = btn_widg.label.get_text(), btn_def['data']
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 on_msg_click(message):
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
- logging = Logging()
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
- console_print(f"[PyFT8] Spots will upload to pskreporter")
330
- qso = FT8_QSO(logging)
331
- rig = Rig(config)
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, on_msg_click, on_control_click)
353
- rx = Receiver(audio_in, [200, 3100], on_decode, on_busy_profile)
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 call_hashes, add_call_hashes
5
+ from PyFT8.callhashes import add_call_hashes
6
6
 
7
7
  #==================== AUDIO OUT ================================================================
8
8
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 2.4.3
3
+ Version: 2.6.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
@@ -5,6 +5,8 @@ pyproject.toml
5
5
  PyFT8/__init__.py
6
6
  PyFT8/callhashes.py
7
7
  PyFT8/gui.py
8
+ PyFT8/hamlib.py
9
+ PyFT8/mqtt.py
8
10
  PyFT8/pskr_upload.py
9
11
  PyFT8/pyft8.py
10
12
  PyFT8/receiver.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyFT8"
3
- version = "2.4.3"
3
+ version = "2.6.0"
4
4
  license = "GPL-3.0-or-later"
5
5
 
6
6
  authors = [
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