Operators rarely write the canton in their address/QTH but do write the city.
Add a per-reference city regex to every one of the 26 cantons (same mechanism as
WAPC): the description match now fires on the canton NAME or any of its main
cities (Genève→GE, Lausanne→VD, Zürich→ZH, Bellinzona→TI, …), with accented and
ASCII/localised spellings. Scoped to DXCC 287 so cross-border city-name clashes
can't misfire. Catalog version bumped 1→2 so the update reaches anyone already
running H26 v1 (unless they've edited it).
The random per-install UUID lives in the LOCAL SQLite, so one operator running
OpsLog on several machines (laptop / desktop / Maestro) or after a reinstall
counted as several distinct "users" — making the user total unreliable. Use the
station callsign as distinct_id when set (public in ham radio, stable across all
of an op's machines); the install UUID stays as a fallback and rides along as an
install_id property so per-machine detail isn't lost.
A SteppIR only covers part of the spectrum (a standard one is 20 m–6 m) but
can't report its own range, so the follow loop's existing out-of-range guard
(FreqMin/FreqMax) never engaged for it. Tuning the rig to a band the antenna
can't reach (e.g. 30 m) made the loop keep trying — the immediate cause of the
stuck TX interlock addressed in the previous commit.
Add a Tunable range (MHz) setting to the Antenna panel, wired into the SteppIR
adapter's Status().FreqMin/FreqMax so the follow loop skips out-of-range
frequencies entirely: no tune command, no TX inhibit. Defaults to 13–54 MHz
(20 m–6 m, the common model) when unset; widen the low edge for a 40 m-equipped
SteppIR. Ultrabeam is unaffected (it reports its own per-band coverage).
Keeps the last-commanded-freq deadband from the previous commit as a universal
safety net for any flaky-status case within the valid range.
The motorized-antenna follow loop keyed its deadband off the antenna's reported
frequency. A SteppIR reports an intermittent/stale status frequency (flips
between the commanded freq and its home/6 m value), so the deadband tripped on
almost every 1.5 s poll and re-issued a tune command. Each command refreshed
noteMotorMoveCommanded(), keeping motorTXInhibitLoop's recentCmd window alive, so
the Flex transmit-inhibit ("Interlock is preventing transmission") never
released — confirmed in a real log (moving=0 throughout, recentCmd was the
driver).
Follow loop now keys the deadband off the LAST COMMANDED rig frequency, only
re-tuning when the radio actually QSYs beyond the step — immune to a flaky
antenna status. Falls back to the antenna's freq only until the first command.
Also dropped SetTXInhibit's `interlock set reason=` sends: `reason` is a
read-only field on the Flex interlock object, so writing it is rejected
(cmd error 5000002D on V1.4.0.0) and did nothing; `transmit set inhibit=` is the
real mechanism and is unchanged.
New built-in award file internal/award/catalog/h26.json (USKA "The Helvetia 26
Award HF"): 26-canton reference list, matches the canton from the QSO address
(OR-rule: QTH) by description, scoped to DXCC 287 (HB) on HF. Fixed a typo in
the description ("Swityerland"). Changelog entry added (EN+FR).
This reverts commit 6a28344. Restore the once-a-day anonymous PostHog heartbeat
and its opt-out toggle: telemetry.go (sendTelemetryHeartbeat, GetTelemetryEnabled/
SetTelemetryEnabled, PostHog host + API key), the startup goroutine, the Settings
→ General toggle + i18n keys, and the README/wiki mentions. version.go is dropped
again (appVersion moves back into telemetry.go). release.ps1 (untracked) pointed
back at telemetry.go by hand.
The matching "five rules" were already documented; add the parts that were only
in code: the per-QSO Explain trace (Test a callsign in the award editor), the
Missing-refs gap finder (in-scope-but-no-reference + bulk assign), how one QSO
credits several references (n-fer POTA / VUCC grid lines, comma/semicolon split),
code-vs-exact matching, and how built-in award updates skip awards you've edited.
The SteppIR client logged commands it sent but never the status frames it read
back, so a misread motor-status byte — which drives the app's "block TX while
the antenna is moving" interlock on a FlexRadio — was invisible. Log the raw
11-byte frame plus the decoded freq/dir/moving and the motor (buf[6]) and
direction (buf[7]) bytes, but only when the frame changes, to keep the 2 s poll
from flooding the log. Purely diagnostic; no behaviour change.
Drop the once-a-day anonymous "app_opened" heartbeat to PostHog and everything
around it: telemetry.go (sendTelemetryHeartbeat, GetTelemetryEnabled/
SetTelemetryEnabled, install-ID/last-sent settings keys, PostHog host + API key)
and the startup goroutine that fired it. The 'Send anonymous usage statistics'
toggle is removed from Settings → General (TelemetryToggle + i18n keys), and the
telemetry mentions are stripped from the README and wiki.
appVersion (still used by changelog/update/livestatus/log-email) moves to a new
version.go; release.ps1 now rewrites it there instead of telemetry.go.
writeRecord gained a fourth `allow map[string]bool` argument in the export
field-picker change; the round-trip test still called it with three. Pass nil
(no field filtering — the historical behaviour).
The RECEIVE card had grown tall after adding WNB and the SmartSDR v4 DSP rows
(NRL/NRS/NRF/ANFL/RNN/ANFT). Keep the everyday NB/NR/ANF visible and tuck the
supplementary controls behind a collapsible 'DSP' button. The button badges +
highlights when any hidden control is ON so an active filter is never hidden,
and its open/closed state persists in localStorage (opslog.flexDspOpen).
Global "Export to ADIF" gains a third option "Choose fields…" next to the
standard-only and full-OpsLog choices, and right-clicking selected QSOs adds
"Export selected — choose fields…". The picker separates official ADIF 3.1.7
fields (grouped by category) from OpsLog/non-standard tags actually present in
the log (discovered via DistinctExtraKeys over extras_json), with All/None per
group and reset-to-defaults; the selection is persisted per user.
Backend: adif.Exporter gains a Fields allow-set that gates every promoted and
extras field in writeRecord; app.go ExportADIF/ExportADIFFiltered/
ExportADIFSelected take a fields []string param, plus new ADIFExtraFields.
The External-services panel (HRDLOG, eQSL, LoTW, POTA) and the many input
placeholders across all tabs (QRZ/ClubLog included) were still hardcoded
English. Added ~40 es.* keys (EN + FR) and wired every label, placeholder,
hint, checkbox, button and the "coming soon" fallback through t(). Renamed the
tab-map param off `t` so it no longer shadows the i18n function.
The Password-encryption, ClubLog-exceptions and ClubLog-Most-Wanted blocks in
the General settings panel were still hardcoded English. Added ~30 gen.* keys
(EN + FR) and wired every heading, checkbox label, description, button and
status line through t(), plus the passphrase-mismatch error.
A QSO with a non-Latin character (Cyrillic Я = \xD0\xAF, Polish ł, …) failed to
save on the shared MySQL backend with "Error 1366: Incorrect string value …
for column 'qso'.'address'". Cause: the database had been pre-created by a
hosting panel (o2switch/cPanel) as latin1, so OpsLog's CREATE DATABASE IF NOT
EXISTS … utf8mb4 was a no-op and every table inherited latin1 — which can't
store those letters, even over a utf8mb4 connection.
OpenMySQL now runs ensureMySQLUTF8MB4 after migrations: ALTER DATABASE to
utf8mb4 (for future tables) + CONVERT any table with a non-utf8mb4 text column
via an information_schema probe. Idempotent and cheap on a healthy DB (the probe
returns nothing → no ALTERs); best-effort so a charset issue never blocks
logbook access. SQLite is unaffected.
The pass-order "#" is a rowIndex valueGetter, but AG-Grid keeps the same row
node across a reorder (getRowId by id) and caches the value, so a moved station
kept its old number. Force-refresh the __order column on every model update in
passOrder mode.
The on-air list was ordered by log time, but a net controller needs the order
the mic goes around. The on-air grid now renders in a controller-owned order:
- A pinned "#" column numbers the round; header sorting is disabled on this
grid (new passOrder prop on RecentQSOsGrid) so a stray click can't reshuffle
it, and any previously-saved sort is stripped on restore.
- ⬆⬇ buttons move the selected station up/down the queue.
- New check-ins append to the end; logged/cancelled stations drop out; the
order is reconciled against the live session list and persisted per net
(localStorage opslog.netOrder.<id>), so a tab switch keeps the round.
- "Log & end" now auto-advances to the NEXT station in pass order.
The first RX-audio CW decoder decoded poorly on real signals (vs SDC) and was
removed in cafade0. This rebuilds the DSP core from scratch in
internal/cwdecode, keeping v1's good ideas (pitch lock, Flex cw_pitch
targeting, tiered acquisition) and fixing its proven failures:
- Two-way DEBOUNCE (pending-commit ~0.3 dit): v1 rejected short marks but not
short spaces, so a one-hop fade inside a dah shattered it into dits — the
single worst real-signal failure.
- Per-character BATCH dit/dah classification at flush time, using the batch's
own bimodal boundary — the first letter of an over decodes at any speed; the
timing clusters (muDit/muDah) are updated with the same attribution, killing
the classify-then-learn feedback spiral ("all dits at 60 WPM").
- dB-domain envelope with separate floor/peak trackers, hysteresis slicer,
span CAP (30 dB — else quiet backgrounds put the off-threshold in the
analysis-window skirts and letters merge) and window de-bias on durations.
- Double squelch: span >= 6 dB AND peak >= bank-median + 9.5 dB (span alone
cannot reject pure noise).
- Hamming-windowed Goertzel, 16 ms window / 5 ms hop (a 20 ms window left
40 WPM inter-element gaps with no envelope dip).
- Hardened acquisition (overlapping windows made "3 stable hops" meaningless)
plus SUPERVISED RE-LOCK: a clearly stronger tone on another pitch sustained
~1 s takes over — heals noise locks and follows a QSY.
- Gap-fed dit-length tracking (inter-element gaps are timing evidence too).
Proven by a synthetic-signal test suite (all passing): clean 12-40 WPM, three
pitches, added noise, QSB fading 0.35-1.0, 10 ms dropouts inside dahs, QRM in
auto and targeted modes, noise-only squelch, mid-over speed change 25->15 WPM,
and jittered hand keying (+/-20-25%, 3 seeds).
Wiring and UI restored from git (app_cw.go, Ear header button, decoded-text
strip with WPM/pitch/level + pitch lock + click-a-word-to-fill-callsign, Tools
menu entry) — strip strings translated (cwd.* i18n keys, EN/FR) this time.
The whole "Manipulateur CW" settings panel was hardcoded in English, so it
stayed English even with the app in French. Added ~35 wk.* keys (EN + FR) and
wired every label/checkbox/select/placeholder through t(). Also removed the long
serial-engine explanation paragraph as requested.
Confirmed from HB9HBY's log: ptt=true, yet the FT-891 dropped to RX between
words during a CQ macro — while the same SCU-17 works in N1MM. Root cause:
go.bug.st/serial drives DTR/RTS through GetCommState/SetCommState, which on the
SCU-17's Silicon Labs CP210x drops the asserted RTS (PTT) the moment DTR (CW)
is toggled. N1MM/WSJT use EscapeCommFunction, which sets one line without
disturbing the other.
OpsLog now drives the lines the same way: a new lineCtl abstraction with a
Windows implementation (linectl_windows.go) that opens the COM port via
CreateFile and toggles DTR/RTS with windows.EscapeCommFunction (SETDTR/CLRDTR/
SETRTS/CLRRTS). linectl_other.go keeps go.bug.st/serial for non-Windows builds.
The serial keyer holds a lineCtl instead of a serial.Port. RTS (PTT) now stays
asserted for the whole macro, so the rig holds TX between words.
Promotes golang.org/x/sys to a direct dependency.
Reported: on a CQ macro the rig drops to RX between words instead of holding TX
for the whole macro. Root cause is PTT not being held (usePTT off, or the RTS
PTT line not wired/honoured). Improvements:
- "Key PTT line" now defaults ON when the Serial engine is selected — without it
the rig runs on semi-break-in and drops between words.
- The serial keyer's line options (PTT, key line, invert, lead/tail, speed) are
now atomic and live-updatable: SaveWinkeyerSettings -> ApplySerialConfig applies
them from the next character, no reconnect needed.
- Each transmission logs its PTT state / key line / lead/tail, so a rig that
still won't hold TX can be diagnosed from the app log.
The macro itself is sent as one string (not fragmented), so PTT is genuinely
held across word gaps when usePTT is on — pending on-air confirmation via the log.
Opt-in (Settings → General). Shows a "MW #rank" pill next to the DXCC entity
name in the entry-strip band/slot matrix — ClubLog's Most Wanted ranking
(1 = most wanted), personalised to the operator's callsign and refreshed daily.
- internal/clublog/mostwanted.go: fetch mostwanted.php?api=1&callsign=<CALL>
(rank→DXCC JSON, real User-Agent), invert to dxcc→rank, cache to
<dataDir>/clublog_mostwanted.json; NeedsRefresh invalidates on callsign change.
- app.go: keyClublogMostWanted setting, a.clublogMW, startup refresh goroutine,
activeCallsign() helper, and Get/Set/Download bindings mirroring the cty ones.
WorkedBefore now carries MWRank (qso.WorkedBefore.MWRank) when enabled.
- BandSlotGrid.tsx: MW pill next to the entity name, colour-tiered by rank.
- SettingsModal General: toggle + download button + status, mirroring the
ClubLog cty-exceptions block.
Also reorganises the changelog: the theme-persistence fix (landed after the
v0.20.11 tag) plus this feature go under a new 0.20.12 entry; 0.20.11 keeps only
what shipped in its binary.
The settings store is backed by the LOCAL SQLite config DB (wired before the
slow remote-MySQL logbook connect), so the "not ready" window is the brief
startup gap before OnStartup builds the store, not a MySQL delay. Fix behaviour
is unchanged; only the comments and changelog wording are corrected.
An update can clear the WebView's localStorage; the theme is then restored from
the portable DB pref, but GetUIPref returned ("", nil) while the settings store
was still starting (indistinguishable from a genuinely-unset pref), so the
self-heal gave up after ~2.4s and fell back to the light default.
GetUIPref now returns an error when the settings store isn't wired yet, and the
theme self-heal treats that as "not ready → keep retrying" (up to ~36s) vs a
real empty answer ("unset → keep default"). A dark theme survives the relaunch.
Confirmed from an FT-891 CAT log: OpsLog tuned via OmniRig's SetSimplexMode,
which returned OK on every spot click but never moved the VFO (readback stayed
put), while SetMode on the same .ini worked — so the CAT link was healthy and
only the frequency write was a no-op. SetSimplexMode was chosen because Icoms
accept direct FreqA writes but don't move; Yaesu/Kenwood are the opposite.
Now, for non-Icom rigs (or if SetSimplexMode errors), also write the VFO
frequency property (FreqA/Freq) directly, which does move them. Icom keeps the
SetSimplexMode-only path so the Main/Sub VFO isn't nudged by a direct write.
Some CW interfaces key on the opposite line level (transmit at rest / key
backwards). New "Invert keying (active-LOW)" toggle flips the asserted level for
both the CW and PTT lines, and the idle state drives both lines to their
inactive level accordingly so an active-low rig isn't left keyed at rest.
Default off = N1MM convention (line HIGH = active).
Adds a 4th CW keyer engine alongside K1EL WinKeyer, Icom CI-V and Flex CWX:
OpsLog bit-bangs the Morse itself on a serial control line (DTR keys CW, RTS
keys PTT — swappable), the hardware-keying method N1MM/WSJT use with a Yaesu
SCU-17 and generic interfaces. No WinKeyer chip needed.
- internal/winkeyer/serialcw.go: serialKeyer — worker goroutine, morse table,
WPM + Farnsworth timing (live speed changes), abortable, PTT lead-in/tail/hang.
- winkeyer.Config gains Type ("k1el"|"serial") + CWKeyLine ("dtr"|"rts");
Manager routes Connect/Send/Stop/SetSpeed/Backspace/Disconnect to it.
- app.go: keyWKCWLine setting; WinkeyerConnect sets Type from the engine.
- Settings -> CW Keyer: new "Serial port (DTR=CW/RTS=PTT)" engine with COM port,
key-line selector, speed/Farnsworth/lead-in/tail/PTT. Macros, auto-CQ and
<LOGQSO> reuse the WinKeyer path unchanged.
UNTESTED on hardware (no SCU-17 to hand): polarity assumes N1MM convention
(line HIGH = active). Weight not yet honoured; Farnsworth is.
The rig's 0x27 waveform frames carry the same leading main-scope selector byte
as the dual-scope IC-7610/9700 (`27 00 00 <seq> <total> …`), but the code keyed
the layout off the CI-V address and treated the 7300 as selector-less, reading
the sequence from Data[1] (always 0x00) so every frame hit the `seq == 0`
guard and was dropped — blank scope. The waveform parser now detects the
leading selector byte from the frame itself (address-independent), and the
scope config/set commands (mode, span, edges) include the selector the 7300
requires. The IC-7610/9700 path is byte-for-byte unchanged (main → idx 2, sub
frames skipped). Confirmed against a real IC-7300 CI-V capture.
0.20.10 was released before the "closing OpsLog switches off the radio's own
scope" fix landed, so its entry is reattributed to 0.20.11 (the fix code is
already committed in 1668455).
SetScope(false) sent both 0x27 0x10 00 (scope DISPLAY off) and 0x27 0x11 00
(CI-V data output off), so quitting OpsLog blanked a local IC-7300's own scope
screen. Only the display-on (0x27 0x10 01) is now sent, and only on enable;
disable stops the CI-V stream alone and never touches the radio's display.
The multi-frame waveform header (USB path) read the second frequency field
as an absolute high edge, but in center mode it is a span — so high < low and
the panadapter had no valid range to map the trace onto (blank scope). It now
applies the same center+span vs low+high disambiguation the IC-7610 single-
frame path already used. FIXED mode was unaffected.
Also stamps changelog 0.20.10 with today's date.
- Station Control: the amplifier panel is now the exact same card as the FlexRadio panel (extracted shared AmpCard: OPERATE/STANDBY, ON/OFF, power level, output-power bar, live FWD/ID/temp meters). Every configured amp gets its own card — no dropdown.
- Help → "Send log to F4BPO" (only when SMTP is configured): e-mails opslog.log + previous session + crash log to the developer. New email.SendFiles for multi-attachment.
- Bulk edit: new "Frequency (MHz)" field sets freq_hz AND recomputes band; out-of-band values rejected. Fixes a run logged on a stale/default freq after CAT dropped.
- Cluster: Time column sorted lexically on the HHMMZ string, so spots after 0000Z sorted below 2359Z and looked frozen at the UTC rollover. Now sorts by real arrival time.
- Also folds in the DVK auto-CQ/2-column layout and Station Control dashboard batch (changelog 0.20.10, EN+FR).
Multi-amp: Settings->Amplifier becomes a list (amps.json, legacy single-amp keys auto-migrate to entry #1); one client per enabled amp with per-id bindings (GetAmplifiers/SaveAmplifiers/GetAmpStatuses/AmpOperate/AmpPower/AmpPowerLevel/AmpFanMode); legacy a.pgxl/a.spe/a.acom point at the first enabled amp of each family. Amp cards in FlexPanel and Station Control gain a dropdown to pick the amp; the status bar shows one clickable chip per amp. Use case: two SPEs run in parallel.
Flex v4 DSP (8000/Aurora): NRL/ANFL (lms_nr/lms_anf), NRS (speex_nr), NRF (nrf) with level sliders, RNN (rnnoise) and ANFT on/off — keys per the FlexLib slice docs; the section only shows when the radio reports these keys (dsp_v4 flag), so 6000-series panels are unchanged.
The station-control code rebuilt a fresh driver on every status poll and every
relay set. Stateful boards (Denkovi FTDI D2XX, USB-serial) hold an OS handle only
one opener can own, so the first poll opened the board and leaked the handle, and
every poll after failed with 'device in use' — the relays greyed out a second
after Save and auto-control never switched. Drivers are now cached per device and
reused, closed on config change. Adds a Test-connection button + detect feedback
in the device editor (reported by VK4MA).
When the configured amplifier is an SPE Expert (not the PowerGenius), the Flex
panel now shows an SPE control card — the Flex doesn't report SPE amps, so it's
driven by OpsLog's own SPE link. The PowerGenius fan-mode control is hidden when
the amp type isn't PowerGenius.
Opening About now fires a fresh CheckForUpdate with a 'Checking…' state, and
clears the update banner when the latest check finds nothing — so it reflects
reality instead of the last periodic check.
Adds GetChangelog (all entries up to the running version, no seen-marker change)
and a Help menu 'What's new' item that reopens the changelog dialog on demand.
Embeds changelog.json (per-version EN + FR notes, curated from the release's
commits). GetWhatsNew compares the stored last-seen version with the running
build and returns the notes for every newer version, once, then advances the
marker. A dialog shows them in the UI language on the first launch after update.
Seeded with the 0.20.5 notes.
Day/Week/Month/Year now show the anchor day hour-by-hour, the last 7 days, the
last 30 days, and the last 12 months — anchored to the period end (else now), in
real date order. Replaces the cyclical hour-of-day/weekday/day-of-month/month
buckets, which left future days empty and ordered the weekend after Monday.
Backend adds cyclical distributions ByHour (hour-of-day), ByDOW (weekday), ByDOM
(day-of-month) and ByMonthYr (calendar month), all UTC. The Activity-over-time
card gains a selector: Timeline keeps the chronological month trend; Day/Week/
Month/Year show the hour/weekday/day/month distribution as bars.
Adds Stats.ByBandCategory (per band: CW/phone/data counts, band-plan order) via a
modeCategory() bucketing in stats.go, and a StackedBandBars chart in the Stats
panel showing the split per band with a legend.
MSHV/WSJT-X often stamp a personal STATION_CALLSIGN (e.g. F4BPO) that differs
from the special-event/contest profile you're running in OpsLog (e.g. TM74TFR).
The QSO then failed the extsvc station-callsign guard and was never uploaded to
the (matching) Club Log logbook. Live UDP logging now stamps the active profile's
callsign when the base call genuinely differs (portable variants left alone).
publishLiveStatus only ran on QSO log or the 15s heartbeat, and the widget polled
every 15s, so an operator's band/freq (and their appearance) could trail by up to
a minute. Now ReportLiveActivity debounce-publishes ~1.5s after a freq/band/mode
change, and the widget polls every 5s (and 1.5s after qso:logged).
The 15s poll effect depended on the dbConn OBJECT, which gets a fresh reference
on several events — each reset the interval before it fired, so the widget could
stay stale for over a minute. Depend on the is-mysql boolean instead, and also
refresh ~2s after qso:logged so our own row appears promptly like the ON AIR
badge.