An operator's screenshot had it side by side: the Awards panel showed Morocco
validated on five bands, and the band/mode matrix two inches above showed the
entity as merely worked.
ADIF's QSL_Rcvd enumeration has both Y and V — "received" and "verified" — and
V is what a LoTW download writes for a confirmation the ARRL has validated. The
award engine's isYes accepted "Y" or "V". Everything else in the app compared
against 'Y' alone: the worked-before status grid, the slot statistics, the row
colouring by QSL status, the awards QSO list, the call history badges. So the
confirmations an operator cares most about were the ones that did not count.
Now one definition per side, named so the next reader finds the other:
qso.ConfirmedValues on the Go side, used by the queries themselves, and
isQSLConfirmed in lib/qsl on the frontend, which rowColors and the panels call
instead of testing the letter.
The Go test drives the real query shape against a real database with a 'V' row
— the constant being right is not the point, the queries using it is.
An operator was about to build band→relay mapping into the custom URL rows.
It already exists, and better: relay auto-control has a per-relay "band" rule
driving webswitch, KMTronic, Dingtian, Denkovi and USB boards. It holds state
so a relay is only commanded when its wanted position changed, reads the
boards live before the first apply so it does not re-command one already in
place, and carries per-relay labels. A URL fired on a band change has none of
that, and would have been a second definition of "which antenna on which band"
— the drift that cost us a week on counties.
So the gap was only the hardware: a hand-made switch is none of the five named
types. It is now the sixth, "HTTP relay": an ON URL and an OFF URL, either as
one pattern with {relay} substituted, or one pair per relay. The per-relay
form is the reason it exists — these boxes often have URLs with nothing in
common between channels (…/FF0101 and …/FF0201), which no pattern can express.
Status is remembered rather than read: most have no endpoint worth trusting.
The cost is stated in the code and the panel — after a restart every relay is
re-commanded once, which is harmless on a board with no memory and far better
than assuming an antenna is already selected.
And the Connections panel now says so, exactly where the wrong choice is made:
picking a band change with a URL transport shows a note pointing at Station
Control. The URL transport stays — a lookup pushed to a webhook or a QSO to a
dashboard is an event, not a state, and Station Control has no place for it.
Two answers to "can people debug this when it does not work".
They could not. A custom UDP row logged "sent 18 bytes to 127.0.0.1:12000"
and nothing about the message — which is a template the operator wrote, so
the byte count says nothing about whether the substitution came out as they
meant. The payload is now in the line, with the trigger that fired it, for
custom rows only: an ADIF record or a binary WSJT frame on every QSO would
bury the log rather than fill it.
Worse was the silent case. A template that is a single placeholder the trigger
does not carry — {freq} on a band change — renders empty, sends nothing, and
looked exactly like a row that had never fired. It now says so, throttled per
row, and points at the field list.
And a password in a URL is redacted before it reaches the file. Storing
http://admin:secret@switch/ as typed was a deliberate choice for LAN gear;
writing it into a log that gets sent to whoever is helping is a different
choice, and was never made.
The Settings section is "Connections" rather than "UDP integrations", since it
has not been UDP-only since the URL transport landed. The log prefix stays
"udp:" on purpose — renaming it would orphan every log an operator has already
sent and every note anyone has written against it.
The hard-coded emitters each speak one published format at one fixed moment.
This is the escape hatch, and it exists mainly for antenna switches: they are
driven by a URL, and the band change is the trigger they want.
Four triggers, each carrying its own field set: band change (the RADIO's band,
not the entry form — a switch follows the rig, not what is being typed), QSO
logged, rotator command and callsign lookup. The panel prints the fields the
selected trigger can fill, which is the point of the whole thing: a placeholder
the trigger does not carry renders as nothing, and without the list an operator
writes {freq} on a band change and has no way to learn why the switch never
moved.
Three things the panel cannot show, handled here:
- Escaping. A URL needs every value percent-encoded; a UDP payload must not
be touched. The first portable callsign, F4BPO/P, puts a path separator in
the middle of a query string otherwise — it works on the bench and fails on
the air.
- Blocking. An HTTP call to a switch that is unplugged would otherwise sit
for the operating system's timeout, on the path of a band change. It runs
in the background with a 3 s limit.
- Silence. A switch answering 404 after a firmware update fails exactly like
success looks from here, so the status code is logged, throttled per row.
The rotator trigger hooks the COMMAND rather than the SP/LP buttons, so the
compass and a spot click fire it too, and the path ("SP"/"LP") is passed
through because an azimuth alone cannot say which was taken — 137° is short
path to one station and long to another.
Credentials in a URL are stored as typed. That is the operator's call, on the
grounds that this is LAN gear, and the hint in the panel says so.
The ADIF datagram reached Logger32's socket and still nothing appeared in its
log. WSJT-X sends TWO messages for every contact — QSO Logged (type 5), the
structured one, and Logged ADIF (type 12) — and receivers differ on which they
read: MacLoggerDX takes the structured one by default and offers the ADIF as
an option. We were sending only the half Logger32 does not use.
Both now go out on that row, in WSJT-X's own order. Nothing double-logs: any
receiver built for WSJT-X already sees both from the real thing.
Type 5 is read positionally, so the field order is pinned by a test against
NetworkMessage.hpp — one field out of place shifts every one after it and the
receiver files nonsense without complaining. QDateTime is Qt's own encoding:
Julian day, milliseconds since midnight, then the time spec, which is sent as
UTC (1) because a spec of 0 would have the receiver re-read the contact in its
own zone. The Julian arithmetic is checked against known values; a day out
there files every QSO on the wrong date.
"sent 1153 bytes to 127.0.0.1:2250" and nothing in Logger32. Both true: the
line the operator screenshotted is titled "Setup additional WSJT/JTDX UDP
sockets", and those receivers take UDP LOGGING PACKETS — the WSJT-X v2
protocol. A bare ADIF record posted to them is dropped without a word.
So there is a new outbound service rather than a change to the existing one:
"WSJT-X logged QSO (Logger32)" sends the same ADIF wrapped in a Logged ADIF
datagram (magic 0xadbccbda, schema 2, type 12), defaulting to port 2250. The
plain-text row stays for JTAlert/GridTracker-style receivers, because the two
really are different wire formats and one row cannot be both.
The id string is "OpsLog", not "WSJT-X": a receiver uses it to tell instances
apart, and impersonating the real thing would make a second WSJT-X
indistinguishable from us.
Tested both ways — the header byte for byte, and a round trip back through our
own ParseWSJT, since that is the same decoding every receiver applies.
Sahara moves to the parchment an operator asked for: page #e6ded0, panels
#f3eee2, toolbars #ded5c4, terracotta #c4501e on the buttons and the focus
ring. Lighter and less saturated than the deep sand it was.
Two silences from one operator's log.
The SteppIR is not blocked — it is being overruled. Every tune is commanded,
acknowledged on the next poll with the frequency asked for, then replaced on
the poll after by a different one: 21075 asked, 21075 confirmed, 21050
reported, again and again, with the operator tuning once more each time. Two
minutes of that reads as a dead link. It now says what it is, once, when the
motors have stopped and the reported frequency is more than 10 kHz from the
commanded one — a controller quantising to its own grid is not a fault, one
sitting somewhere else entirely is.
UDP outbound had no positive signal at all: only failures were logged. When
an operator sets up an ADIF message to a second logger and nothing arrives,
"OpsLog never sent it" and "the other program did not take it" looked exactly
the same from here — and the first is the common case, because a row created
as an inbound ADIF listener instead of an outbound ADIF message matches
nothing and emits nothing. Successful sends are logged with their destination
and service, and a QSO logged with no outbound row says so once a session.
Two problems from one operator's log, both of them the log's fault.
SteppIR. The last line about it is "status query failed, reconnecting: Port
has been closed" and then nothing — for the rest of the session. The poll loop
does retry every two seconds, but a failed open was a bare `continue`: no line,
ever. And startUltrabeam had three silent ways out — settings unreadable, the
antenna turned off, no port or host configured — so a cleared field and a lost
adapter produced exactly the same evidence, which is none. Both now say what
happened, with the port or host named. Reopen failures are reported three
times and then throttled (a port that is gone stays gone), and the recovery
says how many attempts it took.
FLDIGI. Four hundred "ADIF payload ignored" lines and nothing else legible.
The cause is in the same log: two inbound listeners on the SAME multicast
group and port, 239.255.0.1:2237 — one WSJT (MSHV), one ADIF (FLDIGI). Every
WSJT-X packet is delivered to both, and an FT8 cycle is dozens of decodes
every fifteen seconds; the bursts in the log are exactly 15 s apart. The ADIF
listener now checks for the WSJT-X magic and says so once, with what to do
about it, instead of rejecting each packet in writing. Anything else is
described a few times with its payload and then goes quiet.
Four things from an operator's first look at the panel, and one of them was
mine.
The frequency column showed "—" on every row. pskr.Spot.FreqHz was declared
and documented in one edit and never assigned in the next, so the payload's
"f" was parsed and dropped. A click therefore filled the callsign and left
the rig where it was, which is half the point of the panel. Assigned, with a
test that runs a real payload through and checks the value survives into the
Spot — the field being declared is what made it look done.
A row now carries ONE indication instead of stacking them. A station can be a
new band and a new prefix at once; the row says the first that matters, in the
order that would make an operator leave what they are doing: entity, band,
mode, slot, then prefix, then square. Two badges on one line made the list
unreadable at a glance, which is the only thing it is for.
Each category has a filter chip in the header, in its own colour, remembered
across sessions. A toolbar button shows the panel and a cross closes it — the
same split the Super Check Partial panel uses, where the setting decides
whether the feature exists and the button whether it is on screen.
The panel is wider and the country column gets what is left, which is more
than it was now that a row carries one badge.
PSK Reporter tells you what is actually being decoded in your region, which
is a larger set than what somebody chose to spot: nobody spots the FT8 caller
running ten watts from a rare square.
Almost all of it existed. The MQTT payload already carries frequency, mode,
transmitter and both grids; the watcher already drops any report collected
further than NearKm from the operator, which is exactly the question worth
asking — the station is being heard HERE, not in Japan; and with grid chasing
on the subscription is already every band, filtered at the broker by receiver
square, measured at 0.2 to 1.2 messages a second. This reads messages that
were arriving and being discarded.
"New" is not decided here. Every spot goes through ClusterSpotStatuses, the
same function the DX cluster grid uses and the same cached index, so the two
panels cannot drift apart the way the county columns did. Cost per message is
map lookups behind an option cached in an atomic, because the MQTT goroutine
must never wait on the settings store.
The option is its own, not nested under grid chasing: chasing squares and
chasing entities are different wants, and the feed now has three consumers,
any one of which brings it up and none of which cuts the others loose when it
goes down.
The panel says "digital modes only" in its footer. An empty list has to mean
"nothing new on FT8/FT4/JS8 near you", not "the band is dead" — it will never
show a new entity on CW.
The K3NG entry added an hour ago is gone. It named one clone among many —
WKmini, home-built Arduinos, unbranded boxes — for hardware that speaks
exactly the same protocol, and it was the only line in the engine list that
picked a boot delay rather than a protocol. An operator with an unlabelled
clone would have had to guess.
The delay is now learnt per port. The first connect finds out by failing the
quick attempt and succeeding on the slow one; that fact is written to a
global setting keyed by the port, and every connect afterwards goes straight
to the slow attempt. Global rather than per profile on purpose: which keyer
is plugged into COM3 belongs to the computer, and switching profiles for a
different rig does not change the keyer on the desk.
Two tests hold the contract from both sides — a slow keyer must be reported
as slow, and a keyer that answers at once must not be, or every K1EL connect
would inherit seconds it never needed.
The operator's keyer is a K3NG — an Arduino running an emulation of the
WinKeyer protocol — which changes the diagnosis and nearly broke it.
Checked against K3NG's own source before anything else, because yesterday's
handshake now FAILS a connect where it used to press on regardless. It is
safe: k3ng_keyer.ino implements admin echo (0x04) and echoes the byte back,
0x13 is a documented no-op, and OPTION_WINKEY_STRICT_HOST_OPEN — on by
default — ignores every byte except 0x00 before host open, so the resync
nulls are dropped harmlessly and the echo probe still gets through.
The real cause is in K3NG's options file, beside the feature itself:
"disabling Automatic Software Reset is highly recommended", and an option
to "discard errant serial port bytes at startup" for when it is not. On an
Arduino, DTR is wired to reset through a capacitor: opening the port reboots
the board into its bootloader. Ours spoke 400 ms later, to a keyer that was
not running yet.
So the retry now waits 2.5 s, which recovers it without an operator pressing
connect twice, and the engine list gains a K3NG entry that skips the doomed
fast attempt altogether. Everything else is identical to a K1EL — same
settings panel, same protocol.
An operator's log showed the whole fault in its first line: "connected on
COM3 — no reply — the keyer did not answer Host Open", followed by seven
configuration commands and two calls sent as Morse. Nothing was listening.
Reporting a link as up and then writing to it regardless is the part worth
fixing; the handshake is why it was down.
K1EL's Application Interface Guide gives the sequence, and we did one step
of it. Now all of it:
- DTR on, RTS OFF. K1EL's own init sets DTR_CONTROL_ENABLE with
RTS_CONTROL_DISABLE, and on a serial WinKeyer those lines ARE the power
supply — DTR feeds the 3.3 V regulator, RTS provides the negative rail.
go.bug.st/serial defaults both to true, so we drove RTS high on every
connect without a line of code saying so.
- 400 ms after the lines come up, for a WK1 still booting off DTR.
- Three 0x13 nulls to resync the command parser. A keyer left part-way
through a command by whoever spoke to it last would absorb Host Open as
a parameter — the everyday cause of a silent WinKeyer, and one the
operator can do nothing about from the outside.
- An echo test (0x00 0x04 0x55) before trusting the port at all. This is
the step that answers "is there a keyer here", and connecting now fails
on it, with the byte that came back when something else replied.
The whole handshake is retried once, since the first attempt's nulls are
what clear a confused parser. Tested against a fake port that reproduces
each failure: absent, mid-command, and echoing but versionless.
QSL_VIA is the manager. QSL_SENT_VIA and QSL_RCVD_VIA are the ADIF "QSL Via"
enumeration — B bureau, D direct, E electronic, M manager (import-only) —
and say how a card travelled. OpsLog had one column for all three:
- the import folded QSL_SENT_VIA into QSL_VIA whenever QSL_VIA was empty,
which is exactly a Log4OM export (it defaults QSL_SENT_VIA to E), so
OE6CLD saw "E" everywhere OpsLog shows the manager;
- QSL_RCVD_VIA was listed in adifPromoted with no column behind it, so it
was not stored, not kept among the extras, and not exported — dropped
outright on import;
- neither was ever written on export, so an import followed by an export
destroyed both;
- and OpsLog polluted the field itself: the QSL Manager panel wrote
"Bureau" / "Direct" / "Electronic", in full words, into QSL_VIA.
Two columns added (migration 0027), carried through the five places a
promoted ADIF field has to touch, with round-trip tests pinning the reported
case. The QSL panel now offers Bureau / Direct / Electronic for each
direction and stores the enumeration; the manager field is labelled as the
manager and holds only that. M is kept when a file gives it and never
written back out.
Existing logs hold a mixture of the two in one column. The repair is offered,
not performed: the count is shown once per log with a plain question, and a
"no" is remembered. It moves only where QSL_SENT_VIA is still empty, and only
values that normalise to the enumeration — a manager is a callsign and can
never be one of those six words, which a test pins against real manager calls.
An operator asked why K1SEI showed "Middlesex" in Info (F2) and "Lower
Connecticut River Valley" in the cluster. Two sources: the entry panel has
a callbook answer, a spot carries only a callsign so the cluster derives one
from the FCC licence ZIP through GeoNames — and GeoNames has followed the
Census in replacing Connecticut's counties with the 2022 planning regions.
No award, callbook or log uses those, so every CT station matched nothing:
new county for ever, and counting toward nothing.
Measured against a full ULS import (1 556 444 US callsigns), 23 223 resolved
to a name the USA-CA reference does not contain. Three causes, three fixes:
- Spelling. "City and County of San Francisco", "Baltimore (city)",
"Nome (CA)", plus counties renamed since the award list was drawn
(Kusilvak, Oglala Lakota, the Valdez-Cordova split) and Alaska's four
"X City and Borough", whose reference codes read "JUNEAUCITYAND" because
the county-type suffix strip eats the wrong end. Normalised in
award.USCountyKey, which both sides already go through. 7 515 callsigns,
no re-download needed.
- Doña Ana, NM shipped into the reference as "NM/DO̱AANA" — mangled by a
non-UTF-8 CSV line, a code nothing could ever produce, so that county was
unwinnable and silent about it. Row repaired, cntygen now refuses such a
line, and a test makes every one of the 3 102 references reproduce its own
code from its own name.
- Connecticut. A planning region is drawn from towns in several counties, so
no name maps to a name — only the ZIP can resolve it. cmd/ctzipgen builds
the table from the Census 2020 crosswalk, filling PO-box-only ZIPs from the
nearest resolved centroid; all 11 ZIPs GeoNames still labels with a real
county agree with the result. 15 037 callsigns, applied at import, so the
store now carries a rules version and Settings says when a re-download is
needed.
Alignment itself is the last piece: a spot now shows the county the station is
logged with when we have one, and falls back to the ZIP-derived county only for
stations never worked.
From a user's log, repeating throughout the session:
rigctld: split off failed: cat: this radio's backend cannot set split
rigctld: unimplemented command "X PKTUSB -1"
Both are answered with an error, and JTDX treats a rig-control error as fatal:
it abandoned a transmission 0.86 seconds into a 13.8-second frame.
"S 0" — split OFF. Fake It uses no split, and JTDX still sends this to be sure.
A backend that cannot SET split was refusing a request that was ALREADY
satisfied. It now succeeds when the rig is simplex, without touching the rig at
all. Arming still fails loudly: there the request is real and unmet, and a
client that believes it transmits up the band while the radio sits on the DX's
frequency is exactly what that refusal exists to prevent.
"X <mode>" — set_split_mode. Sent during ordinary setup, Fake It included,
where there is no second VFO to give a mode to. Accepted now: applied when a
split is in force, and a plain success otherwise, because the transmit VFO
already has that mode — it is the same VFO. get_split_mode answers to match.
Both regressions fail without the fix with the exact codes from that log,
RPRT -9 and RPRT -11.
Only PTT transitions were recorded. So when JTDX aborted a transmission
0.86 seconds into a 13.8-second frame, the log showed the abort and the Fake It
frequency restore around it — and not the command that preceded them, which is
the one thing needed to tell whether OpsLog answered something the client could
not accept.
Behind the existing CAT wire-trace switch rather than a new one: this is a line
per poll and would drown an ordinary log, and an operator chasing a rig-control
fault turns that trace on already.
RI1FJL/MM resolved to Franz Josef Land while the expedition was still sailing
there, telling the operator they had worked an entity they had not.
The old behaviour was deliberate — the comment read "strict DXCC says no
entity, but the log should still show the operator's country" — and that is
wrong for exactly this reason. A home country on a maritime mobile is not extra
information; it is a false claim about where the contact happened, and it is
the kind of false claim that gets a QSO submitted for an award it cannot win.
Trailing only. A LEADING "MM" is the Scotland prefix and "AM" is Spain, so
MM0ABC and AM5X keep their entities — getting that wrong would be a far larger
error than the one being fixed. /P, /M and /B are unaffected: a portable
station is still ashore.
In the cluster the column says "Maritime mobile — no DXCC" rather than going
blank, because blank is what an UNRESOLVED spot looks like and the two mean
opposite things — one is "we don't know yet", this is "there is nothing to
know". Display only: the QSO's own Country stays empty, since that field holds
a DXCC entity name and there is none.
ADIF defines CNTY as "STATE,COUNTY" — "GA,BARROW". OpsLog wrote the bare county
name, which a receiving logger cannot resolve: county names repeat across
states, and there is a Washington County in thirty of them. The award engine
here was never affected, since it reads the columns rather than the ADIF; the
damage was to every file we hand to someone else.
One writer covers everything — writeRecord — so this fixes file exports and the
LoTW, Club Log, HRDLog and QRZ uploads together. MY_CNTY gets the same
treatment.
The county alone is still written when no state is known: a bare name is worth
more than nothing, and inventing a prefix would be worse than either. A value
that already carries a comma passes through untouched, so re-exporting an
imported record cannot double the prefix.
Import is the mirror: "GA,BARROW" fills both columns, and a file carrying the
bare county — as OpsLog's own older exports do — still imports unchanged. The
state parsed out of CNTY only fills a blank STATE, never overrides it: STATE is
the dedicated field and the more specific statement.
"An existing connection was forcibly closed by the remote host" during DATA
reads the same whether the server refused the SIZE, hit an hourly quota, or
simply dropped the socket — and only the first is something an operator can act
on. The message now carries host:port, the encryption in use and the total
attachment size, so the size can be ruled in or out without a second attempt.
A path that does not exist is not counted: the mail library skips it, so
reporting it would describe a message that was never sent.
It went under the Station Control rotator; the compass that wanted it is the
docked one beside the entry strip. Both are the same RotorCompass, so the
readout moves INTO that component: every compass in the app carries it, and no
caller draws its own.
The bezel already showed the short path as a red marker, but a marker is a
direction, not a number — and the numbers in the status bar are 10px, which is
what prompted this.
Clickable only when the caller passed onGoto, since a compass rendered without
one cannot turn anything and a button that does nothing is worse than a label.
A log greets an operator; it does not address an envelope. "Robert Smith"
filled the Name field with something no one would ever send on the air, and it
is the first name that gets used when the contact is answered.
So fname alone, with the surname kept only as a last resort for a record that
has no first name at all — better a surname than an empty field. The nickname
option now falls back to the first name too, which is what it should have done
from the start.
joinName went with it: nothing composed a name any more.
QRZ publishes <nickname> — the name an operator goes BY on the air — and
OpsLog was composing fname + name instead. "Bob" is what belongs in a log;
"Robert J Smith" is what belongs on a licence.
QRZ only, and deliberately so: HamQTH's <nick> already fills the Name field
that way, so the same switch there would toggle a behaviour it has no way to
turn off.
A published nickname is optional, so an empty one falls through to the
registered name. That is the whole point of it being a fallback rather than a
swap, and it is what the test pins — a blank nickname must never blank the
name.
"The ON button does nothing" has three different causes — the modem-line pulse
was refused, the pulse went out and the amplifier ignored it, or the amplifier
woke and the UI missed it — and the log could not separate them: it recorded
one combined error and never said whether the amp came up.
RTS and DTR are now reported separately (not every USB-serial chip honours the
modem lines, and some refuse them without saying so), alongside the model and
transport. A watcher then logs whether the amplifier answered within 15
seconds, off the caller's goroutine so the click still returns at once.
No change to the wake sequence itself, which is the part that took a long time
to get right on the 1.3K-FA.
Grid: RI0FA transmitted QN35 all evening and was logged with its licence
holder's home square. WSJT-X's logged ADIF usually carries no GRIDSQUARE, so
the lookup was the only source left — and QRZ/HamQTH describe where an operator
LIVES, which for an expedition is the wrong side of the planet.
The square heard on the air is now applied before the lookup runs, so
refineGrid has something to defend. It still upgrades a 4-character square to a
6-character one from the same field, and still refuses a finer square from a
different field.
SPE: decodeCSV marks the client connected on every frame it parses, and the
poll goroutine can be mid-read while PowerOff runs. The frame from a second ago
landed after PowerOff had marked the amp offline and put it straight back,
which is why OFF had to be pressed twice.
Frames are ignored for three seconds after the off key goes out — the window is
opened before the key is sent, so nothing already travelling can beat it. Time
bounded rather than latched: an amp switched back on at its own front panel has
to reappear without being told.
Reported: ten fresh contacts upload and appear on LoTW; twenty older ones,
bulk-edited to exactly the same LOTW_SENT/RCVD state, are accepted by OpsLog
and never arrive. The operator's reading was that a failed attempt could not be
overridden. It is simpler and worse than that.
TQSL's exit codes, from its own cmdline documentation:
8 NO QSOs were processed — already uploaded OR OUT OF DATE RANGE
9 some processed, some ignored — same two reasons
14 some already uploaded, the rest signed
8 and 9 were both read as plain success. So on 8 — nothing uploaded at all —
OpsLog announced "already uploaded (duplicate)" and stamped every selected
contact as sent. They were never on LoTW and now looked as if they were, which
is exactly the reported symptom, and it is not recoverable by re-uploading
because the operator has no reason to try.
"Out of date range" is the cause that bites here: a contact older than the
callsign certificate's validity is silently left out. Older QSOs failing while
today's succeed is the signature.
Now: 8 is a failure and nothing is stamped — a duplicate left at "R" is
harmless and will be refused again, whereas a contact wrongly marked sent is
one nobody will look at twice. 9 and 14 succeed but carry Ignored, and the
caller says so in the console and a toast.
TQSL's own sentence ("20 QSO records are out of date range") is captured and
shown. It was being read and thrown away, and it is the whole answer to "why is
my contact not on LoTW".
OrderedIDs scanned qso_date straight into a time.Time. The column holds a
formatted STRING — the repo writes it with Format(isoMillis) and reads it back
through parseTimeLoose everywhere else — so the scan failed on every call, the
index was never built, and omitempty then dropped the zero from the JSON. A
column that shipped blank.
Scans the string and parses only the newest row; the ordering already came from
SQL, so 30 000 parses were never needed.
Tested against a real SQLite file with the date stored exactly as the repo
writes it, and with the ids running opposite to the dates — the imported-ADIF
case that is the whole reason this is not the id. It fails without the fix with
the scan error itself.
Not the id: the primary key follows insertion order, so importing an old ADIF
gives the oldest contacts the highest ids. This is a rank over qso_date.
Computed over the WHOLE log, not the query result — ranking inside the result
would renumber every contact the moment a filter is applied, and QSO #1 would
change identity as the operator typed.
Held as one id-to-rank map, built from a single ordered id query and dropped
with the other derived indexes when the log changes. A contact logged from the
entry form is the newest, so it takes the next number without rereading the
log: AddQSO deliberately avoids full invalidation because a contest run would
pay for it once per QSO. A contact entered with an OLDER date belongs in the
middle of the order, so there the map is dropped and rebuilt rather than
mis-numbered.
A batch that never reaches the disk looks exactly like one that does: the
locators are in memory either way until the next restart, which is the one
moment the difference shows. The shutdown flush was the least observable of
all — it runs last, and nothing said whether it had written anything.
Audit of every field a lookup returns against what OpsLog can store, prompted
by a report that HamQTH was not fetching the email.
The email was never broken: pinned now against a captured HamQTH answer, it
parses and reaches the QSO. The reported callsign simply has no public address
on HamQTH, which the empty field could not distinguish from a fault — so the
"[email protected]" placeholder is gone. A greyed-out sample address in the one
field an operator checks to see whether the lookup found one reads as a value.
Real gaps found and closed:
- web: the qso table has had the column since migration 0003 and no provider
mapping ever read it. HamQTH sends <web>.
- picture: Result.ImageURL was documented "QRZ only" because HamQTH's element
is <picture>, not <image>. It was there all along.
- zip: sent by both providers, read by neither.
- adr_name: used only as a fallback when a record carries neither nick nor
name. Deliberately NOT preferred over <nick> — a log wants the name the
operator goes by on the air, "Igor", not "Igor Vladimirovich Getman".
The parse tests use a real captured payload, including the stray <div> advert
the server injects into its own XML.
The store shipped without its main source. Locators came only from this
station's own WSJT-X decodes, which is what the whole MQTT discussion was
about.
PSK Reporter now feeds it through a new OnGrid callback, fired before any
geographic filtering: what the store wants is "which square is this callsign
in", and that is true whoever happened to hear the report.
The subscription filters on the RECEIVER's square, a level the v2 topic
exposes. Measured on the live feed: the four opening bands unfiltered are 83
messages a second, of which roughly one in a hundred survived the NearKm test
that already existed here — the rest was received, TLS-decrypted, JSON-parsed
and discarded. One ring of squares is 0.2 to 1.2 a second.
By square rather than by DXCC, which was the obvious alternative: one country
measured 1.2 messages a second (OH) against 72.5 (K) on a single band, because
a DXCC can be a continent. By square the same measurement is 0.2 to 1.2, so the
load follows distance — what the feed is actually about — and is the same for
every operator.
Grid chasing subscribes with the "+" band wildcard, so one subscription per
square covers every band instead of one per band per square.
The store gains a source column (decode | mqtt), migrated in place on an
existing file.
The callsign->grid map died with the process. Every restart began with an empty
Locator column that took an hour of listening to refill, and everything learnt
the day before was thrown away.
internal/gridcache is its own SQLite file in the data directory, not a table in
the settings database: that one sits wherever the operator chose to put it,
often a synchronised folder, and a store that rewrites itself every minute has
no business there. Deleting the file costs a few days of listening and nothing
else.
Callsign is the primary key and the newest report wins — operators move, go
portable, go on expedition, and a stale locator is worse than none for grid
chasing because it reads as a square already worked. Entries not seen in two
years are pruned at open: that is the one way this cache can be actively wrong
rather than merely empty, and it is what bounds a store that would otherwise
only grow.
Only CHANGES are queued. The feeds repeat themselves, so writing every report
would put the whole stream in the batch instead of the news in it. Batches
flush on a timer and on shutdown — a restart is exactly when the cache is worth
the most.
Rotation is switched off while the store is attached: the cap would discard
callsigns the database still holds and the lookup would then miss what we know.
Age in the store is the bound instead.
Two dates side by side put each one in a half-width Field2, which spends 120px
on its label column — the second label overlapped the first input and the row
ran off the panel. The comment ten lines above says exactly this about the
group/subgroup fields; one per row, as everything else in this editor.
Also shows a far-future end date as open-ended. RDA carries 9999-12-31 as "no
end" and printing it back reads like a real deadline.
The claim that an empty per-reference window inherits the award's is now a
test rather than a comment: a QSO before the award's own ValidFrom does not
count for a reference that has no window of its own, and a narrower reference
window still applies on top.
References are not forever: a park is delisted, a district merged, a castle
loses its number. A QSO made while the reference existed is a valid contact
and must keep counting; one made afterwards must not.
The JSON format did not need to change — ValidFrom/ValidTo were already on
awardref.Ref, already columns in award_references, already round-tripping
through export and import. They were simply never read: awardRefMetas dropped
them on the way into the engine, so nothing downstream could enforce them.
This carries them through and checks them in keepRefs.
Empty means the award's own window governs, which inScope already enforces for
every QSO in the award. That fallback is deliberately NOT duplicated per
reference — two places enforcing the same dates is two places for them to
disagree. The editor shows the award's dates as the hint under the boxes so
the operator can see what empty inherits.
Dates are compared as ISO strings rather than parsed times: the stored shape
is "2006-01-02", lexical order on it IS chronological, and this cannot fail on
a malformed value the way a parse can — a reference with a typo in its window
keeps counting instead of silently vanishing from an operator's totals.
The Explain trace names the date and the cutoff, because "did not count" on a
contact the operator remembers making is exactly when they need to be told it
is the reference that has a window, not their log that is wrong.
WSJT-X "Fake It" shifts the dial for the duration of each over and puts it
back afterwards, but the restore is conditional: it reads the frequency back
and only moves the dial if it disagrees with where it believes the radio
should be.
That read lands inside the window where this backend stops polling on
purpose. A Kenwood answers "?;" to IF; while it is transmitting, and treating
that as a fault used to drop the shared CAT link entirely, so the cached
state answers instead. SetFrequency wrote the command to the rig without
touching that cache — so mid-over the cache still described the pre-over
dial, WSJT-X read its own receive frequency back, concluded there was nothing
to restore, and the radio stayed on the transmit frequency. Every later over
started from there.
Reported from a session where the dial stuck at 7075500 after a full FT8
over while a bare TUNE, which never sets a frequency, worked fine.
Only simplex updates the cache. Under split, FreqHz is the transmit frequency
while the write lands on whichever VFO the operator is on, and guessing which
side moved would put a wrong number in front of the operator — a stale one
survives until the next poll.
The test reproduces the reported sequence and fails without the fix with the
same frequency the log shows.
A 2 m opening was announced at 9650 km towards Japan. The callsigns gave it
away — 7M4RRM, JA7RPC, JF1AWC — all working EME, which is routine on 2 m and
reported to PSK Reporter like anything else. Real contacts, real grids, and no
evidence whatsoever about the band: an operator turning a beam on that bearing
would hear nothing.
Removing the flat 2400 km ceiling was right; it threw away genuine multi-hop Es
on 6 m. "No limit anywhere" was the overcorrection. The limit is per band now,
and it is physics rather than a threshold: 2 m reaches a few thousand kilometres
by tropospheric duct or a chain of Es clouds and no further, so 3500 km, and
4 m 4000. Six and ten metres keep no ceiling — multi-hop Es and F2 genuinely do
go round the world, which is the case that started all this.
Pinned from both sides: the 9650 km Japanese burst produces nothing, and a
2000 km 2 m burst in one sector still counts.
QRZ has no record under M0BFS/QRP, so the lookup falls back to the home call —
that part worked. What followed did not: the home record's location is then
discarded, on the reasoning that a portable operator is not at their registered
address. True for /P and /M, and simply wrong for /QRP, which says something
about the transmitter and nothing about where it is.
So the grid came back empty for M0BFS/QRP while the same call without the suffix
answered perfectly, which is exactly how it was reported.
The distinction is now explicit rather than lumped in with the other operational
suffixes. /P and /M keep clearing the location, because mobile and portable both
mean somewhere other than home — that behaviour was correct and is untouched.
A power suffix stacked on a portable one (F4BPO/P/QRP) still clears: one of them
moved the operator.
A NULL never equals '', so a condition written that way returned zero rows. The
question is perfectly clear — the operator wants the ones with nothing in that
field — and answering it with silence makes the filter look broken rather than
mis-stated. eq and ne with a blank value now run the empty / not-empty test.
Empty on a NUMERIC column also had a real fault behind it. IFNULL(col,'')=''
compares 0 against '', which SQLite calls false and MySQL calls true — one
expression quietly answering two different questions depending on where the
logbook lives. Numeric columns test NULL or zero explicitly now; text keeps the
string test, where '' is a real value and 0 is not.
A bulk-editable field passes through THREE tables: the list the dialog shows,
bulkFieldColumns in app.go, and bulkEditableCols in internal/qso. I added mode,
submode and RST to the first and the third and missed the middle one, so the
dialog offered them and every save came back "unknown field".
Nothing warns about that. Each table is perfectly valid on its own, and the
mismatch only surfaces when an operator picks the one field that falls through
the gap — which is exactly how it was found.
So both directions are pinned now: every mapped field must be writable by the
qso layer, and every writable column must have a field mapping to it. A column
nothing maps to looks supported from the inside and cannot be reached from
outside, which is the same fault wearing the other hat.
Every one of the 2660 references was filed under European Russia. Corrected
against the RDA reference list: 991 are Asiatic Russia (15), and the 24 KA-
districts are Kaliningrad (126), which is not Russia for DXCC purposes at all.
1645 were already right.
dxcc_filter went from [15,54] to [15,54,126] with them. That filter decides
which QSOs are even considered, so leaving it alone would have made the 24
Kaliningrad districts unclaimable — a correction that quietly removes references
is worse than the error it fixes.
BA-27 (Mezhgorye) and BO-33 (Schebekino) are not in the reference list at all
and were left exactly as they were. Guessing an entity for a district nobody
lists is how a second wrong answer gets added to the first.
Only the dxcc field changed. Names differ in transliteration between the two
sources — Maykop against Maikop — and rewriting those was not asked for and
would churn 2660 lines to no purpose.
The page was capped at 1100px — a comfortable reading width, and the right
choice for the eight default columns. Now that all 123 fields can be published,
anything past about eight sat behind a horizontal scrollbar on a screen wide
enough to show the lot.
Worse, Windows hides overlay scrollbars until something moves, so a table that
scrolled looked exactly like a table with its right-hand columns missing. That
is what was reported, and the report was reasonable: nothing on screen said
otherwise.
The wrapper grows with its content now, up to the window, and keeps a minimum so
a two-column table does not collapse on a large display. The table sizes to what
its cells need rather than being squeezed to the container first — that squeeze
could wrap a callsign while empty space sat further along the same row. And the
scrollbar is drawn permanently, thin and in the page's own colours.
Nothing computes a distance when a QSO is logged. The DISTANCE field is only
ever filled by an ADIF import that carried one, so publishing it straight gave
an empty column to anyone whose log was made in OpsLog — which is everyone who
reported it.
It falls back to the two locators, which are on the QSO already. A stored
distance still wins: it came from the log that recorded the contact, which knew
more than two four-character squares do. No grids means an empty cell, not a
zero — an empty cell is honest, a zero is a claim.
Rounded to whole kilometres. The squares are tens of kilometres across and a
decimal would assert an accuracy nobody has.
The geometry moved to internal/geo on the way. It lived in package main, which
internal packages cannot import, so the PSK Reporter watcher already had its
maths injected from main and this would have been a third copy. A bearing that
disagrees with itself between two panels is a fault nobody reports, because each
screen looks perfectly plausible on its own.
They were excluded as "per-QSO", alongside callsign and date. That rule confused
two different things: bulk edit is not for describing QSOs, it is for REPAIRING
a batch of them — an import that mapped every contact to SSB, an ADIF that
carried no MODE at all. Refusing because a hundred rows should not normally
share a value left the operator editing a hundred rows by hand.
Setting the mode CLEARS the submode. A submode belongs to the mode it was
recorded under; left behind it contradicts the new one, and "FT8 with a submode
of USB" is not a thing — worse, the submode is what most ADIF readers believe.
Band is still refused on its own, and that half of the rule stands. It travels
with the frequency through BulkSetFrequency, which writes the pair: a band
contradicting its own frequency is invalid ADIF, and every export would carry
the contradiction out into the world. Callsign and date stay out too — they
identify the contact rather than describe it.
It read "6M OPEN" and kept the rest in a tooltip nobody hovers — an alarm with
no content, announcing that something was happening and refusing to say what.
Band, direction and typical distance are on the badge now: the three facts that
decide whether to point an antenna, all readable without touching anything. Out
of season gets a mark there too, since that is the one an operator must not
learn last.
The compass point is computed in Go and carried on the Opening rather than
derived in the UI. Sector() already gets the wrap round north right, and it got
it wrong once — a second copy of that arithmetic is how a badge ends up naming
the opposite direction to the log line beside it.
set_split_vfo and set_split_freq both answered RPRT 0 and did nothing. WSJT-X
and JTDX in "Split Operating: Rig" send exactly that pair, believed both, and
transmitted on the RECEIVE frequency — on a pileup, straight onto the DX, while
showing the operator precisely what they had asked for. A lie that leaves no
trace in any log is the worst kind of bug this program can have.
The two commands are honoured as a PAIR. Arming alone does nothing on the radio,
because WSJT-X sends the frequency second and split armed on whatever the
transmit VFO happened to hold is worse than no split at all: it transmits
somewhere the operator never chose. The request is remembered and set_split_freq
does the work.
Kenwood gains SetSplit — FB to place the dial, then FR0/FT1 to arm, in that
order for the same reason. It writes what State() already knows how to read.
Everything else REFUSES, and that is the feature, not a shortfall. Only Flex and
Icom could even toggle split before, neither could set the transmit frequency,
and Yaesu, TCI and OmniRig have nothing at all. A refusal WSJT-X can report —
and act on, by falling back to Fake It — is worth far more than a success it has
no way to check.
Both paths are pinned: split reaching the rig as one armed call with the right
frequency, and a backend that cannot do it producing an error rather than RPRT 0.
IF reports the VFO "in use". In split that is the RECEIVE VFO while receiving
and the TRANSMIT VFO while transmitting — the backend took it as the receive VFO
in both cases. Everything therefore read correctly until the PTT closed and then
reversed, which is exactly how it was reported from a TS-590 on USB.
The IF frame has carried the transmit bit all along; parseKenwoodIF already
decoded it into f.TX. It was simply never consulted here.
This is not only a display fault. FreqHz is what a QSO is LOGGED on, so a
contact worked in split went into the log on the DX's frequency instead of the
operator's — wrong in the log, wrong in every ADIF exported from it, and
invisible until someone checked a QSO by hand.
The emulated rig in the test package can now be keyed, so the case is pinned
from both sides: verified failing without the fix (tx and rx exchanged) and
passing with it.
Every condition was joined by ONE global AND or OR. "2 m or 70 cm, in FT8, since
January" therefore had no expression at all: AND killed the two bands, OR let
every FT8 QSO through. The filter could ask simple questions and nothing else.
Rather than grow nested groups — a tree in the UI to answer something that is
nearly always "this field, any of these values" — the OR lives INSIDE one
condition and everything else keeps ANDing. Two operators, a comma-separated
value, no change to how the rest of the filter behaves.
Two edges that matter more than they look. An empty list matches NOTHING rather
than being dropped: dropping it would widen the result set, the opposite of what
someone typing a filter expects. And "is none of" wraps the column in IFNULL,
because raw SQL NOT IN discards NULL rows — a QSO with no band recorded is not
one of the listed bands, so it belongs in the answer.
Club Log refuses with its ordinary web page rather than an error string, so the
new test reported a rejected login by pasting a 403 page — title, stylesheets,
navigation and all — into the status bar. The body now goes to the log, where a
real diagnosis happens, and the operator gets the one sentence there is to act
on: check the e-mail, the password and the logbook callsign.
Dropped the startyear=2099 filter with it, and that one matters more than it
looks. It was there to keep the reply small, but it was never verified against
Club Log's API — and Club Log answers an unrecognised request with the SAME 403
it uses for a refused login. An unverified parameter would therefore have made
every CORRECT password look wrong, which is precisely the failure this change
set out to end. The reply is capped at 4 KB and closed at once instead; the
status code arrives ahead of the body either way.
A PSK Reporter message says "X was heard BY Y". The watcher measured the
distance and bearing from the operator to X and stopped there, never asking who
had actually heard it — so a station 1400 km away, decoded by somebody in Japan,
counted as evidence. That proves the path from X to JAPAN and says nothing about
whether anything reaches this station. It is how a "2 m opening" came to be
announced out of a KX9X in the United States, and the operator was right to find
the callsign list absurd.
Reports are now kept only when the RECEIVING station is within 300 km. Far
enough to borrow the ears of a whole region — an opening reaches an area, not a
postcode, and waiting for a decode at one's own antenna is just working the band
— and close enough that the ionosphere doing something there is it doing the
same thing here. The transmitter still supplies the direction and the path
length, which is what was always wanted from it.
Also drops 12 m from the watched bands, at the operator's request: at this point
in the cycle it is open often enough that announcing it is a notification rather
than news, and a band that cries wolf costs the ones that do not. One fewer
subscription is also less traffic on a PC that pays for every message.
Two faults, both found from one field log: 70 000 decodes, 10 and 12 m plainly
open, nothing announced — and the two openings that DID fire named the wrong
direction.
evaluate() required EVERY distinct station in the window to fit inside one 90°
arc. That is the shape of a sporadic-E cloud and of nothing else. With 10 and 12
m open on F2 the reports arrive from all round the compass, the arc is 360°, and
the test can never pass — so the busier the band, the less likely an opening was
announced. Exactly backwards. It now finds the DENSEST sector instead, which
keeps the Es signature intact (a cloud still makes one direction dense) and lets
a real F2 opening be seen through the handful of neighbours who are always
there. Scattered-but-busy still reports nothing: the point was to stop demanding
global agreement, not to call every open band an opening.
Sector() averaged the two bearings arithmetically, so an arc crossing north was
labelled by its opposite: 353–61° averaged to 207° and went out as SW when it
was NE. Not a vague error — a reversed one, given to an operator who may turn a
beam on it. The detector has handled the 0/360 wrap since it was written; only
this label had not.