Reported from a multi-monitor station: window.json held x=-7680 and the
window opened where nobody could see it, with no way back short of
editing the file — which nobody knows to do.
The guard existed but asked the wrong question. It tested the position
against the VIRTUAL SCREEN, the rectangle spanning every monitor, and
monitors rarely tile that rectangle: a wide screen beside a tall one, or
one mounted higher, leaves gaps inside the box that belong to no monitor.
A window in a gap passes a bounding-box test and is invisible. The test
now walks the monitors themselves, through EnumDisplayMonitors, and uses
each one's WORK area — a title bar under the taskbar cannot be dragged
either.
A position that is genuinely lost is now MOVED onto the nearest monitor,
keeping the window's size. Handing it back to Windows lost the size too
and dropped the window on the primary screen wherever Windows chose.
The arithmetic is in screenclamp.go with no Win32 in it, and tested
against the reporter's four-monitor layout and against a gap between two:
this fault is invisible by definition and cannot be reproduced without
the reporter's screens, so a table test is the only place it can be held.
The layout is also logged at every start, since the first question after
'OpsLog does not open' is what the screens looked like.
The list is a ring buffer that held a thousand spots, and on a busy
evening a thousand arrive in a couple of minutes. So the buffer decided
how long a spot lived, and the spot LIFETIME setting never got the chance
to expire anything: fifteen minutes meant nothing when the oldest spot
was pushed out after two.
Now settable (Preferences → Cluster, beside the lifetime, since between
them they decide the same thing), 100 to 10 000. The ceiling is a real
limit rather than a round number: every spot is matched against the
worked index and the alert rules, and is a row the cluster grid and every
open band map re-render.
Club Log wrote to this station about 167 requests in four minutes and a
coming IP block. That was one deletion of a couple of hundred rows going
through delete.php, which is a REAL-TIME endpoint: it exists for an
operator removing a contact they just logged wrongly, at human pace.
Today's earlier fix stops the requests that were pointless -- QSOs Club
Log never had. This one handles the rest, which are legitimate but still
a batch: one delete every 1.2 s, and a stop at 25 with a line saying to
finish on clublog.org. There is no bulk-delete API to move to, so going
slower and refusing to push a large deletion through a real-time endpoint
is the whole of the answer.
Costs the operator nothing: the withdrawals already run in the
background.
Working a DXpedition split means guessing where it listens. The useful
information is not the callsign the DX answered but the FREQUENCY that
station was calling on, and a CW skimmer already marks it: SDC posts each
decoded report to the panadapter as a spot.
Those spots reach OpsLog through the radio's spot feed. On a marker, the
TRANSMIT slice moves there plus a signed offset; the receive slice never
moves, because losing the DX is worse than missing a call. The marker
text is a setting -- it is chosen in SDC by the operator, so any constant
here would be wrong for whoever chose otherwise -- and the switch is a
button beside SPLIT, since it is turned on when a DXpedition appears and
off when it is worked.
Moves are throttled and ignore a marker landing where the slice already
is: a busy pile-up produces several reports a second and the slice would
otherwise never be anywhere long enough to call.
The spot feed is now subscribed to unconditionally. It was tied to
OpsLog's own spot overlay, so an operator running SDC with the overlay
off received nothing -- the reason no foreign spot was ever logged. The
connect-time 'spot clear' stays behind the overlay flag: it wipes every
spot on the radio, a skimmer's included.
The import printed four numbers and left nothing behind. 1106 matched
and 395 unmatched out of 1501 is not a report: the operator cannot see
which contacts were confirmed, nor work through the discrepancies.
Two additions. The Results view is now filled with the confirmed
contacts, flagged new entity/band/mode/slot against what LoTW and paper
QSL already confirm -- the only sense in which a HAMLOG confirmation
changes anything. And the unmatched ones can be written out as ADIF,
because a few hundred of them are a list to open and compare, not
something to read in a log window.
Deleting a selection with 'delete from the remote services' on took
minutes. Club Log was called for EVERY deleted QSO, uploaded or not, and
for the ones it never had it answers 403 -- a full network round trip per
contact, in series, on the goroutine the UI was waiting on. QRZ already
skipped records with no stored logid; Club Log had no equivalent guard.
Three changes, each fixing one part of the delay:
- only contacts whose clublog upload status is Y or M are withdrawn;
- the withdrawals run in the background, after a snapshot -- the local
log is the operator's own copy and must not wait on two websites;
- the hooks read the rows in one query instead of one per id, which on
a remote MySQL logbook was 2 round trips per QSO before the DELETE.
Consecutive refusals now abort the run: Club Log blocks an account that
keeps sending requests it refuses, so working through the rest of the
selection only earns a longer block.
Their agent protocol has no download verb, so the return path is a file
exported from their site. Running that file through the ordinary ADIF
import is the wrong tool and does real damage: it matches on callsign +
UTC minute + band + mode, their export is rebuilt from their own
database and rarely agrees to the minute, and 'update duplicates' then
inserts everything that failed to match -- several hundred copies of
contacts already in the log.
This path matches only. It stamps the confirmation on QSOs it finds,
falls back to the mode-CLASS key for the modes their export renames, and
REPORTS what it could not match instead of adding it: an unmatched
confirmation is a question about the log, not a contact to create.
Also logs and reports how many rows a delete actually removed -- silence
there made a delete that did nothing indistinguishable from one that
worked.
Preferences → Confirmations gains a HAMLOG.online row, sent and received, with
the same defaults every other service has: a new QSO can start at N so the
backlog is meaningful from the first contact. The defaults reach it through a
new setExtraDefault — fill() writes through a pointer to a string field and
these two live in the extras map, which no pointer reaches.
The QSO grid gains four HAMLOG columns (status and date, both directions),
hidden by default: a column per service shown to everyone is how a grid becomes
unreadable.
And the paragraph explaining what "Sent" means is gone from that page.
Bulk edit offered two HAMLOG fields, both dates, and the first question they
drew was the right one: which of these is the status? Collapsing the flag into
the date was neat in the storage and a riddle in the dialog, where the eight
fields above it are four status/date pairs.
So four keys now — APP_OPSLOG_HAMLOG_SENT / _SENT_DATE and _QSL / _QSL_DATE —
and four fields, in the shape an operator already reads for eQSL, QRZ, Club Log
and HRDLog. The stamp written after a successful upload follows: Y in the
status, the day in the date.
Nothing else changes: the QSL Manager's backlog still asks whether the sent key
is present, and the award engine still treats any non-N value in the QSL key as
a confirmation.
An upload now leaves a trace on the QSO: APP_OPSLOG_HAMLOG_SENT holds the day
it went. The date rather than a Y, so a log says WHEN — and so one shape serves
three readers: eligibility (a stamped QSO is not sent twice), the QSL Manager's
backlog, and the appearance rules' sent channel.
HAMLOG joins the QSL Manager's service list. It has no status column to select
on, so its backlog is the ABSENCE of that extras key — a LIKE over the extras
JSON, which is a full scan and is the right trade here: it answers a button an
operator presses, and the alternative is a promoted column for a field no other
program would ever read. The match is on the quoted key, so a QSO whose comment
merely mentions the text is not taken for one already sent — which is what the
test pins.
HAMLOG.online is now reachable: Settings → External services → HAMLOG.ONLINE
takes the API key, the auto-upload switch and the timing, exactly like the six
services beside it, and a link opens the page where the key is issued.
The button next to it checks the KEY, not the connection. HAMLOG answers
KEYSTATUS with the callsign the key belongs to — something no other service
here offers — so the result reads "Key is valid — account F4BPO" rather than a
green tick. A key pasted from a club account or a second station is visible at
once, which is precisely the mistake a tick would have hidden.
No on-close timing, for the same reason as Cloudlog: there is no per-QSO upload
column to select a backlog from at shutdown, so the option would have done
nothing. Immediate or delayed, and a stored on_close falls back to immediate
rather than silently uploading nothing.
The appearance rules gain a HAMLOG.online channel too, so a row can be coloured
"confirmed" on that alone. It reads the ADIF extras — the standard names a field
for hamlog.EU and none for hamlog.ONLINE — using the same keys the award engine
reads, so the two can never disagree about what a confirmation is. "To send"
skips it: a QSO is either uploaded or not, there is no card to be owed.
HAMLOG.online is a seventh external service: one API key, one ADIF record per
QSO, immediate / delayed / on-close like the rest. They publish no API
documentation, so the protocol is read from THEIR OWN client — the HAMLOG Agent
(github.com/hamlogonline/Agent), which is the authoritative source short of
asking them:
POST https://hamlog.online/api/agent/
{"ADIFADD": {"APIKEY": k, "ADIFDATA": record}} → {"STATUS":"OK"}
{"KEYSTATUS": {"APIKEY": k}} → {"STATUS":"OK","CALLSIGN":…}
Success is STATUS == OK, not "no ERROR field": their failure carries ERROR and
no STATUS, and reading an unknown reply — a proxy page, a maintenance notice —
as an acceptance is how a contact goes missing without anyone noticing.
KEYSTATUS buys something no other service here offers: the key can be checked
BEFORE the first QSO, and the answer names the account. A key pasted from
another callsign is caught in the settings panel rather than through a week of
silent refusals.
Their confirmations are also an award source now, ticked like LoTW rather than
expressed through "custom". It reads the ADIF extras, not a column: the standard
names a field for hamlog.EU and none for hamlog.ONLINE, and borrowing the other
site's field would write a falsehood into every exported log. Three plausible
key names from their own export are accepted too, so nobody has to rename a
column by hand after an export.
Yaesu gains antenna selection (AN), remembered per band — the antenna picked on
a band comes back with it, with no table to fill in anywhere. Rigs with one
socket never answer AN and never show the row; the log says which case it is.
And a serial port that is refused now names its likely holder. OmniRig stays
resident once activated and keeps the port of the rig configured in it, so a
native backend never gets it — "Serial port busy" alone accused nobody, and an
FTDX10 spent a morning being blamed for it.
An Icom reached over the LAN streams its receive audio through the Icom
protocol, and Windows sees no sound card for it at all — the audio settings
could only offer the PC's own microphone, so "From radio" had nothing right to
point at and the QSO recorder had nothing to record. The recorder now accepts a
PUSHED source: the decoded stream goes to the speakers and to the recorder
alike, with no virtual cable to set up. Which source it uses follows the CAT
backend, and it is restarted only when that answer changes, so an ordinary
settings save never cuts a recording in half.
OmniRig no longer sends SetSimplexMode to a Yaesu when tuning. It is a silent
no-op on some — an FT-891 logged OK on every spot click while FreqA never
moved — and actively harmful on others. From an FT-2000 log, one QSY:
Vfo="AB"(0x80) Split=0x10000 (off) the operator's state
Vfo="BA"(0x100) Split=0x8000 (ON) after SetSimplexMode
The rig-agnostic "receive and transmit HERE, simplex" call turned split on and
moved reception to VFO B. OpsLog then displayed B — reading the radio correctly,
after having moved it itself. Icom is untouched: there the call is the
authoritative one and the direct write is unreliable.
Also:
- the NEW county badge shows in the entry form itself, inside the field,
where the operator is deciding whether to call.
- the basemap buttons clear the zoom controls; Light sat a few pixels from
the minus button and was being clicked by mistake.
- French cluster status: DÉJÀ CTC reads DÉJÀ QSO.
Panadapter spots are now worth reading. The comment carries the spotter, the
entity and the status in DXHunter's own shape — "CQ up 2 [F4BPO] [Franz Josef
Land] [New Slot]" — which needed two things nothing documents: SmartSDR splits
its command line on SPACES, so the words ran together until every space became
non-breaking; and it truncates past ~60 characters, so the cluster's own words
are trimmed first and the three brackets always survive. RBN column padding is
collapsed on the way in, or a preserved run of spaces opened a gap wide enough
to push the rest off screen.
"Already worked" means the CALLSIGN is in the log, not the entity: saying it of
a station never contacted was simply wrong. Each status can also be kept off the
panadapter entirely, and the WSJT-X decode spots obey the same switches — the
palette governs the panadapter, not one of the two things that feed it.
And the radio is no longer hammered: a spot whose frequency, colour and comment
are unchanged is not removed and redrawn. A busy skimmer feed re-spots the same
station every few seconds; one two-minute session sent 2128 adds, 88 of them for
a single callsign, and the display did not move a pixel for any of them.
CI-V, from an IC-7850 that kept killing JTDX: a reply the rig sent to another
controller on the same bus is no longer taken for ours, and a set_ptt, set_freq
or set_mode whose acknowledgement goes missing is verified by reading the rig
back instead of being reported as a failure. WSJT-X and JTDX answer a failed
command with a Rig Control Error and drop the link mid-over — 98 keyings, 6 lost
acknowledgements, 2 dropped connections in one session. The check waits 700 ms,
not the poll's 150: the rig has just failed to answer twice because it was
retuning, and a short probe would fail for the same reason.
Auto-call is withdrawn — it duplicated DXHunter, which already answers decodes,
and two programs deciding that from one shack key over each other. The library
is kept whole and dormant; a guard in App.tsx makes sure a stored preference
cannot key a transmitter whose switch no longer exists.
Also:
- the log rotates while running, not only at startup: the CI-V trace left on
wrote 416 MB and nothing would have stopped it before the disk did. Closing
it now releases the crash file too — the runtime keeps its own duplicate.
- the interface zoom announces itself, with a badge, a click back to 100% and
a View menu; Ctrl+wheel and Ctrl+0 always worked and nothing said so.
- no more elastic bounce, and no swipe-to-navigate out of the app.
- Edit QSO: your own TX power and the contacted station's extended locator
were saved and written back with no box to set them.
- FT decodes: continents are a multiple choice; a compound MSHV message that
answers two stations in one line is recognised as addressed to you.
Ultrabeam on a serial port never worked, and three faults were stacked so
each hid the next:
- Stop() did not wait for the poll loop, so a stopped client kept the COM
port. Every later client then failed with "Serial port busy" — the
program holding it being OpsLog itself.
- startUltrabeam tore the old client down CONCURRENTLY with starting the
new one, and "Test connection" built a second client on a port already
ours. Harmless over TCP, fatal on a port with one owner.
- A silent serial port returns (0, nil) and bufio retries that a hundred
times: a 4 s timeout became ~7 minutes of a frozen poll loop logging
nothing.
The controller then answered at once. Confirmed on hardware: the USB cable
presents TWO COM ports, only the second reaches the controller, and only at
19200 baud — so the speed is pinned in code (an FTDI cable opens at any
speed, and a wrong one is indistinguishable from a dead controller) and the
port field says which one to pick. The first exchange after each connect is
hex-dumped, which separates silence from a wrong baud from a misread frame.
Databases now carry only the tables their role needs. Every target used to
get the whole migration set, so a shared MySQL logbook grew settings and
station_profiles tables nothing ever wrote to — an operator inspecting the
server could not tell which copy was authoritative. Statements are filtered
by role, unknown tables are kept in both (fail-safe), and existing databases
are cleaned once, dropping only EMPTY tables. Settings → Database gains a
Compact button, since SQLite frees pages inside the file and never shrinks it.
Also:
- Awards: the callsigns behind a cell open the QSL Manager on Paper QSL,
searched, ready for the card dates.
- The record button no longer goes missing after an update: whether manual
recording is possible is a per-profile question that was asked once, at
startup, before the profile was known.
- Alert rules and filter presets confirm that they were saved.
- Spot clicks on the radio panadapter carry the POTA park into F3.
- The build gate is re-checked wherever the active callsign can change; it
ran at startup alone, and a fresh install has no callsign then.
Pressing Save in Preferences tore down the CAT link and the shared CAT server
unconditionally. Two consequences an operator sees at once: WSJT-X and JTDX are
thrown off their rig mid-QSO with an error box, and every spot disappears from a
FlexRadio panadapter — a Flex reconnect clears them by design, so the wipe on
save was the reconnect, not a stray command. Switching profile re-applies the
same settings and did exactly the same.
Two signatures decide it now. catLinkSig covers what shapes the physical link
(backend, host/port, COM port and baud, CI-V address, network credentials);
catShareSig covers the protocol and port of the rigctl/TCI server, which is the
socket WSJT-X actually holds. Same string in, same connection out, so there is
nothing to gain by rebuilding it. The poll interval, command delay, transverter
offset and spot options are applied to the running manager as before — the tests
pin that a spot option in particular can never trigger a reconnect, since that
is the one that would wipe the panadapter.
Also, two things the log would not say:
- WSJT-X/JTDX report their Enable Tx state in every Status and we wrote it
nowhere. When a station is called and nothing goes out, that flag is the whole
answer — a Reply does what a double-click does, it cannot arm a transmitter —
and the only way to see it was to watch a button in another window. Logged on
change, so it costs a few lines a QSO.
- The antenna log said "steppir connected" before anything had talked to it,
which made an unopenable serial port look like a working link with tracking
switched off. It says "configured", which is what it knows.
Three faults an operator's log finally made visible, plus the interface
work that came out of the same session.
Reliability:
- UDP events were dropped on backpressure without a word. A period hands
over twenty-odd decodes at once, and one slow write to the radio was
enough to fill the queue — so a decode simply never appeared, and the
only detector was the operator comparing the panel with JTDX. The drop
is now counted and logged, panadapter spots went to their own goroutine
so the radio can no longer hold the decode stream up, and the queue is
deep enough for a full period.
- The CAT manager waited for its poll loop with a bare <-done. A loop
wedged in a serial read then blocked every later restart inside Start,
before it could even try to connect: the rig stayed dead, no line was
written anywhere, and only killing the process recovered it. The wait
is bounded at ten seconds and says what it abandoned and why the next
connect may fail.
- Shutdown had no logging at all, so a hang left nothing to go on and a
process the operator had to kill — which then blocked the restart after
an update. Every step is logged, and a watchdog forces the exit if one
of them never returns.
Auto-call:
- A QSO in progress is now held by OpsLog itself rather than inferred
from the sender's Status. The moment WSJT-X/JTDX dropped the DX call or
the Enable-Tx flag between overs, the exchange looked finished and the
next CQ was answered, interleaving two and then three QSOs on one
slice. Released on log, on halt, on taking over, and by a watchdog.
Cluster console:
- Replies to a command were buried under the spot flood; a Replies
toggle hides the DX spots, which the list above already shows.
- Twelve named command buttons beside the input, configured in
Settings -> Cluster; a button with no command is not drawn.
- Following the tail is now an explicit switch, and sending a command
re-arms it. It used to measure "am I at the bottom" AFTER committing
the new lines, so a ten-line reply looked like the operator had
scrolled up and was never followed — the one case it exists for.
Awards:
- An award can name NO field. The matching controls disappear with it
and only hand-assigned references count, which is the only thing that
can feed a reference like WWBOTA. A test pins that nothing else is
scanned.
- WWBOTA added to the catalogue with its 31 342 references.
Elsewhere: the rotor widget's Stop button acknowledges the press like
the direction presets already did, and the docked band map can be
switched to fit-to-band from its own header.
STATION_CALLSIGN was the one field the option deliberately skipped, on the
grounds that stamping the active call could re-route a mixed-call log.
That protected nothing. The same option already writes this profile's grid,
rig, antenna, city and postal address onto every record it finds blank — it
has assumed "this log is mine" long before it reaches the callsign. And the
thing that actually keeps a multi-op log intact is that a record CARRYING a
station callsign is never touched, which has always been true and still is.
Withholding it only meant the option quietly failed on the one field an
operator goes and checks afterwards, leaving imported QSOs with no station
call at all — invisible in the ADIF they export, and unroutable for LoTW
and Club Log.
Counted and logged: "did it fill the callsign?" is the first question after
an import, and the log is where it gets answered.
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.
First pass on the panel from operating feedback.
Columns are a grid template shared by the header row and every data row, so
the two cannot drift and the eye has a rail to follow. It is capped at
1500 px and centred: free-flowing, a 2500 px window put the country a foot
from the callsign it belonged to and left a hole in the middle of every
line.
"New" gets a COLUMN. It was only a coloured edge before, which says
something is special without saying what — and every one of these is a
reason to break off what you are doing and call. The entity verdict is a
solid badge, the orthogonal ones (park, grid, prefix, county) are outlined
in the colours markerColour already gives the cluster list and the band
map, so a new park is the same green in all three. Applied inline because
those are categorical --chart-* custom properties, which the theme does not
expose as Tailwind colour utilities: written as border-chart-7 the badge
would simply have had no colour.
Band and mode selectors now appear only when the feed actually carries more
than one of each. One MSHV is one band and one mode, so for most operators
they were furniture; they show up the day a second instance puts a second
band on the link, which is the only day they mean anything. Same rule for
continent, and a receiver count when more than one instance is feeding.
Added a LoTW-only filter, and raised the type throughout (call and message
to 14 px, secondary to 12 px, badges to 11 px) with more room per row.
The decode payload now carries the sending application's own id. It tells
two receivers apart on one multicast group — and it is the address a
WSJT-X Reply message would have to go back to, so it is carried now rather
than requiring another trip through the parser later.
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.
The PH/CW/DIG grid in the Stats panel is the fastest read in the app and its
palette was fixed per theme. Settings -> Appearance now offers the six: the
four status fills, the never-worked fill, and the ring on the cell being
entered.
Stored as OVERRIDES, not as a palette. Each of the twelve themes ships an
--mx-* ramp tuned to its own background, so an operator who only wants a
different green must not thereby freeze the other four to the theme they
happened to be using that day. An empty value means "whatever the theme
says"; the chosen ones are stamped inline on <html>, where they win over
every theme; switching the feature off hands the colours straight back.
The pickers are seeded from what the matrix is painting at that moment
rather than from a fixed palette, so the choice starts from the colours in
front of the operator. A new --mx-cur token carries the current-entry ring:
it follows --warning by default, so it stays theme-correct on all twelve,
but can be recoloured without dragging every other warning in the app along.
The legend under the matrix and its cell tooltips were hardcoded English.
They now go through t() with the same keys as the pickers, so the grid and
the settings cannot disagree about which green is which.
HTTPS to a relay board could not work. Nearly every board that offers it signs
its own certificate — there is no authority anywhere that could have signed it —
so the request failed verification before it left.
A checkbox, per board, off by default. Not a blanket switch, because the other
HTTPS case is real and opposite: a board reached from outside through a proxy
with a genuine certificate, where verification is the only thing standing
between an antenna switch and the internet. Same setting, two boards, different
answers.
Off by default is only safe if the failure explains itself, so a certificate
error now names the box to tick. Go's own "x509: certificate signed by unknown
authority" is accurate and tells an operator nothing about what to do next.
Shown only once an https:// URL is actually in the board's configuration. A
board on plain HTTP has no certificate to argue about, and an option that cannot
matter yet is one more thing to wonder about.
The flag joins the driver cache key: ticking it has to rebuild the driver, or
the cached one would go on refusing the certificate with the verifying client it
already holds.
The boards that take a bare host — WebSwitch, KMTronic — keep verification. An
https:// typed there is the proxy case by construction, since they default to
plain HTTP on the LAN.
Three tests against a real self-signed TLS server: accepted with the box,
refused with a message naming it without the box, and one board's setting not
leaking into another's.
Two faults in the combiner coupling, both reported from the operating position.
THE POWER LEVEL WAS NEVER COUPLED. ON, OFF and OPERATE fanned out to the group;
L/M/H did not — it was simply the command nobody had linked. Two combined
amplifiers left at different power levels feed the combiner unevenly, which is
the thing the coupling exists to prevent.
THE COMMANDS DID NOT LEAVE TOGETHER. The second amplifier was commanded only
once the first had answered, and an SPE answers over its own link in its own
time. The combiner heard power appear on one input before the other and beeped
about it, on every OFF and every ON.
Each target now gets a goroutine, all parked on one channel until every one is
ready; closing it releases them together. That is the difference between "start
one, then start the other" and "both leave at once" — they have separate clients
and separate connections, so nothing downstream re-serialises them.
It matters most on the power level, which is not one command at all: an SPE has
no "set level", so the driver taps the POWER key and waits for the amp to report
the new one before tapping again — up to three taps, up to two seconds each. One
after the other, the pair would sit at different levels for six seconds.
A single amplifier still runs inline: no goroutine, no barrier, nothing new to
go wrong for the operators who have one amp. The one that was clicked stays
first, because its failure is the one worth reporting.
The one field the editor would not let you touch, and the one that was wrong on
WAJA. Every other property of a reference — its name, pattern, entity list,
validity window — was editable; the code was rendered readOnly, so correcting a
number meant deleting all 47 references and importing a new list, throwing away
anything the operator had adjusted in it.
A rename in the store, not a delete plus an insert: everything the reference
carries travels with it, which is the whole point of correcting a number rather
than replacing an entry. A number already in use is refused — REPLACE INTO would
have let one reference silently swallow another, discovered much later as a
prefecture quietly missing from the list.
The typed code is held apart from the selection. The list and every field patch
key off the selected code, so editing it in place made the editor lose the
reference mid-edit.
SaveAwardReference now recomputes the log like Delete and Replace already did. A
reference's name is what the award column SHOWS for awards displaying by name,
and its pattern is part of what matches at all — so editing one changes rows,
and the grid was left showing the old label until something else happened to
trigger a pass.
Changelog: the three TCI-sharing lines are merged into one. The server and the
two fixes made to it while building are one unreleased feature, and an operator
only ever meets the finished thing. The TCI-client PTT line stays separate — it
is OpsLog driving a SunSDR, the other direction entirely.
Wanted for the case where the answers are moving: an operator correcting their
own QRZ record, or chasing a DXpedition whose page changes during the operation,
otherwise waits out thirty days before OpsLog will ask again. Clearing the cache
by hand works once; this is the setting for a whole session.
Nothing is read from it and nothing is written to it — rows stored while it is
off would only sit there going stale, waiting for the day it comes back on.
Switching off is NOT clearing: what it already holds stays, and the Clear cache
button remains the way to throw that away.
Two distinctions the code now has to keep, both load-bearing:
An EXPLICIT stored zero is off; an ABSENT key is the thirty-day default. Every
operator who has never opened this setting has nothing stored, and reading that
blank as a zero would silently switch the cache off for all of them.
The CONSTRUCTOR's zero is still the default, not off. At startup the settings
have not been read yet, and beginning with no cache would hammer the provider
for the first seconds of every launch. Only SetTTL, called once the operator's
settings are known, can switch it off.
A negative lifetime is meaningless and is ignored rather than rounded into
either meaning.
The input had to change too: it derived its value from the stored number on
every keystroke, so the box could not be emptied — and 0 was unreachable
outright, since parseInt('0') || 30 is 30.
internal/rigctld exists because Windows gives a COM port to ONE process: the
moment OpsLog talks to the radio natively, nothing else can. It answers the
programs that speak Hamlib NET rigctl. This answers the ones built around Expert
Electronics' TCI instead — and it answers them whatever radio is connected,
because it sits on the same backend-agnostic Rig interface. An operator with an
Icom or a Yaesu can now hand a TCI-only program a working rig.
One server or the other, never both. They answer the same questions about the
same radio, nothing speaks both, and a second listener is only a second thing to
go wrong.
Written against the official TCI Protocol document (ExpertSDR3/TCI, 12 January
2024, MIT — downloaded and read, not recalled): the initialisation set of §4.1
in its documented order, and the argument order of every command from §4.2. A
client will not proceed past connect without that block, which is why it is
written out in full rather than stubbed.
The one dangerous detail is the VFO mapping. TCI's channel A is where you
LISTEN and channel B where you transmit — the opposite way round from OpsLog's
RigState, which follows ADIF. Getting that backwards would put a station on the
DX's own frequency, so it is pinned in both directions by a test, and RxFreq was
added to the adapter rather than inferred.
Writing channel B while the rig is simplex is ignored: the client asked to
prepare a split transmit frequency, not to QSY, and a logger doing that on every
spot click would drag the operator off the station they were listening to. A
backend that cannot split still refuses out loud.
Only changes are pushed. TCI clients redraw on each command, so re-sending an
unchanged frequency four times a second makes a VFO readout flicker and fights
the operator's own tuning.
Nine tests, no socket needed — the protocol is the decision, not the transport.
QRZ carries <iota> only for the operators who filled it in, and most have not.
But for 99 entities the reference follows from the entity alone — a station in
Ascension is on AF-003 whatever its callbook record says — and the entity is
known for every callsign from cty.dat, with no callbook at all.
So this reaches the operator with no QRZ subscription and the station that has
never touched a callbook, and it lands before the contact is logged, which is
when a reference is worth having.
Filled ONLY when the callbook gave nothing: an operator who typed a reference
knows something a table cannot — an IOTA-heavy entity, a rare island being
activated — and that still wins, as does one picked by hand on the entry.
The table is the IOTA programme's own dxcc_matches_one_iota.json, which lists
exactly the entities that map to ONE reference. France is not among them: a
French station is usually on the mainland and on no island, and guessing would
put a reference on hundreds of contacts that earn none.
Held as a table rather than a download — 3.5 kB that changes when an entity
appears, so fetching it daily would buy nothing and would fail exactly where a
portable station usually is. The header says where it came from and how to
refresh it.
Follows 747c2b9 and eab11db, which read the callbook's own tag.
The three layers underneath were already there and unreachable: the change-log
format (7d664bd), the identity column (724e68b) and the no-backfill decision
(c48294b). This is the wiring that gives the operator a switch.
Settings, all profile-scoped, because each profile can point at its own logbook:
the folder, this PC's name, the machine id minted once from it, the per-peer read
offsets and the tie-break counter. Scoping the machine id is what keeps two
profiles sharing one folder from writing two logbooks into one file.
Three hooks. Add and update publish asynchronously, down with the rest of the
after-the-fact work — a folder on a network share can block for seconds and a
contact belongs on screen long before another machine hears about it. Deletion
publishes SYNCHRONOUSLY and BEFORE the row goes, for the same reason
deleteRemoteCopies does: once it is gone its identity is gone with it and the
tombstone names nothing.
The apply path uses the repository directly and never AddQSO/UpdateQSO/DeleteQSO
— those publish, and a change applied here would be written straight back out,
two machines echoing each other for ever.
Saving writes a probe file to the chosen folder rather than asking whether it
exists. A read-only cloud folder, or a share whose credentials expired, exists
perfectly well and would swallow every contact in silence; if the probe fails the
switch goes back off instead of sitting on while nothing is written.
The panel is mostly status, and deliberately: every part of this runs on another
machine and on a sync client OpsLog cannot see, so "it is not working" has to be
answerable from the settings page — which PCs are in the folder, when each last
logged, what is waiting unread.
Four tests on the apply path, the middle two being the ones that matter: an edit
made on the other PC lands on the copy already here, matched on the contact
itself, instead of becoming a second row — that is what makes the no-backfill
decision safe — and a contact with the same station on another band stays a
separate contact.
Two faults behind one screenshot: 3Y0K, recorded in the log as Bouvet Island,
showing the Antarctica matrix.
SELECTING A QSO now shows what that QSO records. The panel asked WorkedBefore
with no DXCC hint, and a hint of zero makes the backend resolve the entity from
cty.dat and the ClubLog exceptions AS THEY ARE TODAY — right for a live contact,
wrong for one being looked at in the log. The repo's own "infer from past QSOs"
path never ran, because the hint was no longer zero by the time it got there.
Hence Antarctica, and "5 QSOs with this entity" against "11 with this call":
two different entities, one of them nobody had asked about. The selected row's
dxcc now travels with it and is passed as the hint.
Browsing the log shows what the log says, even where the log is wrong.
Correcting an entity stays a deliberate act — right-click, Update from ClubLog.
BACK-ENTERING A QSO now resolves the exception at the CONTACT'S date. An
exception carries a validity window and a DXpedition's window closes: 3Y0K typed
months later, with the activation's own date in the form, was resolved against
today, matched nothing, and fell back to cty.dat. Both callers pass the date
they already hold — the entry strip's, and in the editor the record's own.
The date is trusted to move the resolution BACKWARDS only. A half-typed
"2026-0" must not send the lookup to the year 20, and a mistyped future date
must not resolve against a window that has not opened; both fall back to now.
Midday rather than midnight, because a window given in whole days is inclusive
of its end date and 00:00 sits exactly on the boundary.
A field log showed three pulls of the same 123 615 QSOs inside ten seconds, the
Go heap going 725 MB → 2213 MB → 2539 MB. It is released after the idle TTL, so
not a leak — just the same work done three times with all three results alive at
once.
The cache lock was released before the logbook was read, so every caller that
arrived during a build missed the cache and started its own. Opening the Awards
panel does exactly that. One builder at a time now, with the usual re-check
after taking the lock: the second and third caller wait out the seconds they
were going to spend anyway, minus two round trips to the database.
The slice is also sized from the previous build. Growing to 123 000 structs by
doubling copies the whole thing a dozen times and holds the old and the new
array together at each step, on the largest object OpsLog keeps.
Also, the settings panel registry. The hook trap that broke the window when the
power-supply panel was opened was headed off by a comment, and the comment did
not survive contact with the next panel — so it is now structural. PanelHost is
a module-scope component that calls the selected panel and is keyed by section:
hooks inside a panel land in a component context that persists, section changes
remount it so each gets a fresh and consistent hook list, and no panel state
leaks into the next.
Rendering the panels as <Panel /> instead — the obvious refactor — would have
been wrong: they are nested inside SettingsModal and close over its state, so
each parent render makes a new component TYPE and React would unmount and
remount the panel on every keystroke.
The manufacturer's document arrived, so this is no longer guesswork: 9600 8N1,
function codes 03 and 06 only, and a register map with the output on/off at
0x0001, the measurements at 0x0010…0x0013 and the set points at 0x0030/0x0031.
ONE REGISTER IS WRITTEN — 0x0001, the output. The map also exposes the voltage
and current set points and the three protection trip levels as writable, and
none of them belong to a logbook: a wrong value there is 30 V where a radio
expected 13.8, or a trip level lifted on a supply feeding an amplifier. They are
read and displayed instead, next to the measured values, which is also how an
operator sees at a glance that the supply is on and the radio is drawing nothing.
The wire layer is tested where it can be. CRC-16/MODBUS is pinned against its
published check value — the CRC of "123456789" is 0x4B37 — which fixes the
polynomial, the initial value, the reflection and the absence of a final xor all
at once; the rest of the protocol is checked frame by frame against the manual,
including the byte order of the CRC, an exception reply told apart from a broken
line, and a reply from another slave on the bus refused. The write echo must
match the value sent: it is the only confirmation the output really switched,
and accepting the frame without it is how a radio ends up dark behind a green
light.
Framing follows the manual's own rules: 3.5 character times of silence between
frames (4 ms at 9600), and a frame is over when the line falls quiet — Modbus
RTU has no terminator, and a serial read that times out returns (0, nil) here,
so the reader is built on a deadline and a quiet-time rather than on an error
that never comes.
Untested against hardware — nobody here has the supply.
Relay automatic control and the band-change outbound rows both hang off the rig
state, which is right when there IS a rig: changing Band in the entry strip
pushes a QSY, the new state comes back through the CAT callback, and the relays
follow from there — which is why this works for anyone with rig control.
Without a CAT connection nothing is pushed and nothing comes back, so a station
whose rig OpsLog does not control changed band and the antenna switch sat
exactly where it was. Reported as automatic control not working; it was never
told the band had changed.
The entry selector now says so itself, but only when the CAT push did not
happen, and never when the band or frequency lock is on: a lock means the entry
is deliberately decoupled from the rig, and moving an antenna to match a contact
logged from last year is worse than doing nothing.
The frequency is passed as unknown on that path — a band selector gives a band
and nothing else, and rules written on a frequency RANGE are left alone rather
than evaluated against a made-up dial reading.
The de-duplication of "is this a new band" is now shared by both sources, so a
station that has both does not command its switch twice for one QSY.
Also: the same credit line as the QSL e-mail now closes the default recording
e-mail body, on the same terms — a default in the template, so a stored one is
untouched.
The automatic pass fills a blank county only, and deliberately: a value the
operator or QRZ supplied usually beats one derived from a ZIP code. That leaves
no way at all to correct a county already stored — and the Connecticut planning
regions are exactly that case. Every CT contact logged before that correction
holds a county the state abolished in 2022, and filling blanks never reaches
one of them.
So the new entry OVERWRITES, because it is asked for by hand on a chosen set of
rows precisely because the stored value is believed wrong. Only US entities are
touched, only callsigns the database holds, and only when the answer actually
differs — so re-running it on a mixed selection is safe and the count reported
is the number of counties that really changed. Award refs are re-materialised:
the county IS the reference for CQ USA-CA and the state for WAS.
The entry appears only once the database has been downloaded, and appears
without a restart when one finishes — an action that can only ever answer "no
database" is not worth a line in a menu this long.
The SteppIR report was not a SteppIR fault. In the log the antenna starts,
answers every poll, and sits at 21050 kHz while the rig works 21074 — and the
status line only prints when the frame CHANGES, so a link that is alive and
parked looks identical to one that died. The one line that explained it said
"ultrabeam: follow loop stopped", which covers two quite different situations
and, at startup where nothing was running, reads as a fault. Tracking was simply
switched off. It now says which of the two it is, names the antenna type, and
states the consequence — that the antenna will not follow the rig.
Same log, a second and unrelated fault: every launch failed a tune request from
DXHunter with "frequency 2107400000000 out of the 11-digit CAT range".
<FREQ> was read as MHz, but DXHunter had echoed back the value OpsLog itself
published in the N1MM RadioInfo broadcast, whose <Freq> is in tens of Hz. Both
units come from the same peer, so the unit is now inferred: try MHz, kHz, Hz,
tens of Hz, and keep the first that lands on an amateur band — anything a
station is asked to tune to is in one. Hz before tens of Hz because their only
overlap, 40 m against 4 m, is far more likely to be 40 m. Nothing plausible
tunes nothing and says so, rather than sending the rig to a wrong band.
Every setting is per profile, amplifiers included — that is the whole point of
one profile per station. But the amplifier clients were built once at startup
and again only when Settings was saved, so switching profile left them on the
previous profile's port. An operator with an SPE on COM9 for HF and another on
COM10 for 6 m had to open Settings and press Save after every switch. Save is
not a connect button; switching profile is what asks for that hardware.
The amplifier is what was reported, but it was never alone: the motorized
antenna, the Antenna Genius and the Tuner Genius are configured the same way and
were started in the same two places. All four now restart on a profile switch,
asynchronously, exactly as a save does.
Rotators were already correct — their client is built per command rather than
held open, so they read the active profile every time.
Unconditional, like a save: comparing each device's configuration across
profiles to skip a reconnect would trade a second of downtime for the chance of
missing one. A test now keeps the two lists in lockstep — anything started at
boot must be re-applied on a profile switch or be named, with its reason, as
something that must not be.
It was built the other way round on a misreading of two screenshots: the pattern
held {value} and the per-relay boxes held numbers to drop into it. The ask was
simpler and better — {value} is the name typed in Relay labels, so a switch
addressed by antenna name is one pattern instead of eight URLs:
http://10.10.10.100/relay?on={value} relay 1 named Ant1 → ?on=Ant1
Renaming the antenna re-addresses it, and the name on the button and the name on
the wire cannot drift apart because they are the same string. It works in the
per-relay URLs and in the patterns alike, so the per-relay boxes go back to
holding URLs and nothing about them changes meaning any more.
The label is percent-encoded with %20 rather than "+" for a space: "+" is a
space only in a query string and a literal plus in a path, and this can land in
either half of a URL.
{value} on a relay with no label would send "?on=", an empty parameter that most
boards answer with a cheerful 200 and no movement. The driver refuses it and
names the label as what is missing; the editor warns while it is being typed,
beside the empty box rather than after an antenna fails to switch. The labels
also join the driver's cache key — they are part of the wire format now.
buildDeviceDriver had no case for "httpgen", so the generic board fell through
to the WebSwitch driver: it polled an address it had never been given, reported
itself offline, greyed out every relay button, and sent none of the configured
URLs. Nothing in the interface said so — the board was configured, saved and
listed, and simply did nothing. deviceKey did not cover the URLs either, so once
that is fixed, correcting a typo in one would still have handed back the cached
driver holding the old address until a restart.
Two shapes of home-made switch could not be described at all:
- a bit-mask board whose four URLs differ by one character
(/Set0/1, /Set0/2, /Set0/4, /Set0/8) — {value} in the pattern now takes the
number from the per-relay box, keeping the address in one place;
- a board numbering its channels from zero — {relay-1}, since giving up the
pattern for eight hand-typed URLs was the only alternative.
The pattern decides what the per-relay boxes hold, and the grid says which as
soon as {value} is typed: guessing per box ("does this look like a URL?") would
change meaning on a typo, which is not a thing to do to something wired to an
antenna. A URL typed without a scheme gets http:// like the named boards get
from relayBase; https:// is passed through untouched.
Host and the connection test are gone for this type. It has no address of its
own — its relays may each live on a different box — and no status to read, so a
test could only ever answer "OK, 4 relays". Save was greyed out without a host,
which made a complete configuration of four full URLs impossible to store.
Cluster filters no longer persist across launches. A band lock set weeks earlier
is invisible to whoever set it: the counter reads 76 spots live, the grid is
empty, and the search goes to the cluster instead. Nobody loses work by
re-ticking a chip. Grouping and the panel state still persist — they change how
spots look, never whether they appear.
An operator with a tower at each end and an AlfaSpid on both wanted OpsLog to
talk to them directly. Multiple rotors were already there; the missing half was
the protocol.
Rot2Prog and Rot1Prog, over the controller's own COM port. The byte layout is
in the package doc and pinned by table tests against Hamlib's spid.c, the
reference implementation — including the one trap this protocol has: digits go
out as ASCII and come back as raw bytes. Send raw and the controller ignores
you; read as ASCII and every heading is wrong by a constant nobody would
recognise as such.
Two things the UI has to get right because they cannot be detected: the dialect
(different reply length AND baud rate) and the baud list, which for a SPID is
600 or 1200 — offering the usual 4800-and-up would have left the controller
permanently mute. Both are handled: picking SPID sets serial transport, 600
baud and Rot2Prog, and the baud dropdown changes to the rates these use.
The connection test reads a status rather than moving anything, so a wrong
dialect shows up there as a reply of the wrong length instead of as an antenna
that behaves oddly an hour later.
Untested against real hardware — I have none. The frames are pinned; the
controller is the only thing that can confirm the rest.
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.
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.