Commit Graph
100 Commits
Author SHA1 Message Date
rouggy 5a4ad800b3 test: pin which modes produce a recording
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.
2026-07-30 23:24:58 +02:00
rouggy e152ef0ee0 feat: record CW QSOs again
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.
2026-07-30 23:13:21 +02:00
rouggy b6465ddc9d docs: trim the Flex split entry to the change itself 2026-07-30 23:07:52 +02:00
rouggy 961357474d feat: Flex stays on the RX slice in split
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.
2026-07-30 23:03:29 +02:00
rouggy 702ca4f0c9 docs: one line per changelog entry
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.
2026-07-30 23:02:16 +02:00
rouggy 93b4964b3b chore: release v0.22.3 2026-07-30 22:33:01 +02:00
rouggy 14c57035e1 docs: changelog entry for the DARC DOK Award (DLD) 2026-07-30 22:32:53 +02:00
rouggy 7a7fa62af7 feat: choose how dates are displayed (Standard / French / US)
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.
2026-07-30 22:22:33 +02:00
rouggy 6ee4c430c7 feat: colour the QSL and upload status letters
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.
2026-07-30 22:12:23 +02:00
rouggy 0df6370c59 docs: the author's wording for the remote-delete hint
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.
2026-07-30 22:04:15 +02:00
rouggy fa94657619 docs: cut the remote-delete hint to what must be known before ticking it
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.
2026-07-30 22:03:02 +02:00
rouggy f5b74856c6 fix: the remote-delete switch sits above the service tabs
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.
2026-07-30 22:01:45 +02:00
rouggy d42fdab85c feat: withdraw a deleted QSO from QRZ.com and Club Log
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.
2026-07-30 21:57:40 +02:00
rouggy 83a8708d60 fix: remember the award sort, and the grey line, across restarts
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.
2026-07-30 21:49:25 +02:00
rouggy c0671bfe2e feat: sort award references by reference or description
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.
2026-07-30 21:47:05 +02:00
rouggy ec0cb95de9 style: the play button is green
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.
2026-07-30 21:43:41 +02:00
rouggy 9d691343cf fix: say in the log why a recording was not saved
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.
2026-07-30 21:41:25 +02:00
rouggy c6088bb2de fix: the stop button overlapped the recording counter
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.
2026-07-30 21:39:50 +02:00
rouggy 069728ac68 feat: stop a QSO recording and play it back on the air
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.
2026-07-30 21:36:42 +02:00
rouggy f221c0ee1e feat: record a contact by hand when automatic recording is off
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.
2026-07-30 21:24:51 +02:00
rouggy 013492adb0 fix: name CAT backends by how the radio is reached
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.
2026-07-30 21:20:08 +02:00
rouggy 3a87b58ba3 feat: Kenwood CAT over a network serial bridge
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.
2026-07-30 21:13:19 +02:00
rouggy e6b8a17772 fix: a rig reporting LSB/USB left the mode selector empty
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.
2026-07-30 20:50:05 +02:00
rouggy 802c6e8c43 docs: changelog records what shipped, not how it was built
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.
2026-07-30 20:37:16 +02:00
rouggy f82631690a fix: grey line cut off at the edge of the world copy
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.
2026-07-30 19:35:26 +02:00
rouggy 3b58e39eec feat: grey line on the world map; fix: trace checkboxes lied about their state
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.
2026-07-30 18:06:12 +02:00
rouggy 45e3f3edb8 fix: bulk update no longer holds the application hostage, and says what it does
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.
2026-07-30 18:00:19 +02:00
rouggy c931d8a762 feat: progress bar for bulk QSO updates from QRZ.com
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.
2026-07-30 17:54:07 +02:00
rouggy eb271e8f20 fix: Kenwood split read from FR/FT when the status frame omits it
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.
2026-07-30 16:50:06 +02:00
rouggy e2d2485703 test: drive the Kenwood backend against a rig that answers
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.
2026-07-30 16:36:49 +02:00
rouggy 712a124081 chore: release v0.22.2 2026-07-30 15:14:32 +02:00
rouggy 08a932c9c1 fix: drop the per-column filters from the recent-QSO grid
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.
2026-07-30 15:13:25 +02:00
rouggy fee2303625 test: pin the whole ADIF import path, not just the conversion step
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.
2026-07-30 15:07:48 +02:00
rouggy 4d5c5c3eb3 feat: move ADIF fields on import, for contest exports that misplace the exchange
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.
2026-07-30 14:57:05 +02:00
rouggy d322ea9a07 feat: report UI crashes instead of showing a blank window
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.
2026-07-30 14:36:19 +02:00
rouggy 75c02ea2a0 feat: CI-V byte trace, for a fault the command table cannot explain
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.
2026-07-30 14:30:05 +02:00
rouggy ef84b04c99 fix: CI-V frames read at the wrong offset decoded as plausible frequencies
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.
2026-07-30 14:05:30 +02:00
rouggy fbd6cf400f docs: open 0.22.2 — five entries described work absent from the 0.22.1 build
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.
2026-07-30 12:00:45 +02:00
rouggy b16cb973f2 feat: native Kenwood CAT backend
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.
2026-07-30 11:56:41 +02:00
rouggy da886de189 feat: support ERC rotator controllers through the existing GS-232A backend
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.
2026-07-30 11:07:57 +02:00
rouggy 7759e09b4e fix: Yaesu mode commands always addressed the main receiver
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.
2026-07-30 10:10:43 +02:00
rouggy a8990ff29d fix: the status bar named the settings database, not the logbook
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.
2026-07-29 23:53:18 +02:00
rouggy e3b810b4ff feat: draw the actual 4O3A mark in the sidebar, not a text badge
The wordmark I put there was not what the author expected: the brand is "4", a
green waveform of stacked lenses, then "3A".

Only the waveform is SVG. The digits stay HTML text so they keep the sidebar's
own typeface at any theme or zoom — an SVG <text> would pick its own font and
drift from the menu around it. Seven ellipses, tallest in the middle, match the
shape of the mark.

Still drawn rather than fetched: an image off the web has no verifiable
provenance, and this stays one small component to swap if the official SVG is
preferred.
2026-07-29 23:37:57 +02:00
rouggy 337392bb6d feat: mark the 4O3A panels in the settings sidebar
Antenna Genius and Tuner Genius now carry a small 4O3A wordmark, so the two
panels that drive that hardware are recognisable without reading the labels.

Drawn inline rather than shipped as an image file, following the language
picker's flags: it scales with the row, follows the theme through currentColor,
and adds no binary asset to the repository.

It is a plain wordmark, not the manufacturer's logo artwork — I did not pull an
image off the web, since I cannot verify the provenance or licence of what I
would find, and redistributing a company's mark inside the product is the
author's call. The component is one function and takes the official SVG in its
place if that is preferred.

Not applied to the Amplifier panel: it covers both the 4O3A PowerGenius XL and
the SPE Expert range, so a single vendor mark there would be wrong.
2026-07-29 23:28:27 +02:00
rouggy d98bb2e929 chore: release v0.22.1 2026-07-29 23:13:27 +02:00
rouggy 3ab6b95ff8 fix: auto-call CW gap started before the message had finished
Reported: a 6 s auto-call gap gives clearly less than 6 s between the end of one
CQ and the start of the next.

The wait after sending watched only the keyer's busy signal. That signal travels
from the keyer to the window as an event, and the code gave it one second to
appear before giving up — if it had not arrived and cleared by then, the wait
ended immediately and the gap ran from the START of the call. A 6 s gap after a
4 s CQ becomes about 2 s of silence, which is exactly what was heard.

The message's own duration is now a FLOOR: the wait runs at least the estimated
sending time, and the busy signal only extends it (slow link, long buffer),
capped as before. The estimate can only be approximate, but it cannot be skipped
by a late event, and erring long costs a slightly wider gap rather than a call
that steps on the previous one.
2026-07-29 22:28:18 +02:00
rouggy b0da6a3d14 fix: RX and TX both on SUB is simplex, not split
Reported on an FTDX101: with RX and TX moved together to the sub VFO, OpsLog
showed split — and took the MAIN frequency as the transmit one, which would log
the wrong frequency.

ST is a bare flag. It says "split" without saying which VFO transmits, and this
rig raises it whenever the transmit VFO is the sub one, whether or not the
operator is also listening there. FT names the transmit VFO, so where the receive
VFO is known as well (FR), split is derived from the pair: they differ or they do
not. That is a fact about the rig's state rather than a flag whose meaning varies
by model.

The fix is therefore in the probe ORDER, not in the reading: FT is asked first
when FR answered, and ST stays the fallback for rigs that have neither. A test
pins the order and both halves of the trap — that FT gets this case right, and
that ST alone gets it wrong.
2026-07-29 22:10:21 +02:00
rouggy 2d7469a7f7 fix: do not report a Yaesu as connected when it answers nothing
The seven minutes of reconnects in the operator's log turned out to be the radio
switched off — no defect. But the log said otherwise:

  20:14:42 yaesu: ID query failed (timeout) — continuing…
  20:14:44 yaesu: connected on COM7 @ 38400 baud, model="FTDX101D"

Opening the port was treated as connecting, and the model name was left over from
the previous session, so a powered-down rig produced a line claiming it was
connected as a named model. That is what sent me looking for a software fault.

Connect now tracks whether ANY probe answered. If none did, it clears the stale
model, says so in terms an operator can act on — is it powered on, is the CAT
rate right — and returns an error so the reconnect loop keeps trying rather than
believing it succeeded.
2026-07-29 21:53:26 +02:00
rouggy 775f411606 fix: Yaesu Connect leaked the serial handle on every reconnect
Connect assigned a fresh handle to y.port without closing the one it already
held. It is called again on every reconnect, so a rig that fails to answer leaks
one handle per attempt — every few seconds, indefinitely.

Windows opens a serial port EXCLUSIVELY, which makes this visible as "yaesu: open
COM7 @ 9600 baud: Serial port busy" in an operator's log: OpsLog was competing
with itself for the port.

This is a real defect found while reading the reconnect loop, but I want to be
straight about its limits: it does NOT explain the seven minutes of "ID query
failed — timeout" in the same log, where the port opened and the rig stayed
silent. That one is still open, and the operator alternates between two separate
radios (a TCI SDR and the Yaesu) rather than sharing one port, so a conflict
between backends is not the explanation either.
2026-07-29 21:43:05 +02:00
rouggy 6501e97895 fix: read the FIRST digit of the Yaesu VFO reply — I inverted it yesterday
Reported both ways round, which is what settles it. With RX and TX on SUB
everything worked; with them on MAIN the frequency froze and a spot click tuned
the sub VFO. The log shows the rig answering FR01 in the MAIN case.

So on an FTDX101 the state is the FIRST digit — 0 = main — and the second is a
separate parameter. Yesterday I changed this to the LAST digit, which read FR01
as SUB and inverted the whole thing. The fault that change was meant to fix ("SUB
shows MAIN") came from reading VS instead of FR, and probing FR had already fixed
it on its own; the digit change was an unnecessary guess layered on a real fix.

Nothing else needed changing for the operator's two questions: the display and
SetFrequency already follow curVFO, so a spot now tunes whichever VFO holds RX
and TX, and the main frequency is read when on main. A test states that in those
terms — active VFO and which command a spot writes — rather than only as a byte,
because the byte is what I got wrong while believing the logic was right.
2026-07-29 21:39:22 +02:00
rouggy f3807c21ba fix: FTDX101 answers the VFO query with two digits, and only the first was read
The log named the cause in one line: yaesu: receive VFO is read through FR
(answered "FR01;"). An FTDX10 answers "FR0;" — one digit — so reading the digit
straight after the prefix worked there and took the 0 of "01" on an FTDX101.

The rig was on SUB and OpsLog said MAIN, which is the reported "Sub shows the main
frequency". It also explains the second half: split is "TX VFO differs from RX
VFO", so a receive VFO that alternated between right and wrong made the split flag
flip between consecutive polls and swap freq and rx in the log.

The state is now taken from the LAST digit before the terminator, which is correct
on both forms, and the same reading applies to ST/FT/VS rather than leaving the
next two-digit rig to be found in the field.

On the operator's question of whether freq should simply be the active VFO: no —
freq is the TRANSMIT frequency by ADIF convention (FREQ is the QSO frequency,
FREQ_RX the receive one, set only when split). In simplex they are the same, so it
does read as the active VFO; in split it must stay the transmit VFO or the log
records the wrong frequency. What the operator saw was this bug, not that rule.
2026-07-29 18:35:47 +02:00
rouggy d536c39ef5 docs: label the three numbers in the QSO counter
"Showing 10000 of 23683 matches · 28648 total" was read as a filter matching more
than the log holds. The numbers were right — 23 683 matches out of 28 648 QSOs,
capped at 10 000 displayed — but the middle one belonged to "matches" while
sitting where "of N" normally means the total.

Each number is now named where it appears: shown · match the filter · in the log.
Thousands separators too, since 23683 and 28648 are hard to compare at a glance.

No counting change: both counts run over the same table, so matches can never
exceed the total.
2026-07-29 18:10:39 +02:00
rouggy 284a7a18c7 fix: QRZ download marked every paper-QSL QSO as QRZ-confirmed
Reported: after a confirmation download they all turn to Y.

The test accepted qsl_rcvd = Y as a confirmation. That field is the operator's
OWN paper-QSL flag, which they uploaded to QRZ in the first place — so QRZ handed
it back and OpsLog read it as QRZ's answer. On a log full of paper cards that is
most of the log, which is exactly what was seen.

Only QRZ's own statement counts now: app_qrzlog_status = C, or the ADIF field
qrzcom_qso_download_status = Y that QRZ sets for it. LoTW and eQSL confirmations
are explicitly not accepted either — they are other services' answers, and the
column says QRZ.

This status feeds award slots, so a false Y is a QSO counted as confirmed when it
is not. That asymmetry is why the rule is narrow rather than generous, and the
test lists the negatives as deliberately as the positives.

The changelog warns that earlier downloads may already have set the column
wrongly — the fix stops it happening again but does not undo it.
2026-07-29 17:59:04 +02:00
rouggy 482f81fe45 docs: say that the import option also fills the confirmation statuses
Asked for as a missing feature. It is not missing — the "fill my station fields"
option has stamped the default QSL / LoTW / eQSL / Club Log / HRDLog / QRZ.com
statuses on empty fields since v0.14. What was missing is any mention of it: the
option's description talked only about MY_* fields, so the behaviour was
undiscoverable except by comparing a log before and after.

The description now says it, in both languages, and names why it matters — a
WSJT-X log carries almost no QSL fields.

The rule is also pinned by a test now, which meant splitting the fill from the
settings lookup: blanks are filled, existing values are never touched.
Overwriting them would erase confirmations the operator has actually received,
and that is the half a future edit is most likely to get wrong.
2026-07-29 17:56:10 +02:00
rouggy 5d7a9a9562 docs: changelog for the DXHunter tune-on-spot-click (a036120)
Describes what the operator gets: a spot click in DXHunter now moves the radio,
not just the callsign field. Notes explicitly that a packet without <FREQ>/<MODE>
behaves exactly as before — that is the reassurance anyone already using
remote_call needs, and it is the part a feature description usually omits.

Appended at the bottom of the block, per the rule adopted with 0.22.0: a version
is read top to bottom.
2026-07-29 17:51:23 +02:00
rouggy a036120871 feat: remote_call UDP accepts <FREQ>/<MODE> from DXHunter and tunes the active CAT backend 2026-07-29 17:49:29 +02:00
rouggy ccceab9d02 fix: Yaesu showed MAIN when the operator had moved RX and TX to SUB
The operator's description separates the two cases precisely, and that is what
identifies the fault. RX alone on SUB displayed VFO B correctly; RX AND TX on
SUB — entirely on the sub receiver — displayed VFO A.

VS was being read as "which VFO is in use". On an FTDX101 it does not answer that
question: with both RX and TX on sub it still reported the main VFO. FR is the
command that selects the RECEIVE VFO, and it is now asked first, VS remaining the
fallback for models that lack it.

Split falls out of the same correction: split means the TRANSMIT VFO differs from
the RECEIVE one, so a wrong receive VFO made the comparison wrong too — which is
the second half of what was reported. A test covers all four RX/TX combinations,
including the one that is NOT split (both on sub) and reads as split if you take
the transmit VFO alone.
2026-07-29 17:26:00 +02:00
rouggy 35db1440e4 fix: repair a MySQL logbook already left with TEXT columns
Translating the schema correctly does nothing for a database that already
exists. This operator's had been created by an earlier attempt, so qso.state was
already TEXT and every launch died on the same statement — CREATE INDEX … ON
qso(state), error 1170 — with the connection failing at startup and no way
forward from the UI.

Columns that MUST be VARCHAR (the indexed and keyed ones) are now converted
before the migrations run. It reads information_schema, touches only the columns
in varcharColumns, and only when they are actually TEXT — a no-op on a healthy
database, and it preserves each column's NULL/NOT NULL.

A failure to repair IS reported: it is the difference between a logbook that
opens and one that does not. A failure to READ information_schema is not — a
server that will not answer it can still be running a perfectly good schema, and
refusing to open would turn a non-problem into an outage.
2026-07-29 16:35:57 +02:00
rouggy e3aabc06a4 fix: convert EVERY column in a MySQL baseline, not just the first per line
The quoting fix was necessary but not sufficient: "column 'op_name' can't have a
default value" persisted.

The translation worked line by line and rewrote only the FIRST column on each
line. Our own migrations put one column per line, so nothing showed there — but
sqlite_master stores a table's CREATE statement with every ALTER-added column
appended to the SAME line, and the baseline is built from exactly that. On a
logbook that had been through upgrades, most columns therefore kept their TEXT
type and MySQL rejected the schema.

Two consequences fixed together: every match is now rewritten, and each one is
decided from ITS OWN declaration — the text between the surrounding commas —
rather than from the whole line. Deciding per line would have made every column
sharing a line with a default-bearing one into VARCHAR(255), which breaches the
InnoDB row-size limit the TEXT rule exists to respect.

Tests cover the many-columns-on-one-line shape and pin that a neighbour's DEFAULT
does not leak.
2026-07-29 16:30:44 +02:00
rouggy ab9d0bfe0f fix: MySQL schema build failed on quoted column declarations
Two errors from a shared MySQL logbook, both fatal to the schema and neither
naming its cause:

  CREATE INDEX … ON qso(state) → 1170 BLOB/TEXT column used in key
                                 specification without a key length
  op_name TEXT … DEFAULT ''    → 1101 TEXT column can't have a default value

One root cause. TEXT columns are rewritten to VARCHAR(255) when they are indexed
or carry a default — that is exactly what those two MySQL rules require — but the
pattern only matched a BARE column name. The SQLite baseline dump quotes its
identifiers, and mysqlDDL turns those quotes into backticks before the TEXT rules
run, so every column coming from the baseline kept its TEXT type and MySQL
rejected the schema.

The operator saw none of this: just "MySQL is enabled but the connection failed
at startup" and a silent fallback to local SQLite — which is the worst shape for
a shared logbook, because each operator then logs into their own copy.

The pattern now accepts a quoted or bare name and puts the quoting back. A test
covers both forms, plus the case that must NOT change: an unindexed column stays
TEXT, since VARCHAR everywhere would break MySQL's row-size limit. Reverting the
one-line change makes it fail.
2026-07-29 16:26:04 +02:00
rouggy a3fd32ec91 fix: split read (and set) backwards on a Yaesu reporting the TX VFO
Reported on an FTDX101: the panel showed split ON with the radio OFF, and the
reverse — while the same model behaved correctly for another operator, and an
FTDX10 was fine throughout. That pattern is the clue: one operator was on MAIN,
the other on SUB.

The two commands say different things and were read alike:

  ST is a split FLAG   — ST1 means split, whatever VFO is in use.
  FT names the TX VFO  — FT0 = transmit on A, FT1 = transmit on B.

Split is on when the rig transmits on a DIFFERENT VFO from the one it listens to.
Reading FT1 as "split" is therefore correct only on VFO A, and exactly inverted
on SUB. It is now compared with the current VFO.

Writing had the identical fault, and it was worse: sending FT1 for "split on"
while the operator listened on SUB CLEARED the split it was asked to set. Both
directions are pinned by tests over each VFO state.
2026-07-29 16:20:25 +02:00
rouggy c297f91ca8 fix: a cluster spot dragged the rig from SUB back to MAIN
Reported on an FTDX101 (F4NBZ): listening on the SUB VFO, clicking a spot moved
the radio to MAIN.

SetSimplexMode is the first thing SetFrequency tries, and it means "receive and
transmit here, simplex" — OmniRig applies that to the MAIN VFO by definition. It
is there because several Icoms ignore direct FreqA/FreqB writes, so it stays for
the main VFO; on SUB it is now skipped entirely and FreqB carries the change.

The generic "Freq" property is skipped on SUB too: on several rig files it IS the
main VFO, so writing it would move the VFO the operator is not using — or pull
them back to it, which is the bug being fixed.

An operator on SUB put themselves there deliberately. A spot click asks for a
frequency, not for a change of VFO — that distinction is the whole fix, and a
test pins which property each VFO state writes.
2026-07-29 16:07:20 +02:00
rouggy ee1f9ccf35 fix: ADIF import silently dropped a file with no header
Reported with screenshots: a dozen records lifted out of wsjtx_log.adi into a new
.adi imported as "0 imported, 0 ignored, 0 total". The parser only emitted
records after <EOH>, and a hand-assembled file has no header — so the whole file
was read as header text and thrown away. The count says the file was empty; there
is nothing on screen to suggest a missing tag, which is why this was reported as
"import does nothing".

An <eor> is proof that a record ended, header or no header. Fields are now
collected always and DISCARDED at <eoh> — that tag is exactly the statement
"everything before this was the header", so real headers stay out of the data
just as reliably, and a headerless file keeps its first record instead of losing
it to the same rule.

An existing test asserted the old behaviour. It encoded a defensible reading of
the spec, but the file it rejects is one operators really make and the rejection
is silent, so the test is rewritten with the evidence that changed it rather than
deleted.
2026-07-29 15:06:41 +02:00
rouggy 6fb7f06375 docs: read a changelog block top to bottom — order 0.22.0 chronologically
New entries were being prepended, so a version block read backwards: an operator
met "the Yaesu meters are corrected" and "the console follows the mode" several
entries before learning that a Yaesu console existed. The fixes made sense only
to someone who had followed the development.

0.22.0 is reordered so each feature appears before what was fixed on it, and
CLAUDE.md now says to append at the bottom.

Restored the entry that INTRODUCES the Yaesu console: I had overwritten it when
editing the meters entry in place, so the release described corrections to a
panel it never announced.
2026-07-29 15:01:35 +02:00
rouggy 54caee37c4 chore: release v0.22.0 2026-07-29 14:55:17 +02:00
rouggy 15fdbce22a fix: WinKeyer 2 sent the dit/dah ratio to the key-compensation command
The byte trace from a real WK2 settled in one line what no amount of reading
could: "TX 11 32". Command 0x11 is SET KEY COMPENSATION in milliseconds, not the
dit/dah ratio — that is 0x17. So a neutral ratio of 50 asked for 50 ms of extra
key-down on EVERY element. At 25 wpm a dit is 48 ms, so elements more than
doubled and ran into each other: the reported "it sends one element, then a long
pause".

The ratio now goes out as 0x17, and the compensation is explicitly set to 0 —
merely stopping the wrong command would leave an affected keyer misbehaving,
since it keeps the value in EEPROM until something writes over it.

The init sequence is now built by a separate function so the BYTES are testable,
and a test pins each command number. These numbers are the contract with the
hardware, and a wrong one produces a fault that cannot be diagnosed from the UI
at all — this one cost the operator weeks and needed a trace to find.

This is why the fix waited for the trace rather than being guessed: the plausible
guesses (mode register, sidetone, WK1-vs-WK2 differences) were all wrong, and any
of them shipped blind would have broken the keyers that work today.
2026-07-29 14:53:18 +02:00
rouggy c7737aabd6 docs: changelog block becomes 0.22.0, and the duplicated CW entry is merged
Renamed from 0.21.9 at the author's request: this release adds native Yaesu and
Xiegu CAT, CAT sharing over Hamlib NET rigctl, a Yaesu console and a fifth CW
engine, which is a minor version rather than a patch.

The Yaesu keyer had two entries — one written when it was added, one when the
FTDX10 turned out to refuse it. Merged into the single statement an operator
needs: what it does, where to select it, and which rigs accept it.
2026-07-29 14:49:15 +02:00
rouggy 6f5acc2eef docs: name the rig setting that makes the voice keyer inaudible
The weak audio was not a level at all: the radio was still modulating from its
front microphone, so almost nothing of the USB feed reached the air. On an FTDX10
that is MENU → SSB MOD SOURCE = REAR.

Worth putting in the settings hint rather than leaving in a conversation — every
operator wiring a voice keyer for the first time meets it, and no amount of gain
in OpsLog can compensate for a rig listening to the wrong input.
2026-07-29 14:46:50 +02:00
rouggy 67a03c3ddc feat: a level control for the voice keyer, and name every CAT backend in its PTT list
The messages went to the radio exactly as recorded. Nothing in OpsLog could
raise them, so a microphone captured quietly drove the rig quietly and the only
remedies were the radio's own USB input menu or the Windows mixer — which is
where the operator was heading. There is now a level from 10 to 400 %, applied
with clamping (a wrap would turn loud speech into noise on the air), and Play
previews at that same level so the adjustment is made against what will actually
be transmitted.

The PTT method list also named only OmniRig, Flex, Icom and TCI, so choosing a
native Yaesu left "CAT" with no backend beside it — which reads as "there is no
CAT PTT for my radio" and sends the operator to RTS on a COM port that has
nothing to do with the rig. Same list-needing-every-member shape as three
earlier bugs in this feature. The TestPTT log line had the same rot: it said
"CAT via OmniRig" whatever backend was running.
2026-07-29 14:29:47 +02:00
rouggy 8da3a27803 fix: the PWR bar stayed at full scale after transmitting
Receiving cleared the two bar percentages but not the values the bars now
actually draw from: the watts and the SWR ratio, added when the meters were
corrected. So the power bar sat pinned across the panel with the rig plainly
receiving — the dash in the label said one thing and the bar another.

Every transmit value is cleared now, and the peak-hold state with them: a peak
left in the holder would have carried the last transmission's reading into the
start of the next one, which is worse, being wrong while it looks live.

The panel also draws both bars from zero unless the rig reports transmitting, so
a value that has not been refreshed yet cannot show as output.
2026-07-29 14:20:58 +02:00
rouggy e0cefb5c41 fix: the Yaesu power meter is not linear — calibrate it on three points
One point could not reveal the curve. Scaling 207 = 100 W straight down read
30 W where the radio showed 10, and 75 where it showed 50 — wrong everywhere
except at the single point it was fitted to.

Three readings taken against the rig's own display give the shape:

  raw  62 → 10 W
  raw 155 → 50 W
  raw 207 → 100 W

Interpolating between them reproduces the radio exactly at those points and stays
close in between. I did not fit a formula: three samples can be made to support
several curves, and the operator can check a table against their own meter.

Above the top the last segment's slope continues rather than clamping, so a rig
driving an amplifier does not sit pinned at 100 W. A test pins the measured pairs
and the monotonicity, so a later change that breaks them fails against the radio
rather than against taste.
2026-07-29 14:11:25 +02:00
rouggy 113faede14 fix: hold the TX meters through the gaps between words
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.
2026-07-29 13:59:47 +02:00
rouggy 8e491544dd fix: power is RM5, not RM4 — the CW keying was the experiment
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.
2026-07-29 13:56:59 +02:00
rouggy cbe571a742 chore: log the power setting with the Yaesu meters, to identify the wattmeter
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.
2026-07-29 13:52:45 +02:00
rouggy bdda7ad07a fix: read the real SWR on the Yaesu, and show the ratio
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.
2026-07-29 13:49:09 +02:00
rouggy 3fd6763ff0 fix: the Yaesu SWR bar showed a meter unrelated to the antenna
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.
2026-07-29 13:42:25 +02:00
rouggy a89c6e0df8 chore: sample the Yaesu meters throughout a transmission, not at its start
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.
2026-07-29 13:38:09 +02:00
rouggy 48d92ff547 fix: CW speed drove the wrong keyer; survey the meters to find the SWR one
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.
2026-07-29 13:32:29 +02:00
rouggy adadb632fa docs: FTDX10 confirmed — no CAT keying, the serial DTR keyer is the way
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.
2026-07-29 13:27:04 +02:00
rouggy b0b25c7f1f docs: name PC KEYING = DAKY, the setting that should enable CAT keying
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.
2026-07-29 13:23:22 +02:00
rouggy e120bd4f04 docs: an FTDX10 has no CAT keying command — say what works instead
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.
2026-07-29 13:21:49 +02:00
rouggy 40df4fe22f fix: read the rig's answer to KY — it was killing the CAT link, silently
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.
2026-07-29 13:18:52 +02:00
rouggy d114a73729 fix: the FTDX10 refuses KY; — stop waiting on a status it will never send
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.
2026-07-29 13:15:36 +02:00
rouggy d0666581c3 fix: a CW macro knocked the Yaesu CAT link over
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.
2026-07-29 13:03:09 +02:00
rouggy 37dc2a07e5 fix: the Yaesu keyer refused to send — a status reply we read too strictly
"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.
2026-07-29 13:00:30 +02:00
rouggy 7424bc6e81 fix: the Yaesu keyer reported "Rig CAT offline" while CAT was working
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.
2026-07-29 12:58:08 +02:00
rouggy 01c93d979f fix: the Yaesu keyer selection was thrown away when read back
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.
2026-07-29 12:55:26 +02:00
rouggy 0190255762 fix: the Yaesu keyer asked for a COM port it does not use
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.
2026-07-29 12:49:19 +02:00
rouggy 0cc806722e fix: Antenna Genius drew both ports on the same antenna
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.
2026-07-29 12:46:48 +02:00
rouggy 08f3e54afb feat: CW through the Yaesu keyer — fifth CW engine
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.
2026-07-29 12:42:40 +02:00
rouggy 520d17810c fix: the Yaesu panel showed the TRANSMIT frequency as the main VFO under split
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.
2026-07-29 12:19:49 +02:00
rouggy c9218310ae feat: the Yaesu console follows the mode — CW gets its own controls
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.
2026-07-29 12:07:28 +02:00
rouggy 8cc6997eec feat: colour the Yaesu S-meter like a real one — green, amber past S9, red at +20
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.
2026-07-29 11:59:58 +02:00
rouggy 6d309cada1 fix: SPLIT placed the transmitter on a stale VFO; share the meter widget
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.
2026-07-29 11:55:28 +02:00
rouggy 7153768579 feat: Yaesu panel — sideband on re-click, split readout, one-touch up 1/up 5
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.
2026-07-29 11:46:54 +02:00
rouggy df4155108f fix: centre the Yaesu panel like the Icom and Flex ones
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.
2026-07-29 11:42:42 +02:00
rouggy 9bc5a14c69 fix: Yaesu panel — S units, sidebands, three-step ATT, readable sliders, own tab
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.
2026-07-29 11:37:18 +02:00
rouggy 842d4708a7 feat: Yaesu control panel (meters, bands, DSP, TX), and drop a stale Flex hint
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.
2026-07-29 11:17:08 +02:00
rouggy e2aba828a9 feat: native Xiegu CAT backend (G90 / X6100 / X6200 / X5105)
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.
2026-07-29 11:06:27 +02:00
rouggy 753d8d2ffa fix: accept Hamlib's VFO-prefixed commands — JTDX could not set the frequency
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.
2026-07-29 10:58:16 +02:00