Third empty recording, same station, and the sequence was as plain as it gets:
type the call, click record, log the QSO. The device opened and never streamed.
Endpoint ids are what gets stored, and a DAX channel that is reconfigured,
disabled or recreated comes back with a DIFFERENT id. The stale one opens
without complaint on some drivers and simply never delivers — indistinguishable
from a quiet band until the file turns out to be empty.
The recorder now checks the id against the current endpoint list when it starts,
which takes milliseconds, and says outright that the configured input is gone —
on screen, not only in the log. If it IS still there, the answer is equally
useful: the device exists and the fault is upstream, in whatever should be
feeding it.
The watchdog fired exactly as intended and said:
no audio at all from "{0.0.1.00000000}.{6a27abfd-f277-496b-b6f1-f92ec284c636}"
which tells the operator nothing they can act on. "DAX RX 1 (FlexRadio DAX)"
points straight at the DAX panel.
Endpoint ids are what gets CONFIGURED and stored, so they are what the recorder
holds; the friendly name is resolved only when something has gone wrong. When
the endpoint cannot be found at all the id is still shown — a device that has
disappeared explains an empty recording too.
"No recording was running at log time" says the take had already ended, but not
who ended it. The only path that does is the callsign being cleared — which also
happens when a clicked spot replaces it — and that path was silent.
Now it names itself, so the log shows the moment the recording was lost instead
of only its absence at the end.
Second empty recording from the same station, and this time NEITHER source had
delivered a sample — so the fix for a silent second source could not apply, and
the log still said only "recording was empty" at the end of the QSO.
Two blind spots, both now closed:
- captureStream's error was discarded. A device that cannot be opened — renamed,
unplugged, held by another program — looked exactly like one that is merely
quiet.
- A WASAPI device can open cleanly and then produce nothing at all. A DAX
channel with no stream behind it does precisely that, and it is
indistinguishable from silence until the file turns out to be empty.
A watchdog now reports both, three seconds in, and the first goes to the OPERATOR
as a toast rather than only to the log: they are mid-QSO, and by the time they
find out at save time the audio is gone for good.
From an operator's log: a Flex with From Radio on DAX RX and Recording mic on
DAX Mic, a full CW QSO recorded, and at log time "snapshot failed: recording
was empty".
The mixer took min(len(A), len(B)) samples. In CW the mic path does not run, so
DAX Mic delivered nothing, min stayed 0, and the drift guard then threw the
radio audio away a second at a time. Two devices configured meant the recording
depended on the quieter one being alive.
A source that has delivered nothing for 1.5 s is now treated as absent and the
other is recorded alone, with one line in the log saying so — half a recording
is worth having, silence is not. Both are mixed again the moment the missing one
comes back.
Neither is written off before it has EVER spoken: at startup both timestamps are
zero, and declaring one dead then would clip the opening of every recording.
That case is tested too.
In CW the transmitter builds its tone from the keyer and ignores the DAX TX
audio path, so the button keyed the rig and sent silence. Confirmed on a Flex:
our side reported the playback as done while nothing went out.
Only the button goes. The row stays, or a CW operator who stopped their
recording would be left with no way to resume it — and the recording itself is
still worth keeping.
"I click play and nothing happens" is what an operator sees when a function
returns an error into a promise the interface swallows. Every refusal in the
playback path is now logged as well as returned, and each names its own cause —
audio not initialised, the take still running, the take empty, the device
rejecting the file.
One of those was not even checked: playing back needs an OUTPUT device INTO the
radio, which is a different setting from the recorder's input and is very likely
unset on a station that has only ever recorded. It now refuses with that
sentence instead of failing somewhere inside the player.
CW has moved in and out of this list twice — excluded for the missing sidetone,
restored because the other station is captured regardless. A test says which,
so the next reading of that comment does not undo the decision.
CW was excluded because SmartSDR does not route the operator's own sidetone
through DAX, so the recording sounds one-sided. The audio path is open all the
same and the other station IS captured — which is the half worth keeping.
The frontend had ONE list serving two purposes: which modes may be recorded, and
which modes the voice keyer may transmit on. Adding CW to it would have let the
DVK key the rig with speech on a CW frequency. They are now two lists: PHONE_MODES
for the keyer, RECORDABLE_MODES (phone + CW) for the recorder.
Digital modes stay out of both: their audio is a modem tone nobody will replay.
SmartSDR moves its active flag onto the TX slice as soon as the transmit
frequency is touched, and OpsLog followed it. But the operator is listening to
the DX on the other slice: the S-meter, audio level, filter and DSP they are
adjusting all belong there, so following the transmitter handed them the
controls of a receiver they were not using.
In split the receive slice now wins. Simplex is unchanged, and two same-band
slices in different classes (SSB alongside FT8) are still not a split.
A slice clicked IN OPSLOG is remembered and overrides both the split rule and
the radio's focus — that is the operator speaking. Activating a slice on the
radio or in SmartSDR deliberately does not move OpsLog. The pin is dropped when
that slice stops being in use, so a closed receiver cannot strand the display.
The panel now highlights the slice OpsLog is working with rather than the radio's
focused one; reporting the radio's flag would light up one slice while every
control acted on another.
The 0.22.3 block had grown into explanations: why a fault happened, how it was
found, what it does not cover. That belongs in the commits, where it already is.
An operator opens this list to learn what changed for them. Sixteen entries now
say it in a line each.
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.
Says what the switch does, then names the one limit that changes the decision.
Drops my "no undo" line: deleting a QSO is already understood to be permanent,
and repeating it reads as a warning about the checkbox rather than the QSO.
Four sentences of API behaviour under a checkbox. Two facts actually change the
operator's decision: it cannot be undone, and QRZ.com cannot reach QSOs uploaded
before OpsLog started keeping their record number.
The rest — how Club Log matches, that the local delete happens regardless, that
the log records each answer — is true, and belongs in the log and the code where
it already is. Documentation in a checkbox hint is documentation nobody reads.
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.
It was the same red as the recording indicator beside it, so at that size the
two read as one control. Green is also what the theme already uses for a
go-ahead action (the amplifier ON buttons), and it is a semantic token, so it
follows all four themes rather than pinning a colour.
Two silent returns in the save path: no take was running at log time, or the
mode carries no audio worth keeping. Both simply produced no file.
An operator reporting "the sound is not in the folder" is describing an absence,
which is the same absence in either case — but they are different problems with
different fixes, and neither leaves anything behind to inspect. Now each says
which one happened, and for which callsign.
The success line already existed ("qso-rec: saved <path>"), so between the three
the log now accounts for every logged QSO.
Two absolutely-positioned boxes at guessed offsets: right-14 and right-2. They
collided as soon as the counter passed a minute and the text grew — a layout
that only looked right for the first 60 seconds, and did not even manage that at
this font size.
One flex row with a gap, so spacing is a consequence of the content rather than
an arithmetic guess about it. The callsign field also gains right padding while
the badges are shown, so a long call runs under them instead of being typed over.
The situation, in the operator's words: working VP2MAA, recording, and he asks
to hear his own signal. So: stop, play, and the recording is still saved with
the QSO.
Stopping FREEZES the take rather than ending it — nothing more is captured,
nothing is discarded, and logging writes the file exactly as before. Playback
reuses the voice keyer's path: same output device, same PTT keying and release
with its generation guard, so a replay cannot be cut short by a stale unkey.
Playing is refused while still recording. The transmitted audio would feed back
into the same take on any rig monitoring its own transmission, and stopping is
one click the operator has already made — it is their statement that the take is
finished, not a hoop.
The elapsed clock freezes with the recording and resumes from where it stopped,
because it now shows the LENGTH OF THE FILE rather than the time since the QSO
began. Only a fresh take returns it to zero.
A red dot in the slot the counter will occupy. Click it and the counter starts,
the dot disappears, and logging the QSO writes the file exactly as an automatic
recording would — the save path already keyed off an active take, not off the
setting.
With automatic recording disabled the capture engine is not idle, it is not
running at all, so a manual take starts it. Two consequences handled here:
- It is released again when the take ends — logged, cancelled, or dropped for
being a digital mode. An operator who records one contact does not expect
their microphone held open for the rest of the session. The release is a
deferred call at the top of the save path so every exit goes through it,
rather than one that has to be remembered at each return.
- There is no pre-roll. An automatic recording opens with the seconds BEFORE
the callsign was typed; a manual one can only begin now. The button's
tooltip says so, because otherwise the difference between two files is a
mystery to the person holding them.
The button only appears where it can work: devices configured, automatic
recording off, and a mode whose audio is worth keeping. That state is asked of
the backend rather than inferred from a preference in the interface, so the two
cannot drift apart, and it is re-read when settings are saved.
The list read "Yaesu (native CAT)", "Kenwood (native)", "FlexRadio / SmartSDR
(native)". Native says how OpsLog is built, which is our business; the operator
opening that menu is answering a different question — how is my radio plugged
in.
So: OmniRig, FlexRadio (API), Yaesu (USB), Kenwood (USB, network), Xiegu (USB),
Icom (CI-V USB), Icom (CI-V network), TCI. Shorter, and each entry now tells
someone with a cable in their hand which line to pick.
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.
From a Kenwood trace: IF answered mode digit 1 on 7.160 MHz, the backend read
LSB correctly, and the entry form showed no mode at all while the frequency
tracked perfectly.
Nothing was wrong in the backend. Radios report the SIDEBAND, and the station's
mode list holds SSB — so the value pushed into the selector was not in its own
list of options and nothing could match. Yaesu and Icom report the same way, so
this was never Kenwood-specific; it only surfaced there.
Normalising at the point where a rig-reported mode enters the form fixes every
backend at once, rather than editing three mode tables and the next backend
someone writes.
It is also the ADIF-correct direction, which matters more than the selector:
LSB and USB are SUBMODEs in ADIF, not modes. MODE=LSB is not a valid value and
is what would have gone to LoTW and eQSL.
An operator who has deliberately put LSB or USB in their own mode list keeps
them — the list is their statement of what they log, not ours.
The grey line has never been released, so its off-by-one-copy edge was never
something an operator saw. An entry for it would describe a defect that only
existed between two commits of the same unreleased feature — noise in the middle
of the list someone reads to find out what changed for them.
Same case for the bulk-update progress bar: the feature and the fix that made it
non-blocking are both new in 0.22.3, so they are now one entry describing what
it does. The menu rename goes there too rather than standing alone.
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.
Two faults in the progress bar shipped an hour ago.
It was a modal. A hundred QSOs is a minute; a contest log is two thousand and
ten minutes, and for all of it the operator could do nothing else — while their
radio is on. It is now a card in the corner: same bar, same count, same
callsign, none of the imprisonment.
And the menu said "Update from QRZ.com" while the code has always walked EVERY
configured provider, falling through to HamQTH when QRZ answers not-found. The
label is now "Update from the callsign databases", which is what happens.
Noted while reading that code, NOT fixed here because it trades against the
user's API quota and is their call: this path goes through the lookup cache, so
a second run on the same callsigns re-reads the cache rather than reaching the
provider. The cache comment already argues the opposite for deliberate clicks —
"a lookup the operator asked for by clicking is a deliberate act and must reach
the provider" — and a right-click on a selection is exactly that.
Selecting the 102 contacts of a freshly imported contest log and updating them
from QRZ.com is 102 network round trips. Nothing moved on screen while it ran,
so the only honest reading was that the application had frozen — and the natural
response to that is to kill it, halfway through.
The backend now emits a progress event per QSO and the frontend shows the same
overlay the ADIF import uses: a bar, the count, and the callsign being queried.
The callsign is reported BEFORE the lookup rather than after. With a slow
provider that is the difference between "waiting on F4BPO" and a name that lags
one QSO behind whatever is actually taking the time — which would be the field
someone stares at while deciding whether it is stuck.
The overlay is cleared by the closing event and again in a finally, so neither
a completed run nor a failed one can leave it on screen.
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.
Nobody here owns a Kenwood, and a backend that has never completed a single
exchange is a guess however carefully it was written.
But this repository already contains the other half of the conversation:
internal/catemu ANSWERS this dialect, pretending to be a TS-2000 so an ACOM
amplifier follows OpsLog. The test's responder replies exactly as catemu does —
same FA/FB widths, same 38-character IF layout, same ID019 — so the two halves
are checked against each other instead of against my reading of a manual.
Connect adds a dialPort seam for this; nothing else changed in the backend.
Covered: model identification, simplex on A, everything following the VFO in
use when on B, tuning writing to that VFO rather than blindly to FA, split with
TX and RX the right way round (ADIF FREQ is the TRANSMIT frequency — swapping
them writes the wrong frequency into every logged QSO), the mode digit, and a
silent port reported as not answering rather than as a connected radio. The last
three are each a fault that actually reached a user through the Yaesu backend.
What this does NOT prove: that a real TS-590 answers on the same timings, or
fills every IF field the way its documentation says. That still needs a radio.
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.
CONTEST_ID was reported as not imported. It is imported: this drives an
operator's own IOTA-contest record through a real SQLite logbook and reads the
row back, confirming contest_id=RSGB-IOTA and iota=EU005 in the database.
The conversion-level test was already green while the report stood, which is
exactly why it was not enough — a promoted field can be dropped at any of five
places between the file and the grid, and only the round trip says which. The
answer here turned out to be none of them: the Contest columns are simply hidden
by default in the QSO grid.
An operator worked the RSGB IOTA contest in another logger and got back records
carrying <STATE:5>EU005: N1MM-class software stores the received exchange in the
column its contest module uses, not the one ADIF reserves. Imported verbatim,
every QSO gains a US-state field reading "EU005" and the IOTA award sees
nothing — the reference is in the log, just not where anything looks for it.
The import dialog gains optional source → destination rows, prefilled with
STATE → IOTA since that is the case that prompted it. Applied to the RAW record
before conversion, so it works for promoted columns and Extras alike and the
destination is parsed exactly as if the file had carried it there.
Two rules, both from the same principle that the file outranks our guess:
- the source is CLEARED, so a reference does not linger in STATE where it
would show as the contacted station's US state in the grid and every export;
- a destination the file already filled is kept, judged against the record as
it ARRIVED — otherwise the result would depend on Go's map iteration order,
which is deliberately random.
The swap case in the test is what forced that second rule to be stated: "never
overwrite" and "exchange two fields" cannot both hold silently, so a destination
that is itself a mapping source is treated as an explicit swap and nothing else
is overwritten.
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.
From F4JND's log: a rig sitting on 145.550 MHz FM produced status lines reading
16445550000, 8364550000, 5817408364 and 12640 Hz between good readings — the last
of those even resolved to a 6 cm band.
BCDToFreq treated the nibbles 0x0A..0x0F as the digits 10..15, so a byte stream
that had lost frame sync still decoded into a number. The same log shows why sync
was lost: PTT round trips timing out and a scope stream the rig kept rejecting
(0x27), both competing with the poll on one serial line.
Non-decimal nibbles are now refused. Each caller keeps its last good value rather
than publishing noise: the frequency reader returns an error, the sub-VFO reader
returns not-ok, and the scope keeps its previous edges. Garbage that LOOKS like a
frequency is worse than a refused frame — it reaches the display, the band slots
and eventually the log, and it is the operator who then has to disprove it.
The test builds its fixture with FreqToBCD rather than by hand, because my
hand-written one was byte-reversed and the test caught it.
This does NOT explain the white screen that was also reported, and I have not
claimed it does: it removes one source of absurd values reaching the UI, which is
worth doing on its own evidence.
The 0.22.1 tag was cut at d98bb2e, and six commits landed after it. Five of them
had appended their entries to the 0.22.1 block, so the released changelog
promised things the released binary does not contain: the 4O3A marks, the
status-bar logbook path, the Yaesu per-VFO mode, ERC rotator support and the
Kenwood backend.
Those five move to a new 0.22.2 block, in the order they were written. The
0.22.1 block is restored to exactly the twelve entries the published tag holds —
verified against `git show v0.22.1:changelog.json` rather than by eye.
Worth noting for next time: entries are appended as work lands, so any commit
after the release tag needs its own block opened first. The mistake is silent —
nothing fails, the What's New dialog simply describes a version that never
shipped those changes.
A sixth CAT backend, talking to a TS-590/890/990/2000 over its serial port with
no OmniRig in between — frequency, mode, VFO, split and PTT. Elecraft K3/K4 and
the "Kenwood" setting on other radios speak the same dialect.
The protocol was not researched from scratch: internal/catemu already ANSWERS
these commands, pretending to be a TS-2000 so an ACOM amplifier follows OpsLog.
The IF frame layout here is read from the same format string catemu emits, so
the two halves of the repository agree by construction — and the test caught my
own fixture being one character short, which is exactly the error that layout
invites.
Design notes worth keeping:
- IF; is the poll. One frame carries frequency, TX state, mode, VFO and split,
so simplex operation costs a single round trip; the other VFO is only asked
for when split is actually on.
- FA/FB take ELEVEN digits here where Yaesu uses nine. That is the likeliest
place to copy the Yaesu backend and be wrong by a factor of a hundred, so it
has its own test.
- Every lesson the Yaesu backend learned the hard way is built in from the
start: replies matched to the command that asked, "?;" remembered so a poll
stops paying a timeout for an unsupported command, the serial handle closed
before reopening, and a rig that answers nothing reported as absent rather
than "connected".
Untested on hardware — I have no Kenwood here. The frame parsing is covered by
tests against catemu's own format.
An ERC (Easy Rotor Control, incl. ERC Mini) emulates Yaesu GS-232A/B, which is
exactly what the backend written for the microHAM ARCO already speaks — azimuth
out (Maaa), azimuth in (C), stop (S), which is the whole of what was asked for.
So this is two small gaps rather than a new backend:
- Serial speed was hardcoded to 9600. An ARCO's virtual COM ignores it, but an
ERC runs at whatever its own configuration sets, and a mismatch reads as a
dead rotator. It is now selectable beside the COM port.
- The entry was called "microHAM ARCO", so an ERC owner would never have found
it. It is named after the PROTOCOL now: "GS-232A controller (microHAM ARCO,
ERC…)".
The help text states the condition plainly in both languages, because it is the
one thing that will otherwise waste an evening: an ERC left on Hy-Gain DCU-1
speaks a different command set and simply will not answer.
Untested on hardware — I have no ERC here, and the GS-232 path itself is proven
on a real ARCO.
Reported, and reasoned out from the half that already worked: a spot click now
tunes the right VFO, but it set the mode with MD0 — main — whatever VFO the
operator was on. Working from SUB, the spot changed the mode of the VFO NOT in
use and left the one in use as it was. The author inferred the sub half from
seeing main change, and was right.
The read had the same fault, so the console displayed main's mode while showing
the sub VFO's frequency.
Mode now addresses the receiver in use — MD0 for main, MD1 for sub — in the poll
read, in SetMode, and in the console's raw-mode setter. A rig without a sub
receiver never sees MD1: curVFO only becomes B where the rig reports its receive
VFO in the first place.
Spotted by the author: on SQLite the database chip showed a.dbPath — the
settings/profile database — under a tooltip reading "Local SQLite logbook".
Those became two separate files when the logbook was split out, so an operator
checking where their QSOs live, or which file to copy before a trip, was pointed
at the wrong one. MySQL was already right.
It now resolves the logbook the same way connectLogbook does: the active
profile's own file if it names one, then the default logbook.db, and the settings
db only in the case where it genuinely doubles as the logbook.
Also moves the 4O3A mark beside its menu label instead of the far edge of the
row, as asked.