qso-graph-sim 0.1.0__py3-none-any.whl
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.
- qso_graph_sim/__init__.py +15 -0
- qso_graph_sim/alltxt.py +103 -0
- qso_graph_sim/cli.py +138 -0
- qso_graph_sim/player.py +148 -0
- qso_graph_sim/samples/ALL-sample.TXT +120 -0
- qso_graph_sim/wsjtx.py +315 -0
- qso_graph_sim-0.1.0.dist-info/METADATA +120 -0
- qso_graph_sim-0.1.0.dist-info/RECORD +11 -0
- qso_graph_sim-0.1.0.dist-info/WHEEL +4 -0
- qso_graph_sim-0.1.0.dist-info/entry_points.txt +2 -0
- qso_graph_sim-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""qso-graph-sim: replays what ham radio programs send on the network, for testing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
_pkg_version = version("qso-graph-sim")
|
|
10
|
+
except PackageNotFoundError: # source tree without dist metadata
|
|
11
|
+
_pkg_version = "0.0.0-dev"
|
|
12
|
+
|
|
13
|
+
# Read from the installed distribution, so pyproject.toml is the only place
|
|
14
|
+
# the version is written.
|
|
15
|
+
__version__: Final[str] = _pkg_version
|
qso_graph_sim/alltxt.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Reading WSJT-X's ALL.TXT, the log of every decode it has made.
|
|
2
|
+
|
|
3
|
+
One line per decode, in the format WSJT-X has written since 2.x::
|
|
4
|
+
|
|
5
|
+
260407_233915 14.074 Rx FT8 -12 0.2 1234 CQ K7ABC DN13
|
|
6
|
+
|
|
7
|
+
date_time (UTC), dial frequency in MHz, Rx or Tx, mode, SNR in dB, DT in
|
|
8
|
+
seconds, audio offset in Hz, then the message.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
|
|
17
|
+
_LINE = re.compile(
|
|
18
|
+
r"^(\d{6}_\d{6})\s+(\d+(?:\.\d+)?)\s+(Rx|Tx)\s+(\S+)\s+(-?\d+)\s+(-?\d+(?:\.\d+)?)"
|
|
19
|
+
r"\s+(\d+)\s+(.+?)\s*$"
|
|
20
|
+
)
|
|
21
|
+
# WSJT-X marks a decode it has low confidence in with a trailing "?"; it is
|
|
22
|
+
# not part of the message that was sent.
|
|
23
|
+
_LOW_CONFIDENCE = re.compile(r"\s\?$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Line:
|
|
28
|
+
stamp: str # as written: YYMMDD_HHMMSS
|
|
29
|
+
utc: datetime
|
|
30
|
+
dial_hz: int
|
|
31
|
+
direction: str # "Rx" or "Tx"
|
|
32
|
+
mode: str # the mode's name, e.g. "FT8"
|
|
33
|
+
snr: int
|
|
34
|
+
dt: float
|
|
35
|
+
df: int
|
|
36
|
+
message: str
|
|
37
|
+
low_confidence: bool
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_line(text: str) -> Line | None:
|
|
41
|
+
"""One ALL.TXT line, or None if it is not one."""
|
|
42
|
+
m = _LINE.match(text)
|
|
43
|
+
if not m:
|
|
44
|
+
return None
|
|
45
|
+
stamp, mhz, direction, mode, snr, dt, df, message = m.groups()
|
|
46
|
+
try:
|
|
47
|
+
utc = datetime.strptime(stamp, "%y%m%d_%H%M%S").replace(tzinfo=timezone.utc)
|
|
48
|
+
except ValueError:
|
|
49
|
+
return None
|
|
50
|
+
low = bool(_LOW_CONFIDENCE.search(message))
|
|
51
|
+
return Line(
|
|
52
|
+
stamp=stamp,
|
|
53
|
+
utc=utc,
|
|
54
|
+
dial_hz=round(float(mhz) * 1_000_000),
|
|
55
|
+
direction=direction,
|
|
56
|
+
mode=mode,
|
|
57
|
+
snr=int(snr),
|
|
58
|
+
dt=float(dt),
|
|
59
|
+
df=int(df),
|
|
60
|
+
message=_LOW_CONFIDENCE.sub("", message) if low else message,
|
|
61
|
+
low_confidence=low,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Cycle:
|
|
67
|
+
"""The decodes of one T/R period on one dial frequency."""
|
|
68
|
+
|
|
69
|
+
stamp: str
|
|
70
|
+
dial_hz: int
|
|
71
|
+
mode: str
|
|
72
|
+
decodes: list[Line] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class Log:
|
|
77
|
+
cycles: list[Cycle]
|
|
78
|
+
skipped: int # Tx lines and lines that are not decodes
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def decodes(self) -> int:
|
|
82
|
+
return sum(len(c.decodes) for c in self.cycles)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def read(text: str, start: str = "") -> Log:
|
|
86
|
+
"""Group received decodes into cycles, in file order, from ``start`` on."""
|
|
87
|
+
cycles: list[Cycle] = []
|
|
88
|
+
skipped = 0
|
|
89
|
+
for raw in text.splitlines():
|
|
90
|
+
if not raw.strip():
|
|
91
|
+
continue
|
|
92
|
+
line = parse_line(raw)
|
|
93
|
+
if line is None or line.direction != "Rx":
|
|
94
|
+
skipped += 1
|
|
95
|
+
continue
|
|
96
|
+
if start and line.stamp < start:
|
|
97
|
+
continue
|
|
98
|
+
last = cycles[-1] if cycles else None
|
|
99
|
+
if last and last.stamp == line.stamp and last.dial_hz == line.dial_hz:
|
|
100
|
+
last.decodes.append(line)
|
|
101
|
+
else:
|
|
102
|
+
cycles.append(Cycle(line.stamp, line.dial_hz, line.mode, [line]))
|
|
103
|
+
return Log(cycles, skipped)
|
qso_graph_sim/cli.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""The qso-graph-sim command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import ipaddress
|
|
7
|
+
import re
|
|
8
|
+
import signal
|
|
9
|
+
import socket
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Callable, Sequence
|
|
12
|
+
from importlib.resources import files
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from . import __version__, alltxt
|
|
16
|
+
from .player import Player, Station
|
|
17
|
+
|
|
18
|
+
SAMPLE = "ALL-sample.TXT"
|
|
19
|
+
_STAMP = re.compile(r"^\d{6}_\d{6}$")
|
|
20
|
+
_CALL = re.compile(r"^[A-Z0-9/]{0,13}$")
|
|
21
|
+
_GRID = re.compile(r"^([A-R]{2}\d{2}([A-X]{2})?)?$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def target(text: str) -> tuple[str, int]:
|
|
25
|
+
"""IP:PORT, or [IPv6]:PORT. An address, not a name: nothing is looked up."""
|
|
26
|
+
m = re.match(r"^\[?([^\]]+?)\]?:(\d{1,5})$", text)
|
|
27
|
+
if not m:
|
|
28
|
+
raise argparse.ArgumentTypeError(
|
|
29
|
+
"use IP:PORT, the address QSO Graph Desktop listens on"
|
|
30
|
+
)
|
|
31
|
+
host, port = m.group(1), int(m.group(2))
|
|
32
|
+
try:
|
|
33
|
+
ipaddress.ip_address(host)
|
|
34
|
+
except ValueError:
|
|
35
|
+
raise argparse.ArgumentTypeError(f"{host} is not an IP address") from None
|
|
36
|
+
if not 0 < port < 65536:
|
|
37
|
+
raise argparse.ArgumentTypeError(f"{port} is not a port")
|
|
38
|
+
return host, port
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _matching(
|
|
42
|
+
pattern: re.Pattern[str], what: str, upper: bool = False
|
|
43
|
+
) -> Callable[[str], str]:
|
|
44
|
+
def check(text: str) -> str:
|
|
45
|
+
if text == "":
|
|
46
|
+
return text # the default: not given
|
|
47
|
+
value = text.upper() if upper else text
|
|
48
|
+
if not pattern.match(value):
|
|
49
|
+
raise argparse.ArgumentTypeError(f"{text!r} is not {what}")
|
|
50
|
+
return value
|
|
51
|
+
return check
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _speed(text: str) -> float:
|
|
55
|
+
try:
|
|
56
|
+
v = float(text)
|
|
57
|
+
except ValueError:
|
|
58
|
+
raise argparse.ArgumentTypeError(f"{text!r} is not a number") from None
|
|
59
|
+
if not 0 < v <= 1000:
|
|
60
|
+
raise argparse.ArgumentTypeError("speed is more than 0 and at most 1000")
|
|
61
|
+
return v
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
65
|
+
p = argparse.ArgumentParser(
|
|
66
|
+
prog="qso-graph-sim",
|
|
67
|
+
description=(
|
|
68
|
+
"Replays what a ham radio program sends on the network, from its own log, "
|
|
69
|
+
"so a receiver can be tested without a radio."
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
73
|
+
app = p.add_mutually_exclusive_group(required=True)
|
|
74
|
+
app.add_argument("--wsjtx", action="store_true",
|
|
75
|
+
help="WSJT-X: play an ALL.TXT as its UDP messages")
|
|
76
|
+
p.add_argument("--to", type=target, required=True, metavar="IP:PORT",
|
|
77
|
+
help="where to send: the receiver's address and port (WSJT-X uses 2237)")
|
|
78
|
+
p.add_argument("--file", type=Path, metavar="PATH",
|
|
79
|
+
help="the log to play (default: a sample of 20 m FT8, 120 decodes)")
|
|
80
|
+
p.add_argument("--from", dest="start", type=_matching(_STAMP, "YYMMDD_HHMMSS"),
|
|
81
|
+
default="", metavar="YYMMDD_HHMMSS", help="start at this time in the log")
|
|
82
|
+
p.add_argument("--id", default="WSJT-X", metavar="NAME",
|
|
83
|
+
type=_matching(re.compile(r"^.{1,64}$"), "an Id"),
|
|
84
|
+
help="the instance name to send as (WSJT-X's default is WSJT-X)")
|
|
85
|
+
p.add_argument("--call", default="", type=_matching(_CALL, "a callsign", upper=True),
|
|
86
|
+
help="your callsign, sent in Status")
|
|
87
|
+
p.add_argument("--grid", default="", type=_matching(_GRID, "a 4- or 6-character grid"),
|
|
88
|
+
help="your grid, sent in Status")
|
|
89
|
+
p.add_argument("--speed", type=_speed, default=1.0, metavar="N",
|
|
90
|
+
help="play faster than real time, for testing (default 1)")
|
|
91
|
+
p.add_argument("--loop", action="store_true", help="start again at the end")
|
|
92
|
+
return p
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _read(path: Path | None) -> tuple[str, str]:
|
|
96
|
+
if path is None:
|
|
97
|
+
sample = files("qso_graph_sim").joinpath("samples", SAMPLE)
|
|
98
|
+
return sample.read_text("utf-8"), "the sample"
|
|
99
|
+
return path.read_text("utf-8", errors="replace"), str(path)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
103
|
+
args = build_parser().parse_args(argv)
|
|
104
|
+
try:
|
|
105
|
+
text, name = _read(args.file)
|
|
106
|
+
except OSError as e:
|
|
107
|
+
print(f"qso-graph-sim: cannot read {args.file}: {e.strerror}", file=sys.stderr)
|
|
108
|
+
return 1
|
|
109
|
+
log = alltxt.read(text, args.start)
|
|
110
|
+
if not log.cycles:
|
|
111
|
+
print(f"qso-graph-sim: no received decodes in {name}"
|
|
112
|
+
+ (f" from {args.start}" if args.start else ""), file=sys.stderr)
|
|
113
|
+
return 1
|
|
114
|
+
print(f"{len(log.cycles)} cycles, {log.decodes} decodes from {name}"
|
|
115
|
+
+ (f" ({log.skipped} lines skipped: Tx or not a decode)" if log.skipped else ""))
|
|
116
|
+
host, port = args.to
|
|
117
|
+
family = socket.AF_INET6 if ipaddress.ip_address(host).version == 6 else socket.AF_INET
|
|
118
|
+
with socket.socket(family, socket.SOCK_DGRAM) as sock:
|
|
119
|
+
print(f"sending to {host}:{port} as {args.id}; Ctrl+C to stop")
|
|
120
|
+
player = Player(
|
|
121
|
+
sock=sock, target=(host, port), log=log,
|
|
122
|
+
station=Station(sender=args.id, call=args.call, grid=args.grid),
|
|
123
|
+
speed=args.speed, loop=args.loop,
|
|
124
|
+
)
|
|
125
|
+
signal.signal(signal.SIGTERM, lambda *_: player.stop())
|
|
126
|
+
try:
|
|
127
|
+
player.run()
|
|
128
|
+
except KeyboardInterrupt:
|
|
129
|
+
player.stop()
|
|
130
|
+
except OSError as e:
|
|
131
|
+
print(f"qso-graph-sim: cannot send to {host}:{port}: {e.strerror}",
|
|
132
|
+
file=sys.stderr)
|
|
133
|
+
return 1
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
sys.exit(main())
|
qso_graph_sim/player.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Playing an ALL.TXT back as WSJT-X would have sent it.
|
|
2
|
+
|
|
3
|
+
One cycle every 15 seconds, at the real pace unless sped up for a test. Per
|
|
4
|
+
cycle: a Heartbeat (WSJT-X sends one every 15 seconds), a Status as decoding
|
|
5
|
+
starts, the cycle's Decodes, and a Status as it ends. The Status carries the
|
|
6
|
+
dial frequency, so a band change in the log shows there; WSJT-X does not send
|
|
7
|
+
Clear for a band change, because its Band Activity window keeps the earlier
|
|
8
|
+
decodes, so neither does this. On the way out it sends Clear then Close, as
|
|
9
|
+
WSJT-X does when it shuts down normally.
|
|
10
|
+
|
|
11
|
+
Decodes are stamped with the cycle they are played in, not the one they were
|
|
12
|
+
logged in, so a receiver sees a live band rather than one from last April.
|
|
13
|
+
|
|
14
|
+
It answers Replay the way WSJT-X does: every decode still in its window,
|
|
15
|
+
marked not new, then a Status. Datagrams from anywhere but the address it was
|
|
16
|
+
pointed at are ignored, and so is anything addressed to a different Id.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import select
|
|
22
|
+
import socket
|
|
23
|
+
import time
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from dataclasses import dataclass, field, replace
|
|
26
|
+
|
|
27
|
+
from . import wsjtx
|
|
28
|
+
from .alltxt import Cycle, Log
|
|
29
|
+
|
|
30
|
+
CYCLE_SECONDS = 15
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Station:
|
|
35
|
+
sender: str = "WSJT-X"
|
|
36
|
+
call: str = ""
|
|
37
|
+
grid: str = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Player:
|
|
42
|
+
sock: socket.socket
|
|
43
|
+
target: tuple[str, int]
|
|
44
|
+
log: Log
|
|
45
|
+
station: Station = field(default_factory=Station)
|
|
46
|
+
speed: float = 1.0
|
|
47
|
+
loop: bool = False
|
|
48
|
+
say: Callable[[str], None] = print
|
|
49
|
+
clock: Callable[[], float] = time.time
|
|
50
|
+
_window: list[wsjtx.Decode] = field(default_factory=list, init=False)
|
|
51
|
+
_status: wsjtx.Status | None = field(default=None, init=False)
|
|
52
|
+
_stop: bool = field(default=False, init=False)
|
|
53
|
+
|
|
54
|
+
def stop(self) -> None:
|
|
55
|
+
self._stop = True
|
|
56
|
+
|
|
57
|
+
def _send(self, data: bytes) -> None:
|
|
58
|
+
self.sock.sendto(data, self.target)
|
|
59
|
+
|
|
60
|
+
def _send_status(self) -> None:
|
|
61
|
+
if self._status is not None:
|
|
62
|
+
self._send(wsjtx.status(self.station.sender, self._status))
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------ incoming
|
|
65
|
+
|
|
66
|
+
def accepts(self, address: str) -> bool:
|
|
67
|
+
"""Only the receiver this was pointed at may talk to it."""
|
|
68
|
+
return address.removeprefix("::ffff:") == self.target[0]
|
|
69
|
+
|
|
70
|
+
def _handle(self, data: bytes, address: str) -> None:
|
|
71
|
+
if not self.accepts(address):
|
|
72
|
+
return
|
|
73
|
+
try:
|
|
74
|
+
msg = wsjtx.parse(data)
|
|
75
|
+
except ValueError:
|
|
76
|
+
return
|
|
77
|
+
if msg.sender != self.station.sender:
|
|
78
|
+
return
|
|
79
|
+
if msg.kind == wsjtx.MessageType.REPLAY:
|
|
80
|
+
self.say(f"replay asked for: sending {len(self._window)} decodes")
|
|
81
|
+
for d in self._window:
|
|
82
|
+
self._send(wsjtx.decode(self.station.sender, replace(d, is_new=False)))
|
|
83
|
+
self._send_status()
|
|
84
|
+
# Reply, Halt Tx, Free Text, Close and the rest would act on a real
|
|
85
|
+
# WSJT-X. A monitor never sends them, so there is nothing to do.
|
|
86
|
+
|
|
87
|
+
def _wait(self, until: float) -> None:
|
|
88
|
+
"""Sleep until ``until`` (real time), answering anything that arrives."""
|
|
89
|
+
while not self._stop:
|
|
90
|
+
left = until - time.monotonic()
|
|
91
|
+
if left <= 0:
|
|
92
|
+
return
|
|
93
|
+
ready, _, _ = select.select([self.sock], [], [], min(left, 0.5))
|
|
94
|
+
if ready:
|
|
95
|
+
try:
|
|
96
|
+
data, (address, *_rest) = self.sock.recvfrom(65_535)
|
|
97
|
+
except OSError:
|
|
98
|
+
continue
|
|
99
|
+
self._handle(data, address)
|
|
100
|
+
|
|
101
|
+
# ------------------------------------------------------------ playback
|
|
102
|
+
|
|
103
|
+
def _play(self, cycle: Cycle, played_at: float) -> None:
|
|
104
|
+
ms_of_day = int((played_at % 86_400) * 1000)
|
|
105
|
+
self._status = wsjtx.Status(
|
|
106
|
+
dial_hz=cycle.dial_hz, mode=cycle.mode, de_call=self.station.call,
|
|
107
|
+
de_grid=self.station.grid, decoding=True,
|
|
108
|
+
)
|
|
109
|
+
self._send(wsjtx.heartbeat(self.station.sender))
|
|
110
|
+
self._send_status()
|
|
111
|
+
for line in cycle.decodes:
|
|
112
|
+
d = wsjtx.Decode(
|
|
113
|
+
is_new=True, ms_of_day=ms_of_day, snr=line.snr, dt=line.dt, df=line.df,
|
|
114
|
+
mode=wsjtx.mode_character(line.mode), message=line.message,
|
|
115
|
+
low_confidence=line.low_confidence,
|
|
116
|
+
)
|
|
117
|
+
self._send(wsjtx.decode(self.station.sender, d))
|
|
118
|
+
self._window.append(d)
|
|
119
|
+
self._status = replace(self._status, decoding=False)
|
|
120
|
+
self._send_status()
|
|
121
|
+
|
|
122
|
+
def run(self) -> None:
|
|
123
|
+
if not self.log.cycles:
|
|
124
|
+
raise ValueError("nothing to play: no received decodes")
|
|
125
|
+
# The first cycle is stamped with the T/R period now under way; each
|
|
126
|
+
# one after it, 15 seconds later.
|
|
127
|
+
first = self.clock() // CYCLE_SECONDS * CYCLE_SECONDS
|
|
128
|
+
started = time.monotonic()
|
|
129
|
+
n = 0
|
|
130
|
+
try:
|
|
131
|
+
while not self._stop:
|
|
132
|
+
for cycle in self.log.cycles:
|
|
133
|
+
if self._stop:
|
|
134
|
+
break
|
|
135
|
+
played_at = first + n * CYCLE_SECONDS
|
|
136
|
+
self._play(cycle, played_at)
|
|
137
|
+
stamp = time.strftime("%H:%M:%S", time.gmtime(played_at))
|
|
138
|
+
self.say(
|
|
139
|
+
f"{stamp} {cycle.dial_hz / 1e6:.3f} {cycle.mode} "
|
|
140
|
+
f"{len(cycle.decodes):2d} decodes (logged {cycle.stamp})"
|
|
141
|
+
)
|
|
142
|
+
n += 1
|
|
143
|
+
self._wait(started + n * CYCLE_SECONDS / self.speed)
|
|
144
|
+
if not self.loop:
|
|
145
|
+
break
|
|
146
|
+
finally:
|
|
147
|
+
self._send(wsjtx.clear(self.station.sender))
|
|
148
|
+
self._send(wsjtx.close(self.station.sender))
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
260407_233830 14.074 Rx FT8 0 0.3 2544 N5EMM KE0QEQ EN04
|
|
2
|
+
260407_233830 14.074 Rx FT8 -4 0.2 1662 CQ N5VAN DN91
|
|
3
|
+
260407_233830 14.074 Rx FT8 -7 0.3 1498 CQ KW7E DM33
|
|
4
|
+
260407_233830 14.074 Rx FT8 1 0.2 1940 PY2VZ KJ4KFJ 73
|
|
5
|
+
260407_233830 14.074 Rx FT8 -13 0.5 2221 CQ N8RAF EN90
|
|
6
|
+
260407_233830 14.074 Rx FT8 -11 0.3 1085 KB2KBC KY4JDC +21
|
|
7
|
+
260407_233830 14.074 Rx FT8 -18 0.4 1851 PP5TG KD8VAX -17
|
|
8
|
+
260407_233830 14.074 Rx FT8 -13 0.3 913 CQ NN8M EM79
|
|
9
|
+
260407_233830 14.074 Rx FT8 -15 0.3 1149 CQ PP2BO GH61
|
|
10
|
+
260407_233830 14.074 Rx FT8 -14 0.3 500 KC0IUY N8PFK EM80
|
|
11
|
+
260407_233830 14.074 Rx FT8 -22 0.2 1343 PY2MM AE1JR 73
|
|
12
|
+
260407_233830 14.074 Rx FT8 -14 0.3 2230 LW5DR AK8A RR73
|
|
13
|
+
260407_233830 14.074 Rx FT8 -19 0.4 1513 N5EMM KS5X EM15
|
|
14
|
+
260407_233830 14.074 Rx FT8 -15 0.5 897 VA2PM W6RJR DM13
|
|
15
|
+
260407_233830 14.074 Rx FT8 -17 0.3 2550 N5EMM K8TJM EN91
|
|
16
|
+
260407_233845 14.074 Rx FT8 10 1.1 1003 CX3JY N6PE DM13
|
|
17
|
+
260407_233845 14.074 Rx FT8 1 0.3 2221 N8RAF KI6BWO DM04
|
|
18
|
+
260407_233845 14.074 Rx FT8 3 0.6 497 KD2VRL W6DAD DM33
|
|
19
|
+
260407_233845 14.074 Rx FT8 4 1.2 1237 CQ NL7D DM42
|
|
20
|
+
260407_233845 14.074 Rx FT8 3 0.2 1940 K6VVK KJ4KFJ EN74
|
|
21
|
+
260407_233845 14.074 Rx FT8 1 0.6 1071 AF0DB K8IG EN91
|
|
22
|
+
260407_233845 14.074 Rx FT8 -8 0.3 191 TN8GD AB4OB EM70
|
|
23
|
+
260407_233845 14.074 Rx FT8 -11 0.3 2006 CQ KD6UYK DM13
|
|
24
|
+
260407_233845 14.074 Rx FT8 -10 0.9 1166 NM2V KA9EKJ R+15
|
|
25
|
+
260407_233845 14.074 Rx FT8 -8 0.5 1895 KA3PMW KG5FDT R-19
|
|
26
|
+
260407_233845 14.074 Rx FT8 -9 0.2 667 PY4STO K8RWM EN82
|
|
27
|
+
260407_233845 14.074 Rx FT8 -16 0.9 254 VE3BQ KJ5W -15
|
|
28
|
+
260407_233845 14.074 Rx FT8 -16 0.5 2543 K8ZI N5EMM RR73
|
|
29
|
+
260407_233845 14.074 Rx FT8 -13 0.6 774 TN8GD N8IBW EM89
|
|
30
|
+
260407_233845 14.074 Rx FT8 -12 0.2 400 PY4KU AD9AR EM69
|
|
31
|
+
260407_233845 14.074 Rx FT8 -19 0.3 1620 KQ4SRN <W1AW/5> R+05
|
|
32
|
+
260407_233845 14.074 Rx FT8 -16 0.8 754 CQ K4GBI EM70
|
|
33
|
+
260407_233845 14.074 Rx FT8 -12 0.3 498 N8PFK KC0IUY +06
|
|
34
|
+
260407_233845 14.074 Rx FT8 -22 0.4 1488 W7JZI V31DL EK57
|
|
35
|
+
260407_233845 14.074 Rx FT8 -21 0.3 1118 K4SYT AC3PN R+23
|
|
36
|
+
260407_233900 14.074 Rx FT8 5 0.4 2616 KE8VWF AD4VE DM12
|
|
37
|
+
260407_233900 14.074 Rx FT8 -5 0.2 1662 CQ N5VAN DN91
|
|
38
|
+
260407_233900 14.074 Rx FT8 -5 0.3 2036 KG5FDT KQ6RN -01
|
|
39
|
+
260407_233900 14.074 Rx FT8 -11 0.3 1497 WB8BCU KW7E +00
|
|
40
|
+
260407_233900 14.074 Rx FT8 -16 0.3 1566 WG4P PP2BO +04
|
|
41
|
+
260407_233900 14.074 Rx FT8 -15 0.3 1851 CQ KD8VAX EN82
|
|
42
|
+
260407_233900 14.074 Rx FT8 -7 0.3 1086 PY5AQ KY4JDC R-14
|
|
43
|
+
260407_233900 14.074 Rx FT8 -18 0.4 1957 N5EMM N7VBN -15
|
|
44
|
+
260407_233900 14.074 Rx FT8 -8 0.2 2886 TL8GD AA1ZZ EM16
|
|
45
|
+
260407_233900 14.074 Rx FT8 -10 0.3 912 CQ NN8M EM79
|
|
46
|
+
260407_233900 14.074 Rx FT8 -16 0.5 2221 CQ N8RAF EN90
|
|
47
|
+
260407_233900 14.074 Rx FT8 -14 0.3 499 KC0IUY N8PFK R-11
|
|
48
|
+
260407_233900 14.074 Rx FT8 -23 0.3 1725 VA3WL WD4T EM75
|
|
49
|
+
260407_233900 14.074 Rx FT8 -16 0.5 898 VA2PM W6RJR DM13
|
|
50
|
+
260407_233915 14.074 Rx FT8 3 1.1 1003 CX3JY N6PE DM13
|
|
51
|
+
260407_233915 14.074 Rx FT8 5 0.3 2221 N8RAF KI6BWO DM04
|
|
52
|
+
260407_233915 14.074 Rx FT8 4 1.3 1237 N6BBQ NL7D +00
|
|
53
|
+
260407_233915 14.074 Rx FT8 -2 0.2 1940 K6VVK KJ4KFJ R-01
|
|
54
|
+
260407_233915 14.074 Rx FT8 0 0.5 497 KD2VRL W6DAD DM33
|
|
55
|
+
260407_233915 14.074 Rx FT8 -8 0.3 192 TN8GD AB4OB EM70
|
|
56
|
+
260407_233915 14.074 Rx FT8 -14 0.3 1118 K4SYT AC3PN R+23
|
|
57
|
+
260407_233915 14.074 Rx FT8 -14 0.3 2006 CQ KD6UYK DM13
|
|
58
|
+
260407_233915 14.074 Rx FT8 -7 0.2 667 PY4STO K8RWM R-18
|
|
59
|
+
260407_233915 14.074 Rx FT8 -5 0.6 1071 AF0DB K8IG EN91
|
|
60
|
+
260407_233915 14.074 Rx FT8 -10 0.3 582 TN8GD KF8EFV -15
|
|
61
|
+
260407_233915 14.074 Rx FT8 -11 0.2 1400 CQ NL8F AO63
|
|
62
|
+
260407_233915 14.074 Rx FT8 -14 -0.1 857 PY4KU K8SAW -15
|
|
63
|
+
260407_233915 14.074 Rx FT8 -15 0.9 1166 NM2V KA9EKJ 73
|
|
64
|
+
260407_233915 14.074 Rx FT8 -14 0.2 2715 TN8GD N0STL EN35
|
|
65
|
+
260407_233915 14.074 Rx FT8 -22 0.3 1620 <...> W1AW/5 73
|
|
66
|
+
260407_233915 14.074 Rx FT8 -18 0.5 2544 K8TJM N5EMM -06
|
|
67
|
+
260407_233915 14.074 Rx FT8 -15 0.9 254 VE3BQ KJ5W -15
|
|
68
|
+
260407_233915 14.074 Rx FT8 -16 0.3 2042 KB3DHF KM7MGM DM42
|
|
69
|
+
260407_233915 14.074 Rx FT8 -5 0.3 472 TN8GD W4PSX EM65
|
|
70
|
+
260407_233915 14.074 Rx FT8 -13 0.4 679 W5ERX KA3E -02
|
|
71
|
+
260407_233915 14.074 Rx FT8 -16 0.4 1405 TN8GD K9XJ EN63
|
|
72
|
+
260407_233915 14.074 Rx FT8 -17 0.2 826 W1JKN KJ5KYB EL09
|
|
73
|
+
260407_233915 14.074 Rx FT8 -19 0.5 1895 KA3PMW KG5FDT R-19
|
|
74
|
+
260407_233915 14.074 Rx FT8 -12 0.3 499 N8PFK KC0IUY RR73
|
|
75
|
+
260407_233915 14.074 Rx FT8 -16 0.2 401 PY4KU AD9AR EM69
|
|
76
|
+
260407_233945 14.074 Rx FT8 7 1.1 1003 KD5QJQ N6PE DM13
|
|
77
|
+
260407_233945 14.074 Rx FT8 5 0.3 2221 N8RAF KI6BWO R+02
|
|
78
|
+
260407_233945 14.074 Rx FT8 -9 0.3 666 PY4STO K8RWM 73
|
|
79
|
+
260407_233945 14.074 Rx FT8 -3 1.5 1237 N6BBQ NL7D RRR
|
|
80
|
+
260407_233945 14.074 Rx FT8 -19 0.3 2007 CQ KD6UYK DM13
|
|
81
|
+
260407_233945 14.074 Rx FT8 -13 0.5 2544 K8TJM N5EMM -06
|
|
82
|
+
260407_233945 14.074 Rx FT8 -18 0.5 1895 KA3PMW KG5FDT R-19
|
|
83
|
+
260407_233945 14.074 Rx FT8 -15 -0.1 856 KD5QJQ K8SAW +07
|
|
84
|
+
260407_233945 14.074 Rx FT8 -13 0.3 472 TN8GD W4PSX EM65
|
|
85
|
+
260407_233945 14.074 Rx FT8 -16 0.3 303 KB3DHF AD0AR EN41
|
|
86
|
+
260407_233945 14.074 Rx FT8 -19 0.3 1118 K4SYT AC3PN 73
|
|
87
|
+
260407_233945 14.074 Rx FT8 -17 0.6 774 TN8GD N8IBW EM89
|
|
88
|
+
260407_233945 14.074 Rx FT8 -14 0.3 2655 CQ DX KO6IDY DM03
|
|
89
|
+
260407_233945 14.074 Rx FT8 -17 0.5 1405 TN8GD K9XJ EN63
|
|
90
|
+
260407_233945 14.074 Rx FT8 -18 0.2 1400 CQ NL8F AO63
|
|
91
|
+
260407_233945 14.074 Rx FT8 -14 0.3 498 N8PFK KC0IUY RR73
|
|
92
|
+
260407_233945 14.074 Rx FT8 -18 0.2 827 W1JKN KJ5KYB R-20
|
|
93
|
+
260407_233945 14.074 Rx FT8 -16 0.3 192 TN8GD AB4OB EM70
|
|
94
|
+
260407_233945 14.074 Rx FT8 -18 0.4 2602 CQ KJ5NUD EM44
|
|
95
|
+
260407_234000 14.074 Rx FT8 -4 0.4 2616 KE8VWF AD4VE 73
|
|
96
|
+
260407_234000 14.074 Rx FT8 -7 0.2 1662 CQ N5VAN DN91
|
|
97
|
+
260407_234000 14.074 Rx FT8 -7 0.3 912 VO1DNK NN8M RR73
|
|
98
|
+
260407_234000 14.074 Rx FT8 -9 0.3 1498 <W1AW/5> KW7E -11
|
|
99
|
+
260407_234000 14.074 Rx FT8 -9 0.3 1086 KB2KBC KY4JDC RR73
|
|
100
|
+
260407_234000 14.074 Rx FT8 -12 0.3 2036 KG5FDT KQ6RN -01
|
|
101
|
+
260407_234000 14.074 Rx FT8 -22 0.4 1852 CQ KD8VAX EN82
|
|
102
|
+
260407_234000 14.074 Rx FT8 -24 0.3 1725 VA3WL WD4T 73
|
|
103
|
+
260407_234000 14.074 Rx FT8 -21 0.3 1451 N6PE KD5QJQ +14
|
|
104
|
+
260407_234015 14.074 Rx FT8 10 1.1 1003 KD5QJQ N6PE R-02
|
|
105
|
+
260407_234015 14.074 Rx FT8 7 0.3 2221 N8RAF KI6BWO 73
|
|
106
|
+
260407_234015 14.074 Rx FT8 -11 0.1 1711 TN8GD K1CPT EN22
|
|
107
|
+
260407_234015 14.074 Rx FT8 -17 0.3 1165 XQ5OS AD9AR EM69
|
|
108
|
+
260407_234015 14.074 Rx FT8 -7 0.9 749 CQ KA9EKJ EM64
|
|
109
|
+
260407_234015 14.074 Rx FT8 -10 0.4 1405 TN8GD K9XJ EN63
|
|
110
|
+
260407_234015 14.074 Rx FT8 -14 0.3 2006 CQ KD6UYK DM13
|
|
111
|
+
260407_234015 14.074 Rx FT8 -11 0.3 499 N8PFK KC0IUY RR73
|
|
112
|
+
260407_234015 14.074 Rx FT8 -20 0.2 1940 KI7SKT KJ4KFJ 73
|
|
113
|
+
260407_234015 14.074 Rx FT8 -13 0.3 2655 CQ DX KO6IDY DM03
|
|
114
|
+
260407_234015 14.074 Rx FT8 -14 0.5 2714 TN8GD N0STL R-12
|
|
115
|
+
260407_234015 14.074 Rx FT8 -16 0.3 303 KB3DHF AD0AR R-05
|
|
116
|
+
260407_234015 14.074 Rx FT8 -16 0.5 2543 K8TJM N5EMM RR73
|
|
117
|
+
260407_234015 14.074 Rx FT8 -17 0.7 2402 W5ERX W8XP EM79
|
|
118
|
+
260407_234015 14.074 Rx FT8 -11 0.3 472 TN8GD W4PSX EM65
|
|
119
|
+
260407_234015 14.074 Rx FT8 -17 0.2 1399 CQ NL8F AO63
|
|
120
|
+
260407_234015 14.074 Rx FT8 -18 0.5 775 TN8GD N8IBW EM89
|