Network relay boards (WebSwitch/KMTronic/Dingtian) were always reached at
http://<host>, so a board on the LAN behind a reverse proxy — the common remote
setup where the operator's 80/443 already go to Nginx Proxy Manager — couldn't be
reached from outside. relayBase() now keeps a scheme the operator supplies
(https://relay.example.com, optional sub-path) and only defaults to http:// for a
bare host/host:port. UI hint + test added.
The "dim spots that represent nothing" feature drove the dimming with getRowStyle
and called api.redrawRows() on EVERY spotStatus update to re-apply it — a full
grid re-render on a firehose of status updates, which pegged the CPU on a busy
cluster (reported as PCs slowing down since 0.23.6). Move the dimming to a
defaultColDef cellClassRules ('opacity-40') that the existing light
refreshCells({force:true}) re-applies; drop redrawRows and getRowStyle.
The Ctrl+↑/↓ keyboard-nav useEffect sat AFTER `if (!range) return`, so on a band
with no range — e.g. a spurious 33 cm frequency briefly reported by an Icom over
CAT — the component returned before that hook and the hook count changed between
renders, crashing the whole window with React #300. Moved the effect above the
early return; lo/hi already fall back to [0,1] when there's no range.
A spot whose entity/band/mode is already worked, whose callsign isn't in the log,
and that carries no POTA/county/prefix novelty is dimmed (whole row, opacity) so
the eye skips it. "Nothing" follows the status, which already obeys the "same
slot" option and digital grouping. getRowStyle drives it; the spot-status effect
now redrawRows so the dimming re-applies when a status lands.
New DX Cluster setting (Settings → DX Cluster). Off (today's behaviour) a call
worked anywhere reads as already worked; on, the WORKED-call flag needs the same
band AND mode. It folds through the digital-mode grouping (Settings → General):
grouped, a 20m FT8 contact also marks a 20m FT4 spot worked; ungrouped they are
separate slots. Backend: qso.WorkedCallSlotKeys builds CALL|band and
CALL|band|mode(grouped) keys; ClusterSpotStatuses uses them when the option is on
(a spot with no inferable mode falls back to same-band).
Five operator-requested items:
- Alt+W clears the QSO entry. Handled before the `typing` guard and above
the keyer's key routing, so there is always one key that clears whatever
else is running — Esc is not that key when the CW keyer reserves it.
- The Grid box no longer pops outside the entry panel on a narrow window.
Row 2 needed 300+130+76+gaps = 538 px inside a panel whose min-width is
520, so Grid was pushed out and clipped at every narrow width, not just
extreme ones. QTH's min-width drops to 80 and the row wraps rather than
overflowing if it ever still can't fit.
- Selecting a QSO in the log now drives the Stats (F1) matrix. Uses its own
WorkedBefore call into separate state, NOT runWorkedBefore: that one owns
the entry form's wbRef and can trigger a field backfill, which browsing
the log must never do. The entry form wins whenever it holds a call.
- Clicking a coloured band/mode square lists the contacts behind it. Returns
the exact callsign AND the rest of the entity, because that pair is what
the cell's colour encodes; the call's own QSOs are bolded. The DXCC arm
matches the stored dxcc column only — reconstructing it from the callsign
here would disagree with the matrix above, which is built from that column.
- The Awards DXCC list shows each entity's primary prefix in its own sortable
column. Derived live from cty.dat into Ref.Group rather than stored on the
reference row, so an existing installation needs no re-seed.
Removed the long explanatory hints under the Kenwood COM/baud/host, data-mode,
CAT-trace and PTT-hotkey fields; shortened the CAT-sharing hint to just the
client setup line; and replaced the DTR/RTS hint with a one-liner. Less clutter
in the CAT settings.
1 MB (~6500 lines) fetched, split and re-rendered every second made the window
lag. 512 KB (~3200 lines) keeps a useful backlog — double the original 256 KB —
while staying responsive.
The viewer fetched only the last 256 KB (~1600 lines) of opslog.log. During a
busy trace the oldest lines scrolled out of the window while the operator was
still reading them. Raise the tail to 1 MB (~6500 lines); the backend already
allows up to 4 MB.
The LoTW/QRZ confirmation download ran on the app-lifetime context, so it
kept going after the QSL Manager was closed and a new download didn't stop the
previous one. A slow QRZ sync therefore bled its "flag cleared" log into a
freshly started LoTW download — the operator saw QRZ activity during a LoTW
run. Now each download gets a cancellable context: starting one cancels the
previous, closing the window cancels it (CancelConfirmations), and the parse
loops bail promptly on cancel. (A LoTW http 503 is separate — ARRL's server.)
No single Kenwood mode digit fits every rig for a data mode: a K3/K4 wants
DATA (MD6), a TS-590SG/TS-990S data mode is a USB modifier set on the rig,
and MD6 on a plain Kenwood is FSK/RTTY. So the operator now chooses in
Settings → CAT: USB (default), DATA (MD6), or leave the rig's mode unchanged.
Only soundcard/data modes (FT8, PSK, JT…) follow this; RTTY/FSK stays its own
native mode, and voice/CW are untouched (isKenwoodDataMode + test).
- PGXL: remote AUTH password; direct-link meter fallback (VITA-first) with
250 ms poll + peak-hold; fan mode uses bare "fanmode=" (was ignored).
- TCI: spot colour sent as decimal ARGB (ExpertSDR dropped the hex string);
spot click handled via CLICKED_ON_SPOT / RX_CLICKED_ON_SPOT to fill the entry.
- FlexRadio: clicking an OpsLog spot on the panadapter now applies the mode too.
- Filter presets: the trash icon deletes again (Radix pointer-down intercept).
- PTT hotkey: keys over CAT when a backend is active (was hijacked by a stale
Audio-tab RTS/DTR serial setting); AltGr keys allowed; presses logged.
- CW macro <LOGQSO>: logs after the preceding CW is sent, not on the first letter.
Settings -> Rotator is now a list like the amplifiers: add/remove rotors,
mix PstRotator / Rotator Genius / ARCO, and a Rotator Genius can drive both
its ports (one entry = two rotors). The compass gains a rotor selector, and
a per-rotor motorized-antenna flag so the boom/pattern paths show only for
the rotor carrying the Ultrabeam/SteppIR.
Also in this batch: a keyboard PTT hotkey (hold-to-talk or toggle, reusing
the audio PTT method with a CAT fallback), and TCI spot push to the
panadapter with click-to-fill of the callsign.
The 0.22.7/0.22.8 breakage came from a Xiegu-reported fault being fixed
inside the Yaesu and Kenwood backends. The lesson is structural: nothing
a backend does may be steered by another backend's report or another
backend's setting.
The lower-lines checkbox introduced for 0.22.9 was still one shared key
applied to both Yaesu and Kenwood — the same reflex in miniature. It is
now cat.yaesu.low_dtr_rts and cat.kenwood.low_dtr_rts, each checkbox in
its backend's own settings block, each wired only to its backend. The
shared key never shipped, so nothing needs migrating.
The rig-panel sliders take a non-passive wheel listener and acted on
hover, so scrolling the panel fed the gesture to whichever control sat
under the cursor — RF power included, mid-QSO. An operator scrolling to
read the bottom of the panel could change his transmit power without
touching anything.
They now require focus, i.e. a deliberate click on that slider first.
Without it the event is left alone and the panel scrolls, which is what
the operator meant. Same fix on the Flex and Yaesu panels, which share
the pattern.
This is a fault in its own right. It is NOT an explanation for the
"transmit power moves since 0.22.8" report: that release changed nothing
in the Icom path, and this behaviour is older than it.
Both defaults break someone. 0.22.7 lowered the lines on Yaesu and
Kenwood and silenced a TS-990 whose interface needs RTS raised to
transmit; 0.22.8 stopped lowering them and an FT8 station reported its
output power wandering, cured by closing OpsLog — Windows raises both on
open and the interface reads one as PTT.
So it is now a checkbox on those two backends, off by default: that is
how they behaved before any of this. Xiegu keeps lowering them
unconditionally (its own report, its own explicit keying setting), and
the CI-V backend keeps the behaviour it has always had.
Selecting CW showed FT8 and SSB contacts. The filter ran client-side over
the reference list and nothing else, so a reference kept because it had
one CW contact still displayed the band cells it earned on SSB, still
counted its SSB confirmations, and opening it listed every contact
regardless of mode.
The class now narrows the log BEFORE the engine runs, so the bands, the
totals and the confirmations all describe the selected mode, and
AwardCellQSOs takes the same class. The panel cache is keyed by
"CODE|MODECLASS" — one key per award showed the previous mode's numbers
after switching.
The snapshot is cached by log revision, so this costs a matching pass and
no database read.
The cluster already answers this question for a spot; the entry panel did
not answer it for the station actually being worked. A county hunter
needs to know before the QSO ends — by the time the county shows up in
the awards table the station is long gone.
qso.CountyWorked asks about one county instead of loading the whole
worked set, narrowed to the state so it stays a few dozen rows on a
remote MySQL. It compares through award.USCountyKey on both sides rather
than in SQL, because logged spellings vary ("Los Angeles" against
"LOS ANGELES, CA") and that key function is what resolves it everywhere
else.
An empty or non-US county is not "new" but unknown, and shows nothing: a
badge on a guess is worse than no badge.
Radix gives the content element focus when a tab is selected, so the
focus-visible ring fired on an ordinary click and outlined the whole
lower pane — visible as an orange rule under the tab strip. A tab panel
is a container; the controls inside it carry their own focus styling.
Six equal columns gave a US county name the same width as a two-digit CQ
zone: the county truncated while the zones, the prefix and the DXCC
number sat mostly empty. The row is twelve columns now, spans set per
field.
The bearing and the two distances are read-only and never exceed
"12345 km", so they move to a flex row at a fixed width and the address —
the one field here that was never wide enough — takes the remainder.
The award editor's Test tab matched DK2FJ on its OR rule "qth / exact"
and reported the two Aachen DOKs as ambiguous. The F3 panel, on the same
contact, showed nothing — it builds its own QSO for ComputeQSOAwardRefs
and that object carried address, state, county, zones and continent only.
Any award keyed on the town could not fire there, so live detection was
silently blind to a whole class of awards and the operator only saw them
after logging.
It now passes the entry-strip text as well: QTH, name, country, comment,
note and grid.
The refusal to guess was only visible in the award editor's test tab and
in the awards table — both of them places an operator goes long after the
contact is over. It has to appear while the station is still in front of
you, so ComputeQSOAwardRefs now reports the candidates it declined to
assign and the F3 panel lists them as buttons: click one and it is
written to award_refs, replacing any sibling picked for that award.
They are never auto-added — adding one would be exactly the guess the
setting exists to refuse.
Also: the dark-theme fix for the native calendar glyph named only
input[type=date], so date-time fields kept an invisible icon. All the
temporal input types now share the rule.
Two DARC DOKs are both called "Gießen" — two clubs share the town. Exact
matching finds both and both are right as far as the matcher can tell, so
it kept both and the operator got a QSO credited to a DOK that may well
have been the other one.
Picking one is not an option: it writes a reference into the log with no
evidence behind it. So a Def can now be marked one reference per QSO, and
an ambiguous match on such an award assigns none, names the candidates in
the trace and leaves the contact under missing references — where the
operator's choice now sticks (06e3437).
Off by default, and deliberately NOT a revival of the old `Multi` flag: a
field holding several references (an n-fer POTA activation, a VUCC grid
line) is several references, correctly, and stays that way.
Reported on a Xiegu G90. With the blue programming cable — three wires, no
modem lines — CAT works perfectly. Behind a DE-19, which carries audio, CAT and
PTT, the radio went into transmit the moment OpsLog connected and stayed there.
Windows raises DTR and RTS when a serial port is opened, and the DE-19 reads
them as PTT; Xiegu's own documentation asks for both low. The CI-V backend has
dropped them since it was written, for the same reason on Icom rigs with USB
SEND mapped to a line. Xiegu, Yaesu and Kenwood did not. They do now.
The second half of the report: WSJT-X through OpsLog's rigctld decoded fine and
never transmitted. Nothing was broken in that chain — set_ptt reaches the
backend, which sends the CI-V PTT command, which a G90 ignores. That is why
Xiegu keys on a hardware line instead. The backend can now do that: Settings →
CAT → Xiegu → how the rig is keyed (CI-V / RTS / DTR).
Untested here — no G90 in reach. The operator who reported it offered to try.
A panadapter shot settles it: answering an S5 station, the replayed recording
goes out several times wider and taller than the station being answered — in WAV
as well as MP3, so this is not the encoder.
One setting served two sources that have nothing in common. A voice-keyer
message is a microphone at speaking distance and legitimately wants 195%; a QSO
recording is a receiver's line output, which is far hotter. The same 195% put it
into the transmitter flat out, ALC pinned, noise and voice alike.
The QSO playback now has its own level, defaulting to 100%. The voice keyer
keeps the setting it had, so nobody's F-key messages change.
Reported again after the monitor was wired up: 300% still sounded like 10%. The
monitor fix was right but could not show, because the sliders only changed the
settings panel's own state — the value reached the audio engine when the
operator clicked Save.
A level is set by ear, by dragging. It now applies on every move, to the monitor
and to the recorder mix alike; saving still persists it. Both halves were needed
and neither works alone.
All / CW / Phone / Digital, stacked on top of the worked/confirmed filter. The
question an operator actually has — which DXCC entities have I worked on CW but
not confirmed on CW — needs both at once; answering only one of them is what
sends people to a spreadsheet.
References carried bands but not modes, so the award computation now aggregates
mode CLASSES per reference, worked and confirmed separately. Classes, not ADIF
modes: nobody chasing CW cares whether the digital side was FT8 or RTTY, and a
list of twenty mode names would not answer the question. An unrecognised mode
counts as data rather than as nothing, so a new digital mode does not vanish
from the filter the day it appears.
Two rules the filter needs to be honest:
- Not-confirmed means not confirmed ON THIS MODE. An entity worked on CW and
confirmed on SSB is still a CW entity to chase, and that is exactly what the
filter is for.
- "Not worked" plus a mode is a contradiction — an unworked reference has no
mode — so the mode filter stands aside there instead of emptying the list.
Closes#9.
It was clamped against a GUESSED height — 230 px with the upload submenu, 110
without. The menu is far taller than that with a selection, so right-clicking on
a row near the bottom hid half the entries, delete among them.
It is measured now: placed above the cursor when it does not fit below, pinned
to the top with its own scrollbar when it fits neither way. Its height depends
on which actions the caller passes, so no constant could have stayed right.
The rule between the band-driven columns and the mode-driven ones was an extra
empty cell added to the body rows only. Seven cells per row against six in the
header slid every heading across: Phone stood over the TX antenna, CW over
Phone.
The rule is now drawn on the first power cell itself, in the header and the body
alike, so the two cannot drift apart again.
Antennas and power were two tables listing the same bands, one under the other,
so configuring a band meant matching rows by eye between them.
They are now one row per band. That the two settings are stored separately and
applied on different triggers — antennas on band change, power on band or mode
change — is a backend detail, and no reason to split what is a single decision
per band for the person configuring it. A rule in the header keeps the two
groups readable.
Settings → FlexRadio, beside the per-band antennas and applied the same way:
when the band or the mode changes. 1.5 kW that is fine on SSB has no business
going into an FT8 signal on the same band.
Three classes rather than one row per mode, because what decides the power is
duty cycle and what the amplifier will take of it, not the mode's name — FT8 and
RTTY punish an amplifier the same way, SSB and AM do not. The classification is
pinned by a test: a digital mode sorted as phone would deliver exactly the
1.5 kW this exists to prevent.
An empty box means "leave the power alone", never zero watts, and an unknown
mode changes nothing — setting a transmit power from a name we do not recognise
is not a good guess.
Unlike the antennas, a locked band does not skip it: the lock means "do not let
the rig drag my log entry around", not "let the amplifier keep whatever the last
mode left".
Settings → General, beside the language. An operator reading 2026-07-30 as the
7th of the 30th month is reading their own log wrongly, and that is a display
problem.
Storage stays ISO/ADIF, deliberately and permanently: it is what ADIF
specifies, it sorts correctly as text, and a stored format that followed a UI
preference would make the log unreadable the day the preference changed. Exports
and uploads are untouched.
Two details that decide whether it actually works:
- The columns are rebuilt when the format changes. The formatters are captured
inside AG-Grid's column definitions, so nothing else would notice and the
table would keep the old format until something forced a rebuild.
- main.tsx re-reads the choice after the portable prefs are pulled from the
database. The module reads localStorage at import time, which is BEFORE that
sync — so a copied data/ folder would have shown ISO until the next launch.
UTC is not part of the choice and never will be: a logbook is kept in UTC, and
showing local time would make the display disagree with the QSO's own record
twice a year.
Y green, N red, R blue — colour only, no pill or badge. These columns sit among
callsigns and dates, and a row of coloured boxes would shout louder than the
callsign it belongs to.
Twelve columns: paper QSL, LoTW, eQSL, Club Log, HRDLog, QRZ.com both ways, and
OpsLog's own card and recording flags. All four views the operator named —
recent QSOs, worked before, NET Control, QSL manager — turn out to share this
one grid, so they are all covered by construction rather than by four edits that
could drift.
Semantic tokens rather than fixed colours, so the four themes follow and the
contrast stays right on the light ones. ADIF's I (ignore) is deliberately left
in the default ink: it is neither good news nor bad, and colouring it would put
it in one camp or the other.
It applies to QRZ.com and Club Log both, but it was rendered after the tab
strip — so it read as a QRZ setting when the QRZ tab was open, appeared to
repeat itself on the Club Log tab, and was invisible to anyone opening a third
one.
Above the tabs it belongs to the panel, which is what it actually is.
Opt-in, off by default: neither service can undo it, and a local delete is not
always a statement about the world — a duplicate cleared out of a contest log
was never meant to change the DX's log.
The two APIs are not alike, and the difference decides what is possible:
- Club Log deletes by MATCH (callsign, exact timestamp, band number), so it
works for every QSO in the log, past or future. The band table is its own
list and is pinned by a test: a band mapped to the wrong number does not
fail, it points the delete at a different QSO.
- QRZ.com deletes ONLY by logid. So markExtUploaded now stores the LOGID it
already received on every upload — it was being logged and thrown away. A
QSO uploaded before this cannot be withdrawn from here at all, and the log
says so by name rather than leaving the operator to assume it worked.
Both run BEFORE the local delete, because both need the QSO's own fields to
find their copy and there is nothing left to ask with once the row is gone.
Neither can block it: the operator asked for the QSO to go, and a refusing
website is not a reason to keep it.
The sort choice is now written through writeUiPref and both keys are registered
as portable, so they travel with a copied data/ folder like every other UI
preference.
Registering them is the part that matters: writeUiPref stores to the DB, but
only keys on the portable list are read BACK at startup. A key written and never
restored looks exactly like one that was never saved — which is what would have
happened here.
The grey-line toggle had the same gap. It called writeUiPref from the day it was
written but was never added to the list, so it came back off on the next launch.
Found while adding the award keys beside it.
Reference stays the default: that is how award lists are published and how a
paper list is read. Description answers the other question an operator actually
has — "have I worked anything in Alicante" — which reference order scatters
across eight thousand rows.
Sorted where the filtered list is built, so the grid and list views inherit it
rather than each growing its own copy that could disagree.
localeCompare rather than a byte comparison, because these lists are Spanish,
French and Italian place names: "Ávila" belongs beside "Avilés", not after
"Zaragoza". References compare numerically for the same reason in reverse —
08019 must not sort between 0801 and 081.
The same ASCII stream over a socket instead of a wire: ser2net, an
Ethernet-serial adapter, a Raspberry Pi at the radio. One TCP transport
presented as a serial port, so the backend keeps a single code path.
Explicitly NOT the radio's own RJ45. A TS-890 or TS-990 speaks Kenwood's
KNS/ARCP there — session, authentication, a different protocol — and that needs
one of those radios in hand to write honestly. The setting's help text says so,
because an operator who plugs in their TS-890's Ethernet port and types its
address deserves to learn that from the interface rather than from silence.
Two details that decide whether this is usable or maddening:
- A read deadline expiring is reported as "no data yet", not as an error. It
is exactly what a serial read timeout means to the caller; as an error it
would make ask() abandon a rig that is merely thinking.
- The log line names what was connected to. "connected on @ 0 baud" after a
network connect sends someone hunting a serial fault that does not exist.
The host wins over the COM port when both are filled: it is the more deliberate
setting, and silently preferring the wire leaves someone staring at an address
they typed and a radio that never answers.
The map repeats the world sideways, and zoomed out several copies are on screen
at once. The rings span −180…+180 once, so the shading ended in a hard vertical
edge wherever the next copy began: it read as a rectangle laid over the planet
rather than as night.
Each ring is now drawn again shifted a full turn either way. Three copies cover
every zoom this map reaches and cost nothing — three more polygons on a canvas
layer.
Zooming the map to fit one world would have hidden it rather than fixed it, and
would have taken away the free pan and zoom the operator has.
Grey line — the day/night terminator and its twilight band, off by default,
redrawn every minute so a map left open all day is not silently wrong by
evening. Its own Leaflet pane below the overlays, non-interactive, so it never
hides the path or beam nor swallows a click.
The solar maths is pure and was checked against known positions before being
wired to anything: both solstices, the March equinox, the subsolar longitude at
12 UTC, and day/night at Paris, Tokyo, New York, Sydney and Reykjavik across
seasons and hemispheres. That last set is what caught the real bug: the equation
has TWO solutions per meridian and the naive branch put New York in daylight at
02:00 in December — the terminator at +63° where it belongs at −63°. A shaded
map that looks entirely plausible and is exactly mirrored. It now anchors on the
classical terminator and takes the branch nearest it, which is also what makes
the twilight band follow the terminator instead of jumping hemisphere.
Separately, from a Xiegu user: the protocol-trace checkbox does not stay ticked.
It is React state initialised to false on every mount, so a trace still running
came back unticked — the operator ticks it to "switch it on", which switches it
OFF, and the log they send contains no trace at all. Worse than a cosmetic bug:
it silently defeats the one tool asked for to diagnose their radio. Both boxes
(CAT and WinKeyer) now read their real state from the backend.
Reported from a Flex in Kenwood CAT mode: frequency read perfectly, split never
appeared. Not every rig speaking this dialect fills IF's split bit.
Rather than guess which column that firmware populates, ask the question that
DEFINES split — is the transmit VFO a different VFO from the receive one — which
is exactly what FR and FT answer, and the same rule the Yaesu backend settled on
after several wrong turns. FR is also trusted over IF for which VFO is in use:
they are asked in the same breath, and a rig vague about the split bit may be
just as vague about the VFO field.
Cost is bounded: a rig that rejects FR/FT answers "?;" once and is never asked
again, so this is two short commands per poll only where it works. Both paths
are tested against the emulator — split found through FR/FT with IF silent, and
IF-reported split still working on a rig that refuses FR/FT without re-asking.
Also extends the CAT wire trace to the Kenwood backend, ASCII quoted so an empty
reply is visible as such: "" and ";" look identical unquoted, and telling them
apart is the whole question when a rig half-supports a command. If this fix is
not the whole story on real hardware, the trace is what will say so.
They searched only the rows on screen. With the grid holding the most recent
page, filtering a column for RSGB-IOTA across a 28 000-QSO log returned nothing
— which reads as "the log does not contain it", not "it is not on this page".
That is how an import that had worked perfectly came to be reported as broken:
the tool answered a narrower question than the one being asked, and said so
only by being empty.
The advanced filter queries the whole logbook and is the honest instrument. The
headers also get back the width the filter menu icon was taking, which is the
smaller reason but a welcome one on a grid this dense.
No saved filter can be stranded by this: the grid persists widths, sort and
visibility, never a filter model.
Two white-screen reports are open — one after logging a 10 GHz QSO, one on
starting a DVK auto-call — and neither could be investigated, for the same
reason: a render error emptied the window and left NOTHING. No line in
opslog.log, no dialog, nothing the operator could send but the words "white
screen". A fault the user cannot report is a fault that cannot be fixed.
So before hunting either trigger, the reporting path:
- An error boundary catches a render throw, writes the error and component
stack to the app log through the new LogUIError binding, and shows it on
screen — selectable, with Copy and Reload.
- Global handlers catch what a boundary cannot: throws from timers, event
handlers and rejected promises. An auto-call loop lives entirely in
callbacks, which is exactly where the second report came from, and those
leave no trace at all today.
It deliberately does not try to resume: React cannot promise sane state after a
render throw, and a half-working logger would be worse than a clear stop.
This does not fix either crash. It makes the next occurrence arrive with its
cause attached, which is the step that has been missing.
Reported on an IC-9100: the MOX button does not transmit, it sets split. The
source sends the PTT command (0x1C 0x00) and nowhere sends the split one (0x0F),
so the discrepancy is between what OpsLog sends and what the rig acts on — and
that link has already been shown, in the same operator's log, to lose frame sync.
Rather than guess from the command table, this adds what settled the WinKeyer
bug in one line: the actual bytes. Settings → CAT → Log the CI-V protocol writes
every frame in both directions as hex. RX is traced BEFORE framing, since the
bytes as they arrived are what reveals a lost boundary — a decoded view would
hide exactly the fault being hunted.
Session-only, like the keyer trace: it is a diagnostic, and a log full of hex
helps nobody who forgot it was on.
I am not claiming a cause yet. The last time I inferred one from a protocol
document rather than from evidence, I inverted a digit and broke the case that
worked.