A quarter-per-poll decay covers the milliseconds between CW elements but not the
gaps that matter on the air: between the words of a CQ, in CW as in SSB, the
meters genuinely read 0 for most of a second and the bars fell with them.
A peak now stands for 1.5 s before it starts to fall, and still rises instantly —
a needle goes up fast and comes down slow. The SWR RATIO is not updated at all
from a zero reading: showing 1.0 during a word gap is worse than showing a stale
figure, because it looks like good news.
A test caught a real defect on the way: the proportional decay stalls on
integers. With the needle at 13 and the truth at 10, a quarter of the gap rounds
to zero and the meter sat three units high for ever. It now always steps down by
at least one, so it converges.
An FM carrier could not answer it: constant by definition, so nothing to
correlate. CW at 100 W did, because the keying itself varies the output:
key down: RM4=25 RM5=207 RM6=13
key up: RM4=25 RM5=0 RM6=0
RM5 follows the RF envelope exactly — it IS the power meter. RM4 sits near 25
whether the key is down or up, so it is not measuring output at all, and reading
it as power is what showed 8 W on a 100 W transmission. The operator's hunch was
right and my first reading of the ramp was wrong: what I took for a needle
rising was RM4 drifting, not tracking.
Watts are now derived from that meter (207 = 100 W, measured) instead of the
power SETTING scaled by a percentage — the setting says what was asked for, the
meter says what left the radio.
The bars also hold their peak with a gentle decay. In CW the meters genuinely
read zero between elements, so following the raw value made them flash to nothing
several times a second; a needle has inertia, and this only ever holds a value
the radio really reported.
One transmission cannot name it. RM4 rose while RM5 stayed flat — which points at
RM4 — but RM5 differed BETWEEN transmissions, 208 then 105, which points at RM5.
Both readings are consistent with either answer, so picking one now would just be
the FT-991A table mistake again in a new place.
The survey line now carries the rig's power setting, and the sample cap is raised
from 12 to 40 so two transmissions fit. Keying at, say, 10 W and then 100 W makes
the answer a one-line comparison: the wattmeter is whichever index moves with the
setting.
The displayed mapping is left alone until that comparison exists.
A second measurement at a known mismatch settled both the index and the scale.
At SWR 1.1: RM6=0. At SWR 1.5: RM6=52, while RM4 kept tracking the power.
52/255 = 0.204, which is the reflection coefficient of a 1.5 SWR to three
decimals. So the raw value is rho scaled to 255, and the ratio is
(1+rho)/(1-rho) — physics, not a curve fitted through two points, which is why
2.0 and 3.0 fall out of it correctly without ever having been measured.
The panel now shows that ratio, the number the operator reads on the rig, rather
than a percentage of meter travel — and a dash while receiving, since a stale SWR
from the last transmission reads as a live one.
Both measurements are recorded in the code and in a test, so the mapping is
evidence rather than a table borrowed from another model — which is exactly how
it came to read 81 in the first place.
Measured on the radio, steady carrier for three seconds:
RM1=0 RM2=unsupported RM3=0 RM4=9→18→21→22 RM5=208 flat RM6=0
RM4 is the index that RAMPS with the output, so RM4 is the power meter. RM5 sat
at 208 from the first sample to the last, unmoved by the power — that is not SWR
on an operator reading 1.1, and 208/255 is exactly the 81 that appeared on the
bar. Borrowing the FT-991A's table, which puts SWR on RM5, is what put it there.
SWR now reads RM6. It stayed at 0 throughout, which is CONSISTENT with a 1.1
match but does not prove the index — only a deliberate mismatch would, and I am
not asking for that. A bar at zero on a good antenna is honest; 81 was actively
misleading.
The measurement is written into the code next to the mapping, so the next person
sees the evidence rather than a table copied from another model.
The one-shot survey fired as the transmission began and caught the meters still
at rest — RM4=13 and everything else zero, which identifies nothing. And the
operator's reading points the other way: the bar showing 81 tracks his 100 W,
while the one labelled power sat at 8.
What names a meter is which index FOLLOWS the power over a few seconds of steady
carrier, so the survey now samples on every poll while transmitting, capped at a
dozen lines. Two seconds of tune will settle it.
Still not guessing at the mapping: the numbers will say which index is power and
which is SWR on this radio, and it gets corrected then.
Speed: with DTR/RTS line keying the PC does the timing, so the Yaesu console's
slider — which sets the rig's internal keyer — changed nothing audible and looked
broken. Both entry points now go through one handler that drives the engine
actually sending, and additionally sets the rig's own keyer whenever a Yaesu is
on CAT, so the radio's front panel shows the same figure.
SWR: an operator reads 80 on the bar with a real SWR of 1.1. That is the shape of
reading the WRONG METER — ALC, say — not of a scaling error, and which RM index
carries which meter is not consistent across the family. Rather than guess again
and move the wrong number somewhere else, the first transmission now logs RM1
through RM6 raw, once. The log will say which index is which on this radio, and
the mapping can then be corrected as a fact.
Tested on the radio: DAKY does not help, KY is refused whatever PC KEYING is set
to, and the "Serial port (DTR=CW / RTS=PTT)" engine on the second COM port keys
correctly. So my earlier "try DAKY first" was wrong, and the order is now the
other way round: name what works, mention the model that refuses.
The KY engine stays. It is documented for the FTDX101 / FT-991A / FT-710 family
and costs nothing to keep — a rig that refuses it now says so in one clear
sentence instead of transmitting nothing for no stated reason.
The operator found the menu I said to look for: it offers DAKY, RTS and DTR. KY
is the CAT route, so DAKY is the setting that would enable it — which makes my
previous "this rig has no CAT keying command" too strong a claim to leave
standing, since it was drawn from a refusal at the DEFAULT setting.
The error and the settings hint now name DAKY first, and keep the serial line
keyer as the fallback. That order matters: the CAT route costs nothing, the
fallback costs a second COM port.
Marked as untested rather than verified — I have no confirmation that DAKY makes
KY work on this model, only that it is the option that should.
Confirmed on the radio: it answers "?;" to KY, so this is not a setting to find.
My previous message sent the operator looking for a MENU → CW → PC KEYING entry I
named without checking it exists on that model, and they could not find it.
The message now names the path that does work: the "Serial port (DTR=CW /
RTS=PTT)" keyer on the rig's OTHER COM port — the standard one — while CAT keeps
the enhanced one. Same for the settings hint and the changelog.
The KY engine stays: it is documented for the FTDX101 / FT-991A / FT-710 family.
It now fails loudly and usefully on the models that lack it, which is the
difference between a dead feature and a wrong one.
The log timed it exactly: the CW send at 13:16:17.591, the CAT dropping at
13:16:17.633, forty milliseconds later.
An accepted KY says nothing, but a REJECTED one answers "?;" — and nobody was
reading it. The frame sat in the buffer until the poll loop's next query picked
it up, failed, and the Manager tore the link down. That is the disconnect on
every macro click, and it also explains the silence: the backend was being
rebuilt underneath the send.
Two changes. The KY write is now followed by a short read: silence means
accepted, "?;" means refused — and the operator is told so, naming MENU → CW →
PC KEYING, instead of getting nothing with no reason. And FA; retries once when
it meets a stray rejection, because a "?;" arriving there is almost never about
FA: the rig answers frequency queries perfectly well, it is the previous
command's refusal being attributed to this one.
This does not yet prove the FTDX10 accepts KY at all. It makes the next run say
so plainly either way, which is the point.
The log settled it: the rig answers "?;" — its "unknown command" — to KY; and to
MG;. So the keyer status query cannot work on this model, and every send spent
four seconds waiting for an answer that was never coming.
"?;" is now recognised as what it is. A command refused once is never asked
again: models implement different subsets, and re-asking costs a 600 ms timeout
on every slow beat for a control that will never answer — which is also why the
panel felt sluggish.
Without a buffer status there is still a real constraint: one KY command carries
24 characters and the rig DROPS the rest silently. So the send is now paced by
how long the text takes to key, from PARIS timing at the rig's own speed. A long
macro goes out complete instead of losing its tail.
That leaves the question the log cannot answer: whether KY <text>; itself is
accepted. If the rig also replies "?;" to it, the CAT menu's PC KEYING setting is
the next suspect — but nothing in the code will be guessing at it.
Clicking a macro dropped the CAT for a few seconds, keyed nothing, then came
back.
ask() returned the first ';'-terminated frame it saw, whatever command it
belonged to. KY produces NO reply, so the next query — FA; from the poll loop —
collected a leftover frame, failed to parse it as a frequency, and ReadState
reported an error. The Manager reads that as "lost the rig": disconnect, wait,
reconnect. Hence the drop and the automatic recovery a few seconds later.
Replies are now matched to the command that asked for them: anything else is
discarded and logged, so a stray frame costs one log line instead of the link.
This also explains the silence — the send never got a clean run at the port
while the backend was being torn down under it.
"the keyer buffer stayed full for 4s" and nothing keyed, on a rig whose CAT is
working. The buffer was not full: the check demanded the reply be exactly "KY0;"
with the digit at byte 2, so anything the FTDX10 phrases differently — a space
before the digit, another command's reply arriving first — read as "still full"
on every poll until the deadline.
The test is now asymmetric on purpose: only a clear "1" holds the send back.
Anything unrecognised goes ahead. Refusing to transmit because a status line was
phrased unexpectedly is the worse failure — the operator gets silence with no
explanation, where at worst sending early truncates a long message, which the
chunk loop then recovers from.
The reply is also logged once per run, since we have no verified sample of it —
that line is what turns the next surprise into a fact instead of a theory.
Three places decide what a rig keyer is, and the Yaesu was only in two of them.
The CW panel's status came from a list naming icom and flex; anything else fell
back to the WinKeyer status, which reports disconnected because no WinKeyer is
attached. So the panel said the rig CAT was offline while the console beside it
was reading the FTDX10 perfectly.
The send loop had the same gap: a rig keyer BUFFERS the whole message, so the
wait before <LOGQSO> is a length estimate, while WinKeyer watches a busy echo
that will never arrive here. Auto-call would have raced the transmission.
This is the third time the same shape has bitten in this feature — a list of
engines that needs every member named, with a silent fallback for the rest.
Grepping for the pair "icom || flex" is what finds them.
Choosing "Yaesu (rig keyer)" saved correctly and still produced a WinKeyer panel
asking for a COM port. The engine is normalised on load through a chain that
names icom, flex and serial and maps EVERYTHING ELSE to "winkeyer" — so the
value came back as WinKeyer on every read, whatever the settings said.
That is why the panel offered COM3 then COM10: it genuinely believed the engine
was a WinKeyer. The list now names yaesu too.
The same shape of bug is worth watching for: a normaliser with a silent default
turns an unknown value into a plausible one instead of an error, and the symptom
appears far from the cause — here, in a panel three components away from the
setting.
Selecting the Yaesu keyer fell through to the WinKeyer branch of the settings,
which offers a serial port picker and a Connect button. So the operator went
hunting for the right port — trying the standard COM and the enhanced one in turn
— and neither connected, because this keyer uses NEITHER: it keys over the CAT
link already configured in Settings → CAT.
It now has its own branch, like the Flex one: a speed field, the warning when the
CAT backend is not a Yaesu, and a line saying explicitly that keying rides the
CAT link. Speed changes go to the rig's keyer instead of a WinKeyer that is not
there.
Reported on an 8x2: port A correctly on the 80 m vertical, then a few seconds
later both A and B shown on the same beam.
The display preferred the TX antenna, falling back to RX. On an 8x2 only ONE port
can hold the transmit antenna, so the switch reports the same txant on both — and
every keepalive poll redrew port A as whatever port B transmits through. The RX
antenna is the per-port selection, and the only thing Activate sets, so it is
what a port shows; TX stays the fallback for a port reporting no RX antenna.
A port change is now logged with the raw line — but only on CHANGE, since the
device pushes state every few seconds and logging each one would bury the rest.
Without any trace, "port A jumped to the wrong antenna" cannot be checked.
Two tests pin it: rx wins over a disagreeing tx, and a message about one port
never moves the other — which is the shape that was actually on screen.
The radio has a keyer and a command to feed it (KY), so an FTDX10 needs no
WinKeyer and no second cable, exactly as the Icom CI-V and Flex CWX engines
already do. The rig keys with its own timing, which is why the spacing is right
where a PC keying a line through USB latency drifts.
Text is filtered to what the keyer can actually send: an unsupported byte does
not produce an error on a Yaesu, it can abort the whole buffer, so the rest of a
macro would vanish silently. It is then fed in 24-character pieces, waiting for
room between them — the rig DROPS what does not fit, again with no error, so a
contest CQ would lose its tail.
STOP is the honest gap. Yaesu documents no buffer-clear, so it drops the
transmitter (TX0) instead: nothing queued reaches the air, which is what Escape
means to an operator. It deliberately does NOT send "KY0;" — a plausible-looking
clear that the rig would read as the CHARACTER zero and transmit.
A test caught a real one on the way: tabs and newlines were dropped as
"unsupported", gluing the words either side together, so a macro written on two
lines went out as CQCQ. Whitespace now becomes a word gap before filtering.
Settings warn when the Yaesu keyer is selected without the Yaesu CAT backend —
otherwise it simply never keys, with nothing on screen saying why.
Send path follows the CAT reference and how Hamlib drives these rigs; NOT yet
verified on the air.
Confirmed on the FTDX10: receiving on 14.018.98 and pressing SPLIT put 14.019.98
in the big display with "TX 14.019.98 (0 kHz)" under it. The radio was right —
RX 14.018.98, TX 14.019.98, up 1 kHz on CW — the panel was not.
RigState follows the ADIF convention where freq_hz is the TRANSMIT frequency, so
under split it is the OTHER VFO. Taking it as the main display showed the
operator the frequency they transmit on, and then an offset of that frequency
against itself: zero.
The header now reads the listening frequency (freq_rx_hz when split, freq_hz
otherwise) and the TX line shows the real transmit frequency and offset.
Microphone gain and VOX are meaningless in CW: the rig ignores both, so showing
them is showing dead controls. They are hidden, and a CW card takes their place
with keyer speed (KS), break-in (BI) and ZIN (ZI), the zero-in that retunes so
the station being received lands on the operator's own pitch.
ZIN is a one-shot with no state, so it is a plain button rather than a chip that
would look latched, and no settings read-back follows — the frequency change
arrives through the normal poll like any other.
The keyer values are read on the slow beat whatever the mode, so the card is
already populated the instant the operator switches to CW instead of filling in a
poll cycle later.
Which controls show is decided by the RIG's mode, not the logged one: the logged
mode can be a digital sub-mode the radio knows nothing about.
The bar was one colour for its whole travel, so nothing distinguished a signal
that is merely readable from one that belongs in the log as 59+20.
Green to S9, amber through the S9+ range, red from +20 dB. The thresholds are
computed from the SAME S9 point the label uses rather than hard-coded
percentages: the S9 position is still a hypothesis on this rig, and when it is
corrected the colours have to move with it or the meter would say 59+20 in
amber.
Reported on the FTDX10: listening on 14.244 with VFO B still holding 18.115 from
an earlier session, pressing SPLIT threw the transmitter onto another band. The
button only flipped the rig's split flag, and the other VFO is stale by nature —
the only transmit frequency that makes sense is one derived from where the
operator is listening NOW. SPLIT therefore places the TX VFO too: up 1 kHz on CW
and the data modes, up 5 kHz on phone, the offsets operators actually call. The
+1k / +5k buttons remain for anything else, and a test pins the mapping.
The panel also drew its own flat meters while the Flex and Icom consoles use the
shared LED-segment MeterBar. Two instrument styles in one application is just
inconsistency — it now uses the shared component, and the local one is gone.
And the three consoles are named alike: "Flex Console", "Icom Console", "Yaesu
Console", in the tabs and in the Main-view pane list, in both languages.
Three corrections from the operator's second pass.
The sideband gesture was wrong: I used double-click, which hides the action.
Clicking a button that is ALREADY active now flips its sideband — CW-U → CW-L →
CW-U. One button, one finger, nothing to discover.
A lit SPLIT chip does not tell an operator anything useful: it says split is on,
not where they transmit. The header now shows the TX frequency and the offset in
kHz whenever split is active.
And the offset that matters is set in one action: up 1 kHz on CW, up 5 kHz on
phone. Doing it by hand means swapping VFOs, retuning and swapping back — exactly
the fumbling a panel exists to remove. Both are offered rather than picked from
the mode, because which one is idiomatic is the operator's call, and the button
turns split on at the same time.
The offset is measured from the RECEIVE frequency and written to the VFO we are
not listening on, so it stays correct when the operator works on VFO B, where the
roles are mirrored.
Capping the width without mx-auto pinned the console to the left edge with an
empty window beside it. It now uses the exact wrapper the other two panels use —
h-full min-h-0 overflow-auto bg-background, then max-w-5xl mx-auto p-3 — rather
than a second layout of my own invention.
First look on the FTDX10 turned up six things:
The S meter printed a raw percentage — "57" tells an operator nothing, and it is
the S number that goes into a report. It now reads S1-S9/S9+dB, the same value
the click-to-fill RST already used.
CW, RTTY and the data modes exist on BOTH sidebands and the operator is the one
who knows which they want. The buttons now carry the rig's actual sideband and a
double-click flips it; PSK is added, riding the rig's DATA mode as it does on the
radio itself. This also means the mode row drives the rig directly (MD0 with the
exact mode) instead of going through the ADIF path, which could only pick a
sideband by convention.
The attenuator is a 6/12/18 dB pad on these rigs, not the single step I assumed —
two thirds of the control was unreachable.
Sliders had no visible filled side: --muted is barely lighter than the card it
sits on, so the whole track read as one bar. They also came in three kinds (two
bare range inputs among them). One component now, explicit track colour, and it
takes a min/max so power in watts and DNR 1-15 look like the rest.
The panel stretched across the whole window; it is a column of controls, so it is
now capped and every row stays readable.
And it gets its own Yaesu tab, like FlexRadio and Icom, rather than only being
available as a Main-view pane.
A console pane for the native Yaesu backend, in the same shape as the Icom and
Flex ones: S/PO/SWR meters, band and mode rows, AF/RF/squelch, AGC, the IPO/AMP1/
AMP2 front-end selector, ATT, NB, DNR + level, narrow filter, power in watts, mic
gain, VOX, split and ATU tune.
Three decisions worth keeping:
Panel reads are STAGGERED and live in their own file, away from ReadState. Meters
poll every cycle; settings only change when someone turns a knob, so they refresh
every 8th cycle and right after any set. Polling all of it every cycle would put
twenty queries a second on the serial link the frequency display shares.
Band buttons use the rig's own band memory (BS) rather than a frequency we pick,
so 20 m lands where the operator last was on 20 m — what the radio's own band
keys do.
A set is followed by a read-back, so the panel shows what the RIG ended up with,
not what we asked for; the two differ whenever a value is out of range or the
mode forbids the control. And a control the model lacks keeps its previous value
instead of dropping to zero, which reads as a setting that reset itself.
The S9 point of the S-meter scale is a hypothesis (the manual does not state it)
and is commented as such — one number to correct if reports come out an S unit
off, rather than a fudge spread through the RST helper.
Also removes the FlexRadio settings blurb, which explained the backend to
someone who had already chosen it.
Xiegu speaks CI-V with a reduced command set: frames, BCD encoding, addressing
and the opcodes for frequency, mode, PTT and split are Icom's, so this reuses
internal/cat/civ wholesale instead of re-deriving a codec.
It is a SEPARATE backend rather than the Icom one at address 0x70, because what
the two rigs do NOT share is the deciding part. The Icom backend reads the
spectrum scope, the DSP block, data mode via 1A 06 and the model id via 19 —
none of which a Xiegu implements. Pointed at a G90 it would poll every cycle for
answers that never come, and spend its silence tolerance on commands the radio
was never going to support.
Two consequences of the rig's smaller mode table are handled explicitly rather
than left to fail: there is no data mode, so a digital QSO is set to plain
sideband (what the operator does on the radio anyway) instead of being refused;
and the split TX frequency is NOT reported, because reading the unselected VFO
needs 0x25, which the Xiegu table does not list — a split flag carrying a wrong
TX frequency is worse than the flag alone, since the frequency is what gets
logged.
The published command table has rows that slipped during typesetting (0x07 and
0x0F share a block). Where it contradicts itself the Icom meaning is used, the
rest of the table matching Icom exactly, and every unexpected reply is logged
raw so a first on-air run settles it.
NOT yet verified on a radio.
MSHV worked immediately, JTDX answered "Hamlib error: Invalid parameter while
setting frequency". The two use different Hamlib dialects: MSHV sends
"F 14074000", JTDX names the target first — "F VFOA 14074000". The VFO name
landed in the slot the frequency was read from, the parse failed, and we returned
RPRT -1, which is exactly the error JTDX reported.
A leading VFO name is now stripped from every command's arguments. It costs
nothing: OpsLog follows the rig's own VFO selection, so the name carries no
information we act on — but refusing it locked out a whole family of clients.
Both dialects are covered by a test, the plain one included, since this is an
addition and must not become a swap.
A rejected frequency is also logged with the raw line now. The client only shows
"Invalid parameter", which says nothing about what it actually sent — that is
why this took a screenshot to diagnose rather than a log.
A native CAT backend owns the rig's serial port, and Windows gives a COM port to
one process — so choosing native CAT locked WSJT-X, MSHV and JTDX out of the
radio entirely. That is the cost of dropping OmniRig, which was itself a sharing
layer, and it has to be paid back.
OpsLog now becomes the server, as wfview does. It speaks the Hamlib net rigctl
protocol, which every one of those programs supports natively (rig model "Hamlib
NET rigctl", 127.0.0.1:4532) with no driver to install. It sits in front of the
MANAGER, not a backend, so an operator on OmniRig, Flex, Icom or TCI gets the
same server.
Two details that decide whether a client works at all rather than degrading:
dump_state is parsed positionally and WSJT-X refuses to proceed without a
well-formed block, so it is written out in full and its shape is pinned by a
test; and set_vfo / set_split_vfo answer RPRT 0 rather than an error, because
OpsLog follows the rig's own VFO and a refusal makes WSJT-X abandon the
connection. Unknown commands answer RPRT -11 — never silence, which hangs a
client instead.
The whole protocol is tested against a fake rig, plus one end-to-end exchange
over a real socket, since the framing is as much the contract as the text.
Also: the Yaesu backend is confirmed working on a real FTDX10 (frequency, mode,
VFO, split), so its "not yet verified" note is now wrong and is corrected.
Every Yaesu fault reported so far came from OmniRig's interpretation layer, not
from the radio: a rig file that never exposes the VFO, a Freq property meaning A
on one model and B on another, a split flag that alternates between polls. This
talks to the rig directly, so what the radio answers is what is shown.
Modern Yaesu CAT is plain ASCII with a ';' terminator — FA/FB for the VFOs, MD0
for the mode, VS for the selected VFO, TX to key. Frequency is written to the VFO
the operator is actually on, not always to A, which is the failure that made a
display disagree with the radio.
Two things are genuinely uncertain across the family and are treated as such
rather than guessed. SPLIT is read through ST, then FT if the rig ignores ST —
whichever answers wins and the choice is remembered, because the two commands say
DIFFERENT things (a split flag vs which VFO transmits). If neither answers, split
is reported OFF and the fact is logged, rather than invented. Unknown model ids
and mode bytes are logged raw for the same reason.
Split resolution, frequency parsing and the mode mapping are pure functions with
a table test — the OmniRig equivalent is where every Yaesu bug lived, and it had
no test until late.
Written from the CAT reference; NOT yet verified on a radio.
The log settled it: the backend was right all along. Every set produced
"readback +1.5s FreqA=21140000 Freq=21140000 -> shown 21140000" and published
cat:state with the new frequency and band. The display did not follow.
The fault is in the frontend. Changing band does two things at once: it opens the
1.5 s freeze that protects what the operator is typing, and it commands the rig.
The rig's answer comes back in ~170 ms — inside that freeze — and the handler
DROPPED any snapshot arriving during it. The backend only emits on change, so
nothing came afterwards, and the strip kept the old frequency until the VFO was
nudged. Changing band from the radio always worked because no freeze was open.
A dropped snapshot is now kept and replayed when the freeze closes, rather than
discarded. Further typing simply defers the replay again.
An import that drops the locator leaves it missing on a whole batch, and fixing
that one QSO at a time is exactly what bulk edit exists to avoid.
Added on all three sides in lockstep — the dialog field, the field-to-column map,
and the repository whitelist — because they are separate lists and offering a
field the repository refuses fails only at Apply, after the operator has selected
the QSOs. The existing contract test covers it.
The callsign and RST stay excluded, as before: bulk-setting those corrupts a log.
The locator does not carry that risk — it identifies a place, not a station, and
a wrong value is simply overwritten again.
An operator reports that "QRZ.com received status = N" returns rows showing both
N and Y (issue #5 follow-up). The SQL is a plain col = ?, but reading it cannot
distinguish a wrong query from a UI that kept the previous rows after an error —
so this runs it: five QSOs inserted, filtered, and both the list and the count
asserted. The backend filters correctly, list and count agree. The fault is not
in the query.
Found while looking: the filter builder tested an ADIF date with /^d{8}$/ instead
of /^\d{8}$/. The escape was missing, so the branch never matched and the
calendar input was handed "20260728", which type=date rejects — an empty box
over a value that was really stored.
Adds the IC-7300MKII at CI-V 0xB6.
Doing so exposed a drift between the two hand-kept copies of the model table: the
settings offered the IC-7700 at 0x88 and the IC-7800 at 0x80, which are the
IC-7100's and the IC-7410's factory addresses. Picking either set an address the
rig never answers on — the symptom is a radio that simply stays silent — and the
backend then named it as the other model. Corrected to 0x74 and 0x6A, and the
four models the backend already knew (IC-7100, IC-7410, IC-7600, IC-7851) are now
offered too instead of forcing a manual address.
A test reads the model list out of the .tsx and asserts civ.ModelName agrees, so
the next model added on one side alone fails the build rather than someone's
radio.
The network backend treated "control link alive but no CI-V reply" as the rig
being in standby, and tolerated it WITHOUT BOUND. When another program takes the
CI-V session — WSJT-X through OmniRig, or the Remote Utility — the rig goes on
answering pings on the control stream while sending us nothing at all. Alive()
stayed true, so ReadState returned the cached frequency with err == nil, the
Manager never saw a failure, never reconnected, and re-published a frozen number
with a fresh timestamp on every poll. Only restarting OpsLog cleared it.
Bounded now, on the last SUCCESSFUL read. Past the grace the error is reported so
the Manager tears the session down and reconnects, which re-takes the CI-V
stream. Also fatal immediately: the CI-V reader goroutine having exited — no read
can ever succeed after that, however healthy the control link looks.
The grace backs off to minutes when the silence persists, because the two cases
pull opposite ways: a stolen session recovers on the first attempt, while a rig
switched OFF is silent for hours and re-tearing its session every 30 s would
blink the panel — and its ON button — away continuously. A good read resets it.
The decision table is pinned by a test; the standby case (never answered since
connect) keeps the old tolerate-for-ever behaviour.
ULS was wired into AddQSO alone, so an operator who downloaded it and has no
QRZ.com/HamQTH account saw nothing while typing a US call: cty.dat gives the
country, the zones, and a 4-character grid that is the ENTITY centroid — a
thousand kilometres from the station. The county and the real square were stamped
after logging, too late to point an antenna with.
The enrichment runs last in the lookup wrapper and only fills blanks, so a
provider always wins. ULS carries no name and no address, so it completes a QRZ
record and can never replace one.
The grid needed a rule of its own. refineGrid keeps what is there unless the new
value extends it — correct for a QRZ square, wrong against an entity centroid,
which is simply a different square. A per-callsign FCC square therefore beats any
4-character grid, while a 6-character one already present is left untouched.
Lat/lon are recomputed only when we actually moved the grid.
Portable calls are skipped: W1AW/4 is not what the FCC licensed.
The profile's MY_* metadata is derived from the callsign through cty.dat, and the
effect that derives it ran on every load — so opening the settings counted as
"the source changed" and overwrote whatever the operator had typed. Reported by
an operator in CQ 4 / ITU 4 handed 5 and 9: he corrected them, and each restart
put 5 and 9 back.
cty.dat gives the zones of the ENTITY, and a large country spans several, so the
automatic value cannot be treated as authoritative — it is a starting point. A
load now fills only fields that are EMPTY; a recompute that overwrites happens
only when the callsign or grid itself changes, which is the case the derivation
exists for.
The load-vs-edit distinction is a ref holding the profile id the values were last
derived from — first sight of a profile is a load.
"Last download" and an operator-chosen date are different questions, and they
now ask QRZ different ones. The automatic run wants everything TOUCHED since it
last ran (MODSINCE) — that is what returns a 2015 QSO confirmed yesterday. A date
typed by the operator names a period of OPERATING, so it maps to BETWEEN date and
today, on the QSO date.
The client-side date filter is skipped only under MODSINCE, where it would throw
away the older-but-newly-confirmed records the query exists to fetch. Under
BETWEEN it agrees with the server and stays on as a backstop.
The date window was sent as OPTION=AFTER:<date>. That was a guess, and QRZ
rejects the request outright — RESULT=FAIL with no reason — so the confirmation
download failed completely for everyone, whatever the account.
The documented option is MODSINCE, and it is the better window anyway: it selects
records MODIFIED since the date, so a QSO from years ago that was confirmed
yesterday comes back. A QSO-date window structurally cannot return those, and
confirmations are the whole point of this download — which is why the client-side
date filter is now skipped when the server did the windowing, instead of throwing
those same records away on arrival.
A refusal now falls back to ALL rather than leaving the operator with nothing.
Reported as a regression — reopens on the wrong screen — but the window code is
untouched since it was verified working: the only change to main.go since is the
background colour. So the fault is in the DATA, and there was no way to see it.
Both ends now log their coordinates, plus where the window actually landed after
the un-maximise / move / re-maximise dance. That separates the cases, which need
opposite fixes: nothing was captured, something wrong was captured, or the right
corner was captured and Windows moved the window anyway.
Reverts 30d8827. Ctrl+C / Ctrl+V already work in the app, so the WebView's
default menu only added Refresh / Save as on the neutral areas — noise in an
application window, for nothing gained. Changelog entry dropped with it.
Wails disables the WebView context menu in production, so an operator could not
paste a cluster address, an API key or a callsign anywhere in the app. Ctrl+V
did work — nothing intercepts it — but the menu is where people look, and a key
copied off a web page is exactly what you paste rather than retype.
EnableDefaultContextMenu turns it back on.
Every port box was parseInt(e.target.value) || <default>. Delete the contents
and parseInt gives NaN, so the default is written straight back: the digits
reappear under the cursor and the only way to enter a different port is to
overwrite it character by character. Reported on the cluster editor; the same
line existed in eight other places.
One PortInput component now holds the raw TEXT as its state and only tells the
parent about a value that is actually a port. An empty box stays empty while you
type; on blur an invalid one falls back to the default, so nothing is ever saved
as 0 or NaN. It adopts a value arriving from the backend only while the operator
has not started typing — re-syncing mid-edit is the behaviour being fixed.
This is the controlled-input trap the project notes already warn about: keep the
raw text in local state and derive the stored value from it.
Same slip as before the 0.21.6 release: I appended to a block that had already
shipped. 0.21.7 is back to its 12 published entries; the microwave-band fix
opens 0.21.8.
Reported on 3 cm. cat.BandFromHz stopped at 23 cm, so a 10 GHz frequency came
back EMPTY — and an empty band is not cosmetic: the QSO is logged without one,
counts in no award slot, and exports with no BAND field. The low end was short
too (2190m / 630m / 560m).
Four band tables had to be extended because four exist: the CAT one, the award
plan in app.go, the cluster spot classifier, and the frontend's. Plus the places
that LIST bands — the QSO editor and award-definition pickers (you could not set
3 cm by hand either), the statistics axis, and the award band ORDER, where a
missing band sorts to the end instead of in frequency order.
Ranges and names are ADIF 3.1.7, which is what an export has to carry.
A test now pins one frequency per band across the Go tables and asserts they
agree — that is what was missing, four hand-maintained copies with nothing
checking them. It also pins that an out-of-band frequency stays empty rather
than snapping to the nearest band, which would file a QSO under a band the
operator never used.
Left out as "internal", but the log is something operators read and send —
and the Flex meter removal, which is the same nature, was listed. Consistent
now.
An FTDX10 operator reports that changing band from OpsLog moves the rig but the
displayed frequency stays on the old band until they nudge the VFO. The existing
readback is taken immediately after the write, so it always shows the old value
and proves nothing: OmniRig queues the CAT command and sends it over serial.
A second readback is now logged ~1.5 s later, from ReadState (the goroutine that
owns the COM apartment), together with what OpsLog concluded. That separates the
two candidates, which look identical to the operator: the rig ignored the write,
or the rig moved and its rig file never re-reads the frequency.
The backend already parsed the atu object and exposed ATUStart / ATUBypass /
SetATUMemories through the Flex controller and the App bindings — only the UI
was missing, so the radio's own tuner was the one thing on the panel you still
had to reach SmartSDR for.
Placed under TUNE, which is the order of operations: the tuner needs a carrier
to measure against.
The status is decoded into words rather than shown raw. That is the point of the
block: TUNE_FAIL and "never tuned" leave the radio looking identical on its own
front panel, and the operator transmits into a bad match either way. Failed is
red, tuning amber, tuned green.
Switching station is the frequent act and editing a profile the rare one, but
the callsign opened the settings — so changing station took four clicks and a
scroll. It is now a dropdown of every profile: pick one and it activates. The
active one is ticked and disabled, and "Manage profiles…" stays at the bottom
for the rare case.
The list reloads on every profile change, so a profile added or renamed in the
settings appears without a restart.
The UTC clock leaves the header for the status bar, next to the database: in the
header it sat beside the frequency in the same weight and competed with it for
the eye, while being something you consult rather than watch.
0.21.6 was published before these landed, so its notes must stay exactly as they
shipped — a released block is a record of what an operator actually got, not a
place to keep appending. Restored it to the 11 entries in the release commit and
opened 0.21.7 for the eight that followed.
The bulk-edit line was the one subtlety: it was published in 0.21.6 and then
REWRITTEN in place to cover the filter builder. 0.21.6 keeps its published
wording; the filter half is new and gets its own 0.21.7 entry.
An operator reported migrations being very slow and sent a log with three
interleaved runs — and no way to tell one database migrated three times from
three databases migrated once. Each line now carries its target: the SQLite file
name, mysql:<database>, or baseline:memory for the in-memory pass that derives
the schema for a FRESH MySQL database (that one is fast and is not a real
database — in the report it sat between two slow runs and looked like a third).
A pass also announces how many migrations it will apply, so a run that applies
nothing is visibly distinct from one that does.
Three faults compounding, all visible in one operator's report (117k QSOs):
1. We asked for OPTION=ALL every time, so QRZ serialised the entire logbook —
56 MB — and cut it short. The parser stopped at record 27832 with "unexpected
EOF" and everything past that was never read. The window is now sent to the
server (OPTION=AFTER:date); the client-side date filter stays as a backstop.
2. The last-download date was stored RIGHT AFTER the fetch, before knowing
whether the records had been processed. So the truncated run still advanced
the window past data it had never read — and every later run skipped it. It
is now written at the end, and only when the ADIF parsed cleanly. A window
that moves past unseen data is worse than one that re-reads.
3. Which is why the run in the report ended "Confirmed 0, added 0 (of 0
returned)" despite parsing 27832 records: an earlier truncated run had
already pushed the window to yesterday, so every record was filtered out
client-side. The operator could not recover without clearing the key by hand.
A truncated download now says so in plain words, and says the window was not
moved.
My mistake yesterday: the new confirmation columns went into uploadStatusCols —
the QSL Manager's FILTER whitelist — instead of bulkEditableCols. The dialog
listed QRZ.com received status and the ten dates, and the repository refused
them at Apply, after the operator had selected 54 QSOs.
Moved to the right map. And two fields have been broken this way since long
before: State and County were offered under "Contacted station" (whose IOTA /
POTA / SOTA / SIG siblings are all allowed) but were missing from the whitelist,
so choosing one always failed. Added — they are exactly what an import loses and
what a whole run shares.
The real fix is the test. Two lists in two languages had to agree by hand and
nothing checked it, so the failure only ever surfaced as an error message at the
last click. The new tests read the ACTUAL field ids out of BulkEditModal.tsx and
FilterBuilder.tsx and assert the repository accepts every one — a field added to
the UI alone now fails the build instead of the operator.
They arrive many times a second and were dumped every 5 s, drowning the rest of
the log. They only ever existed to confirm the meter NAMES during development,
which the subscription line already does — and that one is now a single summary
instead of one line per meter, a Flex announcing dozens of them at each connect.
Grid on the UDP path — WSJT-X and MSHV can only send a FOUR-character grid,
that is all the FT8 protocol carries. The enrichment rule there is "fill only
what is empty", so the coarse JN05 always won and QRZ's JN05JG was thrown away:
roughly 100 km of accuracy discarded on every digital QSO. The lookup grid is
now taken when it EXTENDS the received square. A lookup that disagrees outright
(JN18 against JN05) is not a refinement — the station may be portable and what
came over the air is then the better record. Both rules are pinned by tests.
Manual fetch in the QSO editor — it read the CACHE, valid for 30 days. An
operator who upgraded their QRZ subscription went on getting the thin
free-account record and clearing the cached row by hand was the only way out.
The button now forces a lookup that bypasses the cache, reaches the provider and
REFRESHES the stored row, with a 15 s budget instead of 2 s: a deliberate click
must reach the network, where giving up early would fall back to cty.dat and
look like nothing happened. The automatic type-ahead lookup keeps the cache,
which is where it earns its keep.
Same fetch: it used `??`, which only guards against null. Go marshals an unset
string as "", so a QRZ record with no grid BLANKED the grid already in the QSO.
The lookup still wins — that is the point of asking for it — but an empty
result no longer erases a good value.
"Upload" was wrong for half of them: a paper QSL is not uploaded. Every entry
now says whether it sets a STATUS or a DATE, and in which direction — "QRZ.com
sent status", "LoTW received date" — which is also how the ADIF fields read.
Added: the QRZ.com received status (the column existed, it was simply never
offered) and the ten confirmation DATES — QSL / LoTW / eQSL / QRZ.com in both
directions, Club Log and HRDLog outbound only, those two having no return
channel. Dates use the same picker as the QSL Info tab, with a Today (UTC)
button; clearing the field writes an empty value, which is how a wrong date is
removed from a batch.
Fields are grouped by CHANNEL now, each status followed by its date, in the same
order as the QSL Info tab — a longer list, but nothing to hunt for.
The repository whitelist is extended to match: without it the new columns would
have been refused at write time. The old ids (qrz_upload, clublog_upload,
hrdlog_upload) still resolve, in case anything still holds one.
Statistics — the activity chart ignored the period selector: picking a year up
top left it showing the last 7 days, two controls contradicting each other. The
buttons were four fixed rolling windows anchored on the newest QSO, not
granularities. They now choose the BUCKET SIZE over the selected period, with an
Auto default and a step-up when a bucket would be too fine (the backend drops a
series past 750 buckets rather than ship 6000 unreadable bars). The hour-of-day
histogram covered a single day, too little to read anything from; it moves to
its own card with a weekday view, over the whole period.
Band map — the CW/data/phone sub-bands were shaded with the STATUS tokens, the
same amber that means "new band" on the pills drawn on top of them. A sub-band
is an identity, not a state: categorical hues now, validated in both themes
(violet failed on the dark steps — 1.9 ΔE from blue for a protan reader) and
added to the legend, since identity must never be colour-alone.
Frameless window — the OS title bar was a dead 32px band above a window that
already has its own. Minimise/maximise/close move into the app header, which
carries the drag region and double-click-to-maximise. The drag opt-out is by
ROLE in CSS, so a future button in that bar keeps working without anyone
remembering the rule.
Fixes:
- Saving settings froze the window while a device was slow: restarting a link
waits for its poll goroutine, which can sit 45 s inside an uninterruptible COM
Connect when another program holds the rig. The restart is now off the UI
path; a slow one is logged.
- Multi-screen: a MAXIMISED window was never repositioned, so it came back on
the primary screen every launch. The corner is now recorded even when
maximised (it names the monitor) and re-applied while still hidden.
- The Callsign field is marked out at rest — operators were typing the call into
Name, which sat beside it and looked identical.
- QSL dates get a real picker (native, for the OS calendar and locale order); a
malformed old value stays in a text box rather than vanishing behind an empty
one.
Also: a Support OpsLog entry in Help, and a WinKeyer protocol trace. The WK2
report (one element then a ten-second pause, sidetone that will not switch off)
is NOT fixed here: the WK1/WK2/WK3 command sets differ and a guessed fix would
break the operators it works for today. The trace logs every byte with the
command name and spells out the firmware family, which is what will settle it.
Moving the folder (C:\OpsLog → D:\OpsLog, or onto a stick) broke everything:
config.json and each profile's logbook path were absolute, so on the new
machine the pointer named a drive that no longer applied.
The logbook case was the dangerous one. db.Open CREATES what is missing, so if
the stale C: path happened to be creatable, the operator silently got a NEW
EMPTY logbook instead of an error — with their QSOs sitting untouched in the
folder they had just copied.
A path inside the application folder is now stored relative to it and resolved
against the current location at read time. A path OUTSIDE it (a synced folder,
a chosen drive) stays absolute and untouched — that is a deliberate choice; it
is only re-rooted when it has gone missing AND a file of the same name exists in
this install's data folder, i.e. the copied-folder case. With no such twin the
path is left alone so the failure is reported rather than papered over with an
unrelated database. Both rules are pinned by tests.
The Main-tab pane and the full Recent QSOs tab are the same component sharing
one storage key, but the pane is about half as wide — so it wants fewer and
narrower columns. Whichever was opened last rewrote the other's widths.
The pane now stores under "mainpane", which also scopes its award-column
visibility and widths. The full tab keeps the historical key, so its layout is
untouched; the pane starts from defaults once.
1. A language change rebuilt the column defs, which sets the anti-clobber
guard, but the effect that clears it listened to only two of the rebuild's
causes. Guard stuck on → every column change was silently discarded for the
rest of the session. Now keyed on the rebuilt defs themselves, so any future
cause is covered.
2. The cluster grid had no guard at all: the same rebuild wrote its defaults
over the saved layout, in the cache AND the database. Unrecoverable.
3. Worked-before and cluster read their state before the active profile was
known, so they looked up the unscoped cache key (always a miss) and then
saved under the scoped one. They now wait for the scope and remount on a
profile switch, like the main log.
4. GetUIPref returns an ERROR, not "", while the settings store is not yet
scoped — deliberately distinct from "unset". The frontend read it as "no
preference", rendered defaults, and the first column event overwrote the
good copy. It now retries instead of giving up.
5. The QSL manager had no storageKey, so it shared the main log's layout and
each rewrote the other.
Also: the DB write is debounced (a resize drag fired dozens of writes) with a
flush on close, and a rejected write is retried rather than dropped.
On its CAT/AUX connector an ACOM is the MASTER: it polls a transceiver and
changes band from the reply, so nothing can be pushed to it. internal/catemu
answers those polls on a second serial port, in ACOM command set 5 (Kenwood /
Elecraft): FA;, FB;, IF; and ID;, and nothing else — a wrong-length reply is
worse than none, it desynchronises the amp's parser for the following poll.
Also offered for SPE. Some amps do not poll at all but read the radio↔PC CAT
line in parallel; those never hear a responder, so an optional unprompted send
(500/1000 ms) covers them.
The TX frequency is what is sent: in split the amp must be tuned where we
transmit. Frame lengths are pinned by tests — the failure they guard against is
silent and only shows up as an amp that mistunes.
Untested on hardware.
The release prompt was answered with a missing dot and the script propagated
it verbatim into both constants, so the shipped exe reported "021.4".
That is not cosmetic: versionLess splits on dots, so "021.4" parses as [21,4]
and compares GREATER than "0.21.5" — a client on that build would be told it
is up to date forever and never offered another update. It also broke the
What's new dialog on a fresh install, where the changelog entry ("0.21.4") is
matched against appVersion by string equality.
Same code as the release otherwise; only the version string changes.
OmniRig reports the VFO pair (AA/AB/BA/BB) on the whole Yaesu range and never
the single-letter form. Only the latter was honoured, so every rig in that
family stayed pinned to VFO A: pressing SUB moved the radio but not OpsLog,
and a QSO worked on SUB was logged on the main VFO's frequency. The first
letter of the pair is the VFO being listened on — Log4OM reads it and gets the
right frequency on the same rigs, which is what showed the data was there.
The enum now wins over the Yaesu Freq==FreqB inference, which is only a
fallback for a rig file that names no VFO at all (the stock FTDX10 one answers
neither VS; nor FR; usably — verified on the air).
Split: the ON flag is still latched to survive a rig file that flips it on its
own, but the latch is now ARMED only after 8 flips in 30 s. A first cut at
3-in-15s was armed by the operator toggling split while testing, imposing the
6 s clearing delay on a radio that did not need it; a misreading file flips a
dozen times in that window untouched, so the two cases separate cleanly.
Distance (km) column added to Recent QSOs and Worked before (shared catalog).
Computed from the QSO's OWN my_grid/my_lat/lon first, falling back to the
current profile's locator: a log spans years and portable outings, so the
station a QSO was made from is not necessarily today's.
Locator: a precise QRZ/HamQTH grid is no longer overwritten by the cty.dat
entity centroid. The lookup runs several times per QSO and the provider gets
2 s; a slow second answer fell back to cty.dat and downgraded JN05JG to JN16
while name and QTH survived (they are only written when non-empty).
The OmniRig diagnostic line now logs what OpsLog concluded, not just what
OmniRig reported. icomnet.go: gofmt alignment only.
- SCP/N+1: new internal/scp downloads the community MASTER.SCP master list and a
docked two-column widget shows, as you type a call, the known calls containing it
(Partial) and the calls one edit away (N+1) — click to fix a busted call. Opt-in
in Settings → General; top-bar toggle; queried debounced on callsign input.
- Flex: OpsLog's GUI-client detection was too loose and could bind to "SmartSDR CAT"
(or DAX) — both carry "smartsdr" in the program name — instead of the real GUI
client, making SmartSDR CAT drop and reconnect in a loop while OpsLog was open.
Now it binds only to a real SmartSDR/Maestro GUI client (e.g. a FLEX-8600M's
integrated screen) and excludes cat/dax; dropped the risky empty-program fallback.
- Telemetry: on a fresh install the callsign isn't set at launch, so the once-a-day
heartbeat recorded the machine UUID. Now it waits (~10 min) for the operator to
enter their callsign before sending, falling back to the UUID only if none appears.
For an entity resolved only via cty.dat (e.g. a French call not on QRZ/HamQTH),
the provider block sets a coarse country-centroid grid. The worked-before backfill
now overrides that with the precise locator from the last QSO with this call, and
takes that QSO's lat/lon (derived from the grid when the record stored none) so
the map and saved record stay consistent. A real provider grid still wins.
Moved the (unreleased) worked-before backfill entry into a new 0.21.2 changelog
block, since 0.21.1 is already out.
When a callsign resolves only to cty.dat (not on QRZ/HamQTH, or no lookup service
configured) — or the lookup errors — enrich the entry from the most recent QSO
already in the log with that call. Fills ONLY the fields the provider left empty
and the operator hasn't edited (name, QTH, grid, country, address, state, county,
lat/lon, zones, continent, email, QSL-via), so a real QRZ/HamQTH hit is never
overridden. Uses the worked-before entries (qso_date DESC, [0] = latest) via a
live ref so it works regardless of the lookup/worked-before debounce ordering.
CSS `zoom` re-lays-out and pixel-rounds each element, which opened ~1px seams
between map tiles (a faint grid over the map when zoomed). Switch the persistent
UI zoom to `transform: scale(z)` from the app root's top-left, with the root
counter-sized to (100/z)vw × (100/z)vh so it reflows to fill and scales back to
the window. transform scales the subtree as one composited layer, so tiles stay
seamless. Removed the now-unneeded tile-overlap CSS hack.
Under our CSS zoom the map tiles scale by a fractional factor, opening ~1px gaps
between adjacent tiles that read as a faint grid over the map. Tag the document
with data-uizoom and, only while zoomed, enlarge each 256px tile by 1px so
neighbours overlap and cover the seams. The 100% view is untouched.
CSS `zoom` doesn't rescale viewport units, so the 100vh app root left an empty
strip below the UI when zoomed under 100%. Counter-size the app root to
(100/z)vw × (100/z)vh so that, after the zoom scales it back down, it exactly
fills the window again. Zoom still applies to the whole document so portalled
menus/modals scale with it.
- Zoom: our own Ctrl+wheel zoom (CSS zoom on the root, 50–250%), persisted in
localStorage and restored at startup; Ctrl+0 resets. Replaces the non-persistent
native WebView2 zoom. The freq-digit wheel now ignores Ctrl so it passes through
to zoom.
- Award column widths: they were stripped from AG Grid's saved column state (that
strip fixes a visibility desync) which also dropped their width. Now persisted
separately per award code (localStorage + portable DB copy) and re-applied on
rebuild/reopen.
- CW keyer widget: macros padded to 9 (F1–F9 slot) and empty macros hidden like
the voice keyer, with the F-number kept tied to the real macro index so the
F-key shortcuts still line up. New QRZ? default for F9.
They took vertical space in the CW Keyer panel for text operators already know.
Kept the actionable CAT-backend mismatch warnings and the checkbox labels.
With ESM enabled (Settings → CW Keyer), the keyer active and mode CW, Enter in
the entry strip fires a macro by QSO stage instead of logging:
- callsign field empty → F1 (CQ)
- callsign entered (in call field) → F2 (report), then focus jumps to RST-sent
- Enter in RST-sent / RST-rcvd → F3 (TU), which logs via its own <LOGQSO>
Works for every keyer engine (WinKeyer / serial DTR-RTS / Flex CWX / Icom CI-V)
since it routes through the shared macro sender. Enter in other fields still logs
normally, and ESM off keeps the classic Enter-to-log behaviour.
Backend: new `esm` flag on WinkeyerSettings (winkeyer.esm). Frontend: esmHandleEnter
state machine keyed off data-esm markers on the call/RST blocks, a Settings
checkbox + hint, and i18n EN/FR.
Rolling the mouse wheel over the hundreds / tens / units-of-kHz digit of the
header (and compact top-bar) frequency steps the frequency by 100 / 10 / 1 kHz
(wheel up = higher). The display updates optimistically on every notch and the
rig QSYs over CAT, debounced so a fast scroll doesn't flood the link. Only the
kHz digits are wheel-sensitive; MHz and Hz stay static.
New FreqWheelDisplay component (replaces the plain fmtFreqDots span in both the
full header and the compact top bar) + nudgeFreqHz handler.
The FlexRadio panel, AmpCard and TunerCard each carried their own copy of the
MeterBar (LED-bar) component. Even small drift between the copies made the tuner
meters look slightly off in height/LED size vs the others. Extracted one shared
components/MeterBar.tsx (segments, bar height, padding) and imported it in all
three, so every meter renders at exactly the same size. No behaviour change.
FlexRadio panel refinements from feedback:
- TRANSMIT and RECEIVE now share one collapse state — folding either folds both
(Card gained an optional controlled open/onToggle mode; the parent owns the
shared txrx state, persisted).
- Meter sizes: the Tuner Genius PWR/SWR meters used a 2-column grid (wider than
the Flex/amp meters). Switched to the same grid-cols-2 sm:grid-cols-3 so every
meter across the METERS, AMPLIFIER and TUNER cards is the same width.
- MIC and COMP meters are hidden outside phone modes (shown for SSB/AM/FM only).
- S-meter dBm moved inline next to the S-value (was a second line under the bar),
and the redundant dBm line under PWR removed — keeps only the watts. Saves height.
Addresses three points of feedback on the Tuner Genius work:
- Meter sizes: the amplifier meters were rendered `compact` (smaller than the
FlexRadio meters). Dropped compact so the amp, tuner and Flex meters are all
the same size.
- Collapsible cards: the FlexRadio panel Card, AmpCard and TunerCard now fold
from a chevron in the header, state persisted per card (opslog.cardOpen.*).
The amplifier cards share the "amplifier" collapse key across their SPE/ACOM/
PGXL variants so folding sticks regardless of the shown model.
- TGXL responsiveness: the tuner's device poll dropped 1500ms→400ms and the
three UI pollers 1500ms→500ms, so the SWR/power meters track TX without the
2–3s lag behind the amplifier the user saw.
- Icon: the Tuner Genius top-bar toggle used Zap, same as the CW keyer — changed
the tuner's icon (top bar + widget + card) to Gauge so the two are distinct.
Push the tuner control further so it mirrors the native 4O3A app and the way the
PowerGenius XL is surfaced.
Backend (internal/tunergenius):
- Status now carries both RF channels A and B (source/mode, band, frequency,
bound Flex nickname, per-channel bypass, antenna, PTT), the active channel,
the C1/L/C2 relay-network positions, and the 3-way-vs-SO2R hardware variant
(learned once from the `info` reply). Flat freq/antenna still mirror the active
channel for the compact widget.
- New TunerGeniusActivate(ch) binding → `activate ch=N` (or `ant=N` on 3-way).
Frontend:
- New shared TunerCard, styled exactly like AmpCard (PWR/SWR meter bars,
A/B channel selector with source+freq+antenna, Tune/Bypass/Operate). Used in
BOTH the FlexRadio panel (its own card, like the PGXL) and Station Control.
- Docked TunerGeniusPanel widget now shows the two channels A/B with their
source/frequency/antenna and lets you click to make one active — the missing
A/B state the user flagged.
- i18n EN/FR for the new labels (channels, antenna, in-line/bypassed, title).
Still UNTESTED on hardware — verify the per-channel field names/units and the
activate behaviour on the real box.
New internal/tunergenius client speaking the same "Genius Series" text API
as the Antenna Genius / PowerGenius XL: banner on connect (with optional
"AUTH" for remote access), "C<seq>|<cmd>\n" commands, "R<seq>|<code>|<msg>"
replies and the "S<seq>|status k=v …" snapshot; async "M|<msg>" info lines
are consumed inline. Polls status ~1.5s and exposes SWR (return-loss dB
converted to a VSWR ratio), forward power (dBm→W), and the operate/bypass/
tuning/active-channel state. Actions: autotune, global bypass, operate/standby.
Controlled directly (not via the radio) so OpsLog uses only one of the box's
four connection slots, per the device's protocol doc.
Wiring:
- app.go: tunergenius.* settings keys, Get/Save/start, GetTunerGeniusStatus,
TunerGeniusAutotune/SetBypass/SetOperate; started in the background at launch.
- Settings → Tuner Genius panel (enable + IP + optional remote code).
- Docked TunerGeniusPanel widget (SWR/power readouts + Tune/Bypass/Operate),
top-bar toggle, shown when enabled — mirrors the Antenna Genius widget.
- i18n EN/FR (sec.tunergenius, tg2.*, tgp.*).
Command verbs (operate/bypass/autotune/status/auth) come straight from the
4O3A "Tuner Genius XL — Protocol Description"; UNTESTED on hardware.
award_refs was only reset when the callsign was emptied, so swapping from one
call to another (e.g. clicking successive spots) kept the previous station's
references in the F3 Awards tab. Move the reset to just past the same-call guard
so any change clears them; the new call's spot POTA + live detection re-populate
right after. Changelog 0.21.1.
Note that entering address/city/state and per-band rig & antenna, then ticking
"Fill my station fields from my profile" on ADIF import, backfills the MY_* fields
automatically for a fully-described log.
In the QSO details Awards tab, the detected-refs line grew tall (DXCC/WAS/WAZ/
WAC/WPX/USA-CA… each with its full name), squishing the AwardRefSelector above so
you couldn't see the selected awards. Show just CODE@REF chips (full name on
hover), cap the block height with overflow scroll, and add a separator. Changelog 0.21.1.
Add a keyNav prop to BandMap: when set (only the docked Main-view map, not the
multi-band Band Map tab), Ctrl+ArrowUp / Ctrl+ArrowDown selects the next in-band
spot above / below the rig frequency and tunes to it via the existing spot-click
handler. Higher freq is up on the map (freqToY), so Up = next higher spot.
Ignored while typing in an input. Changelog 0.21.1.
flex-wrap with fixed-width cards started each new row below the TALLEST card of
the previous row, leaving big gaps under short panels (e.g. the amplifier alone
on a second line). Switch the dashboard to balanced CSS columns (column-width
430px, break-inside-avoid): cards flow top-to-bottom and pack tightly by height.
"Auto" fits as many columns as the window allows; 1-4 cap the column count via
the container max-width. Grip-drag reorder unchanged. Changelog 0.21.1.
GroupCard was defined inside ExportFieldsDialog, so it got a new component
identity every render and React remounted the whole grid on each sel change,
making the per-group All/None buttons (and checkboxes) feel unresponsive. Hoist
GroupCard to module scope with sel + handlers passed as props. Changelog 0.21.1.
The theme (and other per-profile UI prefs) are read via a.settings.Get, which is
scoped to the active profile. GetUIPref only guarded on a.settings==nil, so in
the startup window AFTER NewStore but BEFORE SetProfile(active) it resolved with
the wrong (empty) scope. The frontend theme self-heal treats a resolved "" as
"answered, unset → keep default, stop retrying", so a race landed on the light
default and stayed. Add a settingsScoped atomic flag set right after
SetProfile; GetUIPref/SetUIPref return "not ready" until then, so the frontend
keeps retrying and restores the real theme. Also prevents seeding prefs into the
wrong profile scope. Changelog entry added to 0.21.0 (EN+FR).