-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathpskreporter-sender
More file actions
executable file
·224 lines (185 loc) · 7.38 KB
/
pskreporter-sender
File metadata and controls
executable file
·224 lines (185 loc) · 7.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#!/usr/bin/env python
"""
FTx sender to PSKReporter
Usage:
pskreporter-sender [options] <logfile> [<mode>]
pskreporter-sender -h | --help
Options:
-h --help Show this screen.
--no-send Don't send, just print the spots
--tcp Use TCP to send reports
--callsign=<callsign> The callsign of the reporter
--locator=<locator> The locator for the radio -- at least 6 characters
--antenna=<antenna> The antenna used on the radio
"""
from pskreporter import PskReporter
import re
from datetime import datetime, time, timezone, timedelta
import subprocess
from docopt import docopt
import sys
import logging
import signal
def get_unix_timestamp(hhmm: str) -> int:
# Get the current date and time
now = datetime.now(timezone.utc)
today = now.date()
# Parse the HHMM string
hours = int(hhmm[:2])
minutes = int(hhmm[2:])
# Create a datetime object for today at the specified time
specified_time = datetime.combine(today, time(hours, minutes)).replace(tzinfo=timezone.utc)
# If the specified time is in the future, move to the previous day
if specified_time > now:
specified_time -= timedelta(days=1)
# Get the Unix timestamp
unix_timestamp = int(specified_time.timestamp())
return unix_timestamp
def do_wspr(stream, pskreporter, mode):
r1 = re.compile(
r"(?P<ts>[0-9]{4}) +(?P<snr>[-0-9]+) +(?P<u2>[-.0-9]+) +(?P<freq>[0-9.]+) +(?P<u3>[-.0-9]+) +<?(?P<callsign>[A-Z0-9][A-Z0-9/-]+)>? +(?P<locator>[A-Z0-9]+) +(?P<power>[0-9]+) *$"
)
r2 = re.compile(
r"(?P<ymd>[0-9]{6}) +(?P<ts>[0-9]{4}) +(?P<u1>[-.0-9]+) +(?P<snr>[-0-9]+) +(?P<u2>[-.0-9]+) +(?P<freq>[0-9.]+) +<?(?P<callsign>[A-Z0-9][A-Z0-9/-]+)>? +(?P<locator>[A-Z0-9]+) +(?P<power>[0-9]+)"
)
raw_bits_r = re.compile(r"^(?P<msg>.*?)\s+#(?P<hex>[0-9a-fA-F]{16,})")
for line in stream:
# 0044 -26 0.2 14.097059 0 W5NR EM10 23
# or
# 241104 0024 0.21 -18 0.02 18.1060273 N3BBF FM19 37 0 2
# 260206 2312 0.27 -23 -0.74 18.1060907 W7BJH/R 37 -2 2
original_line = line.decode("utf-8").rstrip()
line = original_line
try:
m = raw_bits_r.search(line)
if m:
raw_hex = m.group("hex")
line = m.group("msg")
else:
raw_hex = None
m = r1.search(line)
if not m:
m = r2.search(line)
if m:
ts = get_unix_timestamp(m.group("ts"))
snr = int(m.group("snr"))
freq = 1000000.0 * float(m.group("freq"))
pskreporter.spot(
callsign=m.group("callsign"),
mode=mode,
timestamp=ts,
frequency=freq,
db=snr,
locator=m.group("locator"),
hexbytes=raw_hex,
)
elif not line.startswith("<Decode"):
print(f"Failed to parse: {original_line}")
except Exception as e:
logging.error(f"Failed to create spot ({e}): {original_line}")
def submit(stream, pskreporter, mode):
# It turns out that the field that I thought was snr is actually a score and is fairly unrelated to snr.
r = re.compile(
r"(?P<ts>[0-9/]+ [0-9:]+) +(?P<score>[-0-9]+) +(?P<dt>[-+.0-9]+) (?P<freq>[0-9,.]+) ~ (?P<msg>.*)$"
)
cq = re.compile(
r"CQ (DX )?(?P<callsign>[A-Z0-9][A-Z0-9/-]+[A-Z0-9]) (?P<locator>[A-Z][A-Z][0-9][0-9]) *$"
)
cq2 = re.compile(r"CQ (DX )?(?P<callsign>[A-Z0-9][A-Z0-9/-]+[A-Z0-9]) *(?P<locator>)$")
tx2 = re.compile(
r"(?P<their_callsign>([A-Z0-9][A-Z0-9/-]+[A-Z0-9])|(<\.\.\.>)) (?P<callsign>[A-Z0-9][A-Z0-9/-]+[A-Z0-9]) (?P<locator>[A-Z][A-Z][0-9][0-9]) *$"
)
raw_bits_r = re.compile(r"^(?P<msg>.*?)\s+#(?P<hex>[0-9a-fA-F]{16,})")
for line in stream:
# 2024/05/02 02:01:00 18 +1.44 7,075,718.8 ~ CQ NF3R FN20 #abcdef0123456789
# 2024/05/02 02:01:00 18 +0.88 7,074,100.0 ~ CQ EA7LZ IM76
original_line = line.decode("utf-8").rstrip()
line = original_line
try:
m = raw_bits_r.search(line)
if m:
raw_hex = m.group("hex")
line = m.group("msg")
else:
raw_hex = None
m = r.search(line)
if m:
msg = m.group("msg")
match = cq.search(msg)
if not match:
match = cq2.search(msg)
if not match:
match = tx2.search(msg)
if match and match.group("locator") == "RR73":
match = None
if match:
ts = datetime.strptime(
m.group("ts"), "%Y/%m/%d %H:%M:%S"
).replace(tzinfo=timezone.utc).timestamp()
freq = float(m.group("freq").replace(",", ""))
dt = float(m.group('dt'))
pskreporter.spot(
callsign=match.group("callsign"),
mode=mode,
timestamp=ts,
frequency=freq,
locator=match.group("locator"),
hexbytes=raw_hex,
dt=dt*1000,
)
except Exception as e:
logging.error(f"Failed to create spot ({e}): {original_line}")
if __name__ == "__main__":
args = docopt(__doc__)
logging.warning(f"Starting pskreporter-sender with args {args}")
file = args["<logfile>"]
mode = args["<mode>"]
if not mode:
if "ft8" in file.lower():
mode = "ft8"
elif "ft4" in file.lower():
mode = "ft4"
elif "wspr" in file.lower():
mode = "wspr"
if not mode:
sys.exit("Need to provide the mode argument")
if not args["--callsign"]:
sys.exit("Need to provide the callsign argument")
if not args["--locator"]:
sys.exit("Need to provide the locator argument")
pskreporter = PskReporter(
callsign=args["--callsign"].upper(),
grid=args["--locator"],
antenna=args["--antenna"],
dummy=args["--no-send"],
tcp=args["--tcp"],
)
proc = subprocess.Popen(["tail", "-F", file], stdout=subprocess.PIPE)
# Set up signal handlers for graceful shutdown
def signal_handler(signum, frame):
print(f"\nReceived signal {signum}, shutting down gracefully...")
raise KeyboardInterrupt
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
try:
if mode == "wspr":
do_wspr(proc.stdout, pskreporter, mode.upper())
else:
submit(proc.stdout, pskreporter, mode.upper())
except KeyboardInterrupt:
pass
except Exception as e:
logging.error(f"Unexpected error: {e}")
finally:
# Disable signal handlers during cleanup to prevent threading issues
signal.signal(signal.SIGTERM, signal.SIG_DFL)
signal.signal(signal.SIGINT, signal.SIG_DFL)
try:
pskreporter.close()
print("Final upload completed successfully.")
except Exception as e:
logging.error(f"Error during final upload: {e}")
finally:
proc.terminate()
proc.wait()
sys.exit(0)