Every station on an already-worked band was flagged NEW MODE.
A WSJT-X Decode does not carry the mode's name. It carries the
one-character marker from the decode line - "~" for FT8, "+" for FT4 - and
that character was passed straight through as though it were a mode. The
status resolver then compared "~" against the modes worked for the entity,
matched nothing, and concluded the mode had never been worked. Same cause
put "~ -07" in the comment of every decode spot pushed to the FlexRadio
panadapter, which nobody had traced back.
Resolved through a marker table, with the mode from the sender's last
Status as the fallback - Status is the message that carries the real name.
So an unlisted or future marker degrades to correct rather than to
nonsense, and a sender that puts the name in the field directly is believed
as-is. With neither available the mode is left empty, which makes the
resolver answer "worked": the safe side, since a wrong mode invents a
new-mode flag exactly as the marker did.
Clicking a decode now ANSWERS it. It sends WSJT-X/MSHV a Reply message
(type 4), which is the same thing as double-clicking the line in their own
Band Activity window: the application looks the decode up, sets its
transmit frequency to the caller's and starts the exchange.
It deliberately does not tune the radio, which is what it did before and
why nothing happened. On FT8 the whole band sits inside one passband, so
moving the dial changes nothing about who gets answered - the decision
belongs to the decoding application, and the Reply is the only way to hand
it over. Tuning would also just fight it for the VFO. The entry is still
filled so the QSO can be logged here.
The reply is routed by PROGRAM ID, not by listener: two receivers can share
one multicast group, and answering a station heard on the 6 m instance by
talking to the 20 m one would start a call on the wrong band. It goes to
the address that instance's packets actually arrive from - a multicast
listener must answer the sender, never the group. WSJT-X matches the reply
against its own decode list, so the payload replays the decode field for
field: time, snr, delta time, audio offset, mode and message text.
Two columns added, DT and Freq - the audio offset inside the passband, not
the RF frequency, which is the same for every station in the list and says
nothing. Past about two seconds DT takes a warning tint: that station is
drifting out of the window.
The transmit strip. "You cannot see what you are sending, or who you are
calling" - two separate faults. The message was only ever threaded into its
period, and in FT8 you transmit in the slots you are NOT receiving in, so
its period had no decodes and the whole line was dropped; a transmit slot
now creates its period. And the state is a strip of its own at the top,
because it is the one thing on the screen that is about the operator rather
than the band. It is fed by every Status rather than only by one carrying
transmit text, so it can still name the station being called on MSHV and
older JTDX builds, which stop before tx_message in the Status payload.
"New only" became per-category badges, in the colours and the vocabulary of
the Chase New panel. None lit shows the whole band - this is a decode log
first, and a panel that opened by hiding most of the traffic would be lying
about what is on the air.
Every FTx decode WSJT-X, JTDX or MSHV puts on the wire, grouped by T/R
period. Optional and closable, from Tools -> FT decodes; its open state is
remembered, because an operator running digital modes leaves it open for
the session rather than consulting and closing it.
The period is the point, and what separates this from the cluster list.
FT8 is a sequence of fifteen-second slots and a band is read by watching
them go by: who called CQ this slot, who answered, what I was sending while
they did. A flat list sorted by time loses exactly that, so the list is
grouped one section per period, newest first, with the operator's own
transmission shown inside the slot it went out in.
Three fields had to be carried up from the wire to make it possible:
- the decode's OWN timestamp, which the parser read and threw away. It is
what assigns a slot: a period's decodes arrive in one burst a second or
two after it closes, so arrival time piles a whole period into the next
one. Rebuilt to UTC from milliseconds-since-midnight, with the
day-boundary case handled - a decode stamped 23:59:58 arriving at
00:00:01 would otherwise be dated a day ahead and sit at the top of the
list for the rest of the session.
- the decoded line itself. The exchange is what says where a station is in
a QSO, and no set of extracted fields reads like "R-09" does.
- tx_message and transmitting from Status, which nothing parsed before.
Recorded once per message rather than on every Status, which repeats it
about once a second for the whole over.
Also picked up on the way: is_new, low_confidence, off_air, the operator's
own call and grid, and the T/R period itself - better authority on slot
length than the mode name, which says nothing about a custom period. The
Status tail is read defensively: those fields were appended over successive
schema versions and JTDX and MSHV each stop at their own point, so a short
packet is normal and keeps whatever parsed.
Status flags come from ClusterSpotStatuses, the resolver the cluster list
and band map already use, filling the same cache. One verdict per call:
"new band" in this panel and plain worked in the cluster two seconds later
would be worse than no flag at all. Clicking a call goes through the same
handler as a cluster spot, so answering a station is one gesture whether it
came off telnet or off the receiver.
Filters: CQ only, new-anything only, band, mode, continent, an SNR floor
and a free search. The band, mode and continent choices are built from what
is actually on the feed - offering 160 m to a station whose receivers are
all on 6 m is noise.
Decodes are held in the frontend and pruned to a rolling half hour: they
are a live view, not data, nothing outside the panel reads them, and a
night of FT8 on 20 m would otherwise grow a list no filter can rescue.
Arrivals are staged on a 300 ms timer so a period landing as fifty packets
costs one status lookup and one render.
N0CALL is what WSJT-X transmits under when its owner never set a callsign. It
has a letter, a digit and an ordinary shape, so nothing rejected it: it was
spotted, coloured, counted as a new WPX prefix, and now that CQ grids are read
it would have put a grid into the worked index under a callsign nobody holds.
The obvious fix - adding it to looksLikeCall's reject list - was wrong, and the
test caught it before it shipped. That function answers "could this token be a
callsign at all", and the CQ grammar uses it to decide whether the word after CQ
is a modifier (DX, NA, a zone) or the call itself. Teaching it that N0CALL is
not a callsign made "CQ N0CALL JN36" skip a slot and return JN36. Shape and
policy are different questions and now live in different functions.
Chasing that turned up the real defect behind it: ANY unrecognised word after CQ
made the parser skip a slot, and a four-character grid passes every shape test a
callsign does. "CQ FOO JN36" returned JN36 as the sender - logged, spotted and
coloured as a station. A grid in the callsign slot is now refused outright.
Backend groundwork; the columns and filters that consume it come next.
GRIDS. A CQ is the one WSJT-X message that carries a locator, and wsjtSender was
throwing that token away. It is now returned, validated as a real field+square,
and remembered per callsign. This is the ONLY grid source available: a DX-cluster
line carries the spotter's grid at best and never the DX's, and a per-callsign
QRZ lookup under an RBN firehose is not a trade worth making. So grids are known
for the stations this receiver decoded - which is exactly the FT8/FT4 watering
hole an operator is looking at while grid chasing.
RR73 is why the grid is validated rather than pattern-matched. R is inside A-R
and 73 inside 00-99, so a sign-off satisfies the Maidenhead shape exactly and
would have planted a grid that does not exist into the index, silently.
NEW GRID keys on "GRID|MODE" with the mode put through the same normMode as
everything else, so the "group digital modes" option decides whether a grid
worked on FT8 is still new on FT4 - one rule, no branch. Grids are truncated to
four characters: a log holds a mix of JN36 and JN36QU, and without that the same
square is new forever, once per subsquare.
On cost, which was the condition: one more DISTINCT scan when the status
snapshot is rebuilt, then map lookups per spot. The same shape as the county and
POTA sets it sits beside, and the snapshot exists precisely so a spot batch never
touches the logbook.
Spotter continent and the LoTW flag come from tables already in memory - the
DXCC prefix table and ARRL's user list - so they cost a lookup each. The spotter
continent answers a different question from the DX's: whether anyone near you is
hearing this at all.
A forwarder (W&P, seen in the field in front of MSHV) prepends the origin as
plain text before re-broadcasting:
"127.0.0.1:2237|" + <the original, untouched WSJT-X packet>
That puts the magic 15 bytes in, so every datagram failed on "bad magic
0x3132372e" — those four bytes being ASCII "127." — and an operator running
MSHV behind the relay saw no decodes, no callsigns and no auto-logged QSOs.
No new service type: what follows the header IS a WSJT-X packet, so the parser
and everything downstream apply unchanged, and a separate type would duplicate
decode, status and logged-ADIF handling to strip 15 bytes. ParseWSJT skips the
header instead, which also covers any other relay that wraps traffic this way.
The match is deliberately narrow — the magic must fall within the first 64
bytes AND every byte before it must be printable ASCII. A corrupt or truncated
packet that merely contains those four bytes somewhere is not resurrected into
a QSO; it fails exactly as it did before.
Test data is the real captured datagram, header included.