From 19c2de5f61611cc93676466cb435e248de211300 Mon Sep 17 00:00:00 2001 From: rouggy Date: Thu, 9 Jul 2026 16:51:57 +0200 Subject: [PATCH] chore: release v0.19.2 --- app.go | 24 +++ frontend/src/components/FlexPanel.tsx | 214 +++++++++++++++++--------- frontend/src/lib/i18n.tsx | 4 +- frontend/src/version.ts | 2 +- frontend/wailsjs/go/main/App.d.ts | 8 + frontend/wailsjs/go/main/App.js | 16 ++ frontend/wailsjs/go/models.ts | 12 ++ internal/cat/cat.go | 11 ++ internal/cat/flex.go | 94 +++++++++++ telemetry.go | 2 +- wiki/Amplifiers-and-Switches.md | 23 +++ wiki/Audio-and-Keyers.md | 36 +++++ wiki/Awards.md | 47 ++++++ wiki/CAT-Control.md | 49 ++++++ wiki/Contest-Logging.md | 27 ++++ wiki/DX-Cluster-and-Spots.md | 40 +++++ wiki/FlexRadio.md | 54 +++++++ wiki/Getting-Started.md | 59 +++++++ wiki/Home.md | 41 +++++ wiki/Icom.md | 55 +++++++ wiki/Import-and-Export-ADIF.md | 32 ++++ wiki/Installation.md | 44 ++++++ wiki/Logging-Basics.md | 45 ++++++ wiki/Maps-and-Antennas.md | 38 +++++ wiki/Multi-Operator-Live-Status.md | 42 +++++ wiki/Net-Control.md | 30 ++++ wiki/Profiles-and-Databases.md | 35 +++++ wiki/QSL-Card-Designer.md | 35 +++++ wiki/QSL-Management.md | 44 ++++++ wiki/README-PUBLISHING.md | 45 ++++++ wiki/Recent-QSOs-and-Filters.md | 33 ++++ wiki/Remote-Icom-over-the-Internet.md | 55 +++++++ wiki/Security.md | 24 +++ wiki/Settings-and-Data.md | 51 ++++++ wiki/Troubleshooting.md | 58 +++++++ wiki/_Footer.md | 2 + wiki/_Sidebar.md | 37 +++++ 37 files changed, 1395 insertions(+), 73 deletions(-) create mode 100644 wiki/Amplifiers-and-Switches.md create mode 100644 wiki/Audio-and-Keyers.md create mode 100644 wiki/Awards.md create mode 100644 wiki/CAT-Control.md create mode 100644 wiki/Contest-Logging.md create mode 100644 wiki/DX-Cluster-and-Spots.md create mode 100644 wiki/FlexRadio.md create mode 100644 wiki/Getting-Started.md create mode 100644 wiki/Home.md create mode 100644 wiki/Icom.md create mode 100644 wiki/Import-and-Export-ADIF.md create mode 100644 wiki/Installation.md create mode 100644 wiki/Logging-Basics.md create mode 100644 wiki/Maps-and-Antennas.md create mode 100644 wiki/Multi-Operator-Live-Status.md create mode 100644 wiki/Net-Control.md create mode 100644 wiki/Profiles-and-Databases.md create mode 100644 wiki/QSL-Card-Designer.md create mode 100644 wiki/QSL-Management.md create mode 100644 wiki/README-PUBLISHING.md create mode 100644 wiki/Recent-QSOs-and-Filters.md create mode 100644 wiki/Remote-Icom-over-the-Internet.md create mode 100644 wiki/Security.md create mode 100644 wiki/Settings-and-Data.md create mode 100644 wiki/Troubleshooting.md create mode 100644 wiki/_Footer.md create mode 100644 wiki/_Sidebar.md diff --git a/app.go b/app.go index 278990b..334edba 100644 --- a/app.go +++ b/app.go @@ -8670,6 +8670,30 @@ func (a *App) FlexSetAPFLevel(l int) error { } return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetAPFLevel(l) }) } +func (a *App) FlexSetWNB(on bool) error { + if a.cat == nil { + return fmt.Errorf("cat not initialized") + } + return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetWNB(on) }) +} +func (a *App) FlexSetWNBLevel(l int) error { + if a.cat == nil { + return fmt.Errorf("cat not initialized") + } + return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetWNBLevel(l) }) +} +func (a *App) FlexSetTXFilter(low, high int) error { + if a.cat == nil { + return fmt.Errorf("cat not initialized") + } + return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetTXFilter(low, high) }) +} +func (a *App) FlexSetMicProfile(name string) error { + if a.cat == nil { + return fmt.Errorf("cat not initialized") + } + return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetMicProfile(name) }) +} func (a *App) FlexSetCWSpeed(wpm int) error { if a.cat == nil { return fmt.Errorf("cat not initialized") diff --git a/frontend/src/components/FlexPanel.tsx b/frontend/src/components/FlexPanel.tsx index 919da86..80704aa 100644 --- a/frontend/src/components/FlexPanel.tsx +++ b/frontend/src/components/FlexPanel.tsx @@ -7,6 +7,7 @@ import { GetPGXLStatus, PGXLSetFanMode, FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna, FlexSetSplit, FlexSetActiveSlice, FlexSetTXSlice, FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel, + FlexSetWNB, FlexSetWNBLevel, FlexSetTXFilter, FlexSetMicProfile, FlexSetAPF, FlexSetAPFLevel, FlexSetCWSpeed, FlexSetCWPitch, FlexSetCWBreakInDelay, FlexSetCWSidetone, FlexSetSidetoneLevel, FlexSetCWFilter, FlexSetFilter, } from '../../wailsjs/go/main/App'; @@ -25,6 +26,8 @@ type FlexState = { rx_ant?: string; tx_ant?: string; ant_list?: string[]; tx_ant_list?: string[]; split: boolean; rx_freq_hz?: number; tx_freq_hz?: number; nb: boolean; nb_level: number; nr: boolean; nr_level: number; anf: boolean; anf_level: number; + wnb: boolean; wnb_level: number; + tx_filter_low: number; tx_filter_high: number; mic_profile?: string; mic_profiles?: string[]; mode?: string; cw_speed: number; cw_pitch: number; cw_break_in_delay: number; cw_sidetone: boolean; cw_mon_level: number; apf: boolean; apf_level: number; filter_lo: number; filter_hi: number; @@ -42,6 +45,7 @@ const ZERO: FlexState = { mon: false, mon_level: 0, mic_level: 0, atu_memories: false, rx_avail: false, agc_threshold: 0, audio_level: 0, mute: false, split: false, nb: false, nb_level: 0, nr: false, nr_level: 0, anf: false, anf_level: 0, + wnb: false, wnb_level: 0, tx_filter_low: 0, tx_filter_high: 0, cw_speed: 25, cw_pitch: 600, cw_break_in_delay: 30, cw_sidetone: true, cw_mon_level: 0, apf: false, apf_level: 0, filter_lo: 0, filter_hi: 0, amp_available: false, amp_operate: false, @@ -143,27 +147,28 @@ function LevelRow({ label, on, onToggle, value, onLevel, disabled, accent, slide // `display` overrides the numeric readout; `segColor` colours segments by their // 0..1 position (zones); the top ~18% light red by default (overload/peak). const METER_SEGMENTS = 26; -function MeterBar({ label, value, unit, lo, hi, accent = '#16a34a', extra, display, segColor, onClick, title }: { +function MeterBar({ label, value, unit, lo, hi, accent = '#16a34a', extra, display, segColor, onClick, title, compact }: { label: string; value: number; unit?: string; lo: number; hi: number; accent?: string; extra?: string; display?: string; - segColor?: (frac: number) => string; onClick?: () => void; title?: string; + segColor?: (frac: number) => string; onClick?: () => void; title?: string; compact?: boolean; }) { const span = hi - lo; const pct = span > 0 ? Math.max(0, Math.min(100, ((value - lo) / span) * 100)) : 0; const lit = Math.round((pct / 100) * METER_SEGMENTS); return (
-
+
{label} - + {display !== undefined ? display : ( <>{Math.abs(value) >= 100 ? value.toFixed(0) : value.toFixed(1)}{unit} )}
{/* LED bar — recessed track + gradient segments for a cleaner instrument look. */} -
+
{Array.from({ length: METER_SEGMENTS }).map((_, i) => { const on = i < lit; const frac = i / METER_SEGMENTS; @@ -176,7 +181,7 @@ function MeterBar({ label, value, unit, lo, hi, accent = '#16a34a', extra, displ ); })}
- {extra &&
{extra}
} + {extra && !compact &&
{extra}
}
); } @@ -262,6 +267,13 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number ? SSB_BW.reduce((best, bw) => (Math.abs(bw - curBW) < Math.abs(best - curBW) ? bw : best), SSB_BW[0]) : -1; + // Compact header readouts for PA voltage + temperature (numbers, not bars) — the + // user wanted these in the header to save vertical space in the Meters card. + const hdrMeters = st.meters || []; + const hdrFind = (name: string) => hdrMeters.find((m) => (m.name || '').toUpperCase().includes(name) && !(m.src || '').toUpperCase().includes('AMP')); + const voltM = hdrFind('13.8') || hdrFind('VOLT'); + const patempM = hdrFind('PATEMP') || hdrFind('PA TEMP'); + return (
@@ -274,6 +286,23 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number {t('flxp.smartsdrRemote')}
+ {/* Compact PA voltage + temperature readouts (numbers) to save space. */} + {!off && (voltM || patempM) && ( +
+ {voltM && ( + + {t('flxp.voltage')} + {voltM.value.toFixed(1)}V + + )} + {patempM && ( + + {t('flxp.paTemp')} + = 70 ? 'text-red-400' : patempM.value >= 55 ? 'text-amber-400' : 'text-slate-100')}>{patempM.value.toFixed(0)}°C + + )} +
+ )} @@ -315,6 +344,58 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
)} + {/* Live meters (UDP VITA-49 stream) — placed right under the slices. */} + + {(() => { + const meters = st.meters || []; + if (off || meters.length === 0) { + return

{t('flxp.noMeters')}

; + } + const isDbm = (m?: Meter) => !!m && /dbm/i.test(m.unit || ''); + const dbmToW = (d: number) => Math.pow(10, (d - 30) / 10); + // Radio meters (exclude the amplifier's, which we show separately). + const radio = (name: string) => meters.find((m) => + (m.name || '').toUpperCase().includes(name) && !(m.src || '').toUpperCase().includes('AMP')); + const sig = radio('LEVEL') || radio('SIGNAL'); + const fwd = radio('FWDPWR'); + const swr = radio('SWR'); + // Mic input level + speech-compression (voltage & PA temp live in the + // header now). All already in the meter stream. + const mic = meters.find((m) => /MIC/i.test(m.name || '') && !(m.src || '').toUpperCase().includes('AMP')); + const comp = radio('COMPPEAK') || radio('COMP'); + // S-meter: dBm → S-units (S9 = -73 dBm on HF, 6 dB per unit). + const sUnit = (dbm: number) => { + const sv = (dbm + 127) / 6; // S0 = -127 dBm + if (sv >= 9) { + const over = Math.max(0, Math.round(dbm + 73)); // dB over S9 + return { display: over > 0 ? `S9+${over}` : 'S9', bar: sv, s: 9, over }; + } + return { display: `S${Math.max(0, Math.round(sv))}`, bar: Math.max(0, sv), s: Math.max(0, sv), over: 0 }; + }; + const cur = [ + sig && (() => { const dbm = peakHold('s', sig.value); const s = sUnit(dbm); return ( + onReportRST(sMeterRST(s.s, s.over, st.mode)) : undefined} + segColor={(fr) => { const sval = fr * 19; return sval < 9 ? '#16a34a' : sval < 12.33 ? '#f59e0b' : '#dc2626'; }} /> + ); })(), + fwd && (() => { const w = peakHold('p', isDbm(fwd) ? dbmToW(fwd.value) : fwd.value); return ( + + ); })(), + swr && , + // Mic input level: fixed -40..+10 dB scale, RED from 0 dB up (overdrive). + mic && (fr >= 0.8 ? '#dc2626' : fr >= 0.7 ? '#f59e0b' : '#16a34a')} />, + // Speech compression (dB of gain reduction). + comp && 0) ? comp.hi : 25} accent="#0891b2" />, + ].filter(Boolean); + return ( +
{cur}
+ ); + })()} +
+ {off && (
{t('flxp.waiting')} @@ -391,6 +472,39 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number change('mic_level', v, () => FlexSetMic(v))} /> {st.mic_level}
+ {/* TX audio bandwidth — low/high cut (Hz). Typed locally, committed on + blur/Enter so we don't spam a command per keystroke; the hold guard + keeps the poll from clobbering the field mid-edit. */} +
+ {t('flxp.txFilter')} + {(() => { + const editTX = (patch: Partial) => { hold.current['tx_filter_low'] = Date.now() + 8000; hold.current['tx_filter_high'] = Date.now() + 8000; setSt((p) => ({ ...p, ...patch })); }; + const commitTX = () => { hold.current['tx_filter_low'] = Date.now() + 900; hold.current['tx_filter_high'] = Date.now() + 900; FlexSetTXFilter(st.tx_filter_low || 0, st.tx_filter_high || 0).catch(() => {}); }; + const inp = 'w-[70px] h-8 rounded-md border border-input bg-background px-2 text-xs font-mono tabular-nums disabled:opacity-40'; + return (<> + editTX({ tx_filter_low: parseInt(e.target.value, 10) || 0 })} + onBlur={commitTX} onKeyDown={(e) => { if (e.key === 'Enter') { (e.target as HTMLInputElement).blur(); } }} /> + + editTX({ tx_filter_high: parseInt(e.target.value, 10) || 0 })} + onBlur={commitTX} onKeyDown={(e) => { if (e.key === 'Enter') { (e.target as HTMLInputElement).blur(); } }} /> + Hz + ); + })()} +
+ {/* Mic profile — SmartSDR named mic-EQ/processor presets. */} + {!!st.mic_profiles && st.mic_profiles.length > 0 && ( +
+ {t('flxp.micProfile')} + +
+ )}
) : ( /* CW keyer controls (replace VOX/PROC/MIC when the slice is in CW). */ @@ -464,6 +578,10 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number change('nb', !st.nb, () => FlexSetNB(!st.nb))} onLevel={(v) => change('nb_level', v, () => FlexSetNBLevel(v))} /> + {/* WNB — wideband noise blanker (the extra hardware NR the Flex has). */} + change('wnb', !st.wnb, () => FlexSetWNB(!st.wnb))} + onLevel={(v) => change('wnb_level', v, () => FlexSetWNBLevel(v))} /> change('nr', !st.nr, () => FlexSetNR(!st.nr))} onLevel={(v) => change('nr_level', v, () => FlexSetNRLevel(v))} /> @@ -560,71 +678,29 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number {t('flxp.fault')}: {st.amp_fault} )}
+ {/* Amplifier meters (FWD / ID / TEMP) — moved here from the Meters card + and shown compact so they sit with the amp controls. */} + {(() => { + const meters = st.meters || []; + const dbmToW = (d: number) => Math.pow(10, (d - 30) / 10); + const amp = meters.filter((m) => (m.src || '').toUpperCase().includes('AMP') + && !/^(RL|DRV)$/i.test((m.name || '').trim())); + if (off || amp.length === 0) return null; + return ( +
+ {amp.map((m) => { + if (/fwd|pwr/i.test(m.name || '') && /dbm/i.test(m.unit || '')) { + return ; + } + const acc = /temp|degc|degf/i.test(`${m.unit}${m.name}`) ? '#ea580c' : /volt/i.test(m.unit || '') ? '#2563eb' : '#16a34a'; + return ; + })} +
+ ); + })()} )} - {/* Live meters (UDP VITA-49 stream) */} - - {(() => { - const meters = st.meters || []; - if (off || meters.length === 0) { - return

{t('flxp.noMeters')}

; - } - const isDbm = (m?: Meter) => !!m && /dbm/i.test(m.unit || ''); - const dbmToW = (d: number) => Math.pow(10, (d - 30) / 10); - // Radio meters (exclude the amplifier's, which we show separately). - const radio = (name: string) => meters.find((m) => - (m.name || '').toUpperCase().includes(name) && !(m.src || '').toUpperCase().includes('AMP')); - const sig = radio('LEVEL') || radio('SIGNAL'); - const fwd = radio('FWDPWR'); - const swr = radio('SWR'); - const amp = meters.filter((m) => (m.src || '').toUpperCase().includes('AMP') - && !/^(RL|DRV)$/i.test((m.name || '').trim())); - const accentFor = (m: Meter) => /swr/i.test(`${m.unit}${m.name}`) ? '#dc2626' - : /temp|degc|degf/i.test(`${m.unit}${m.name}`) ? '#ea580c' - : /volt/i.test(m.unit || '') ? '#2563eb' : '#16a34a'; - // S-meter: dBm → S-units (S9 = -73 dBm on HF, 6 dB per unit). - const sUnit = (dbm: number) => { - const sv = (dbm + 127) / 6; // S0 = -127 dBm - if (sv >= 9) { - const over = Math.max(0, Math.round(dbm + 73)); // dB over S9 - return { display: over > 0 ? `S9+${over}` : 'S9', bar: sv, s: 9, over }; - } - return { display: `S${Math.max(0, Math.round(sv))}`, bar: Math.max(0, sv), s: Math.max(0, sv), over: 0 }; - }; - const cur = [ - sig && (() => { const dbm = peakHold('s', sig.value); const s = sUnit(dbm); return ( - onReportRST(sMeterRST(s.s, s.over, st.mode)) : undefined} - segColor={(fr) => { const sval = fr * 19; return sval < 9 ? '#16a34a' : sval < 12.33 ? '#f59e0b' : '#dc2626'; }} /> - ); })(), - fwd && (() => { const w = peakHold('p', isDbm(fwd) ? dbmToW(fwd.value) : fwd.value); return ( - - ); })(), - swr && , - ].filter(Boolean); - return ( - <> -
{cur}
- {amp.length > 0 && ( -
-
{t('flxp.amplifierHdr')}
-
- {amp.map((m) => { - if (/fwd|pwr/i.test(m.name || '') && isDbm(m)) { - return ; - } - return ; - })} -
-
- )} - - ); - })()} -
); diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 3969f21..6958a70 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -195,7 +195,7 @@ const en: Dict = { 'dvkp.voiceKeyer': 'Voice keyer', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Disable voice keyer', 'dvkp.noMsgPre': 'No messages recorded yet. Open', 'dvkp.settingsPath': 'Settings → Audio devices & voice keyer', 'dvkp.noMsgPost': 'to record F1–F6.', 'dvkp.transmit': 'Transmit F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — empty', 'dvkp.message': 'message', 'agp.portDeselect': 'Port {letter} — click to deselect', 'agp.portSelect': 'Select on port {letter}', 'agp.online': 'online', 'agp.offline': 'offline', 'agp.close': 'Close', 'agp.connecting': 'Connecting…', 'agp.noAntennas': 'No antennas configured.', 'agp.filterOnHint': 'Showing antennas for {band} only — click to show all bands', 'agp.filterOffHint': 'Showing all antennas — click to show only the current band', 'flxp.smartsdrRemote': 'SmartSDR remote control', 'flxp.offline': 'OFFLINE', 'flxp.waiting': 'Waiting for the FlexRadio… (set CAT to FlexRadio and connect)', 'flxp.transmit': 'Transmit', 'flxp.rfPower': 'RF Power', 'flxp.tunePwr': 'Tune Pwr', 'flxp.splitHint': 'Split: RX/TX on separate slices. ON creates a TX slice +1 kHz (CW) / +5 kHz (SSB) up, like SmartSDR.', 'flxp.sliceHint': 'Click to make this the active slice — frequency, mode, DSP and spot-clicks all follow it.', 'flxp.txSlice': 'This slice transmits', 'flxp.setTxSlice': 'Move TX to this slice (transmit here)', 'flxp.voxDly': 'VOX Dly', 'flxp.speed': 'Speed', 'flxp.pitch': 'Pitch', 'flxp.delay': 'Delay', - 'flxp.receiveActive': 'Receive (active slice)', 'flxp.muted': 'Muted — click to unmute', 'flxp.mute': 'Mute RX audio', 'flxp.filter': 'Filter', 'flxp.amplifier': 'Amplifier', 'flxp.ampInLine': 'Amplifier is in line (transmitting through PA).', 'flxp.ampBypassed': 'Amplifier bypassed (standby).', 'flxp.pgConnected': 'PowerGenius connected', 'flxp.pgOffline': 'PowerGenius offline', 'flxp.fan': 'Fan', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Broadcast', 'flxp.fault': 'FAULT', 'flxp.meters': 'Meters', 'flxp.noMeters': "No meters yet — waiting for the radio's UDP stream…", 'flxp.amplifierHdr': 'AMPLIFIER', + 'flxp.receiveActive': 'Receive (active slice)', 'flxp.muted': 'Muted — click to unmute', 'flxp.mute': 'Mute RX audio', 'flxp.filter': 'Filter', 'flxp.amplifier': 'Amplifier', 'flxp.ampInLine': 'Amplifier is in line (transmitting through PA).', 'flxp.ampBypassed': 'Amplifier bypassed (standby).', 'flxp.pgConnected': 'PowerGenius connected', 'flxp.pgOffline': 'PowerGenius offline', 'flxp.fan': 'Fan', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Broadcast', 'flxp.fault': 'FAULT', 'flxp.meters': 'Meters', 'flxp.voltage': 'VOLTAGE', 'flxp.paTemp': 'PA TEMP', 'flxp.txFilter': 'TX filter', 'flxp.micProfile': 'Mic profile', 'flxp.noMeters': "No meters yet — waiting for the radio's UDP stream…", 'flxp.amplifierHdr': 'AMPLIFIER', 'icmp.spectrum': 'Spectrum', 'icmp.scopeFixed': 'Fixed — double-click / wheel to tune', 'icmp.scopeCenter': 'Center — follows VFO', 'icmp.scopeOff': 'Scope off', 'icmp.scopePanDown': 'Shift scope −50 kHz', 'icmp.scopePanUp': 'Shift scope +50 kHz', 'icmp.scopeCenterVfo': 'Center scope on the current frequency (±50 kHz)', 'icmp.notConnected': "Icom not connected. Enable the Icom CI-V backend in Settings → CAT and connect the radio's USB port.", 'icmp.refresh': 'Refresh', 'icmp.meters': 'Meters', 'icmp.transmit': 'Transmit', 'icmp.power': 'Power', 'icmp.mic': 'Mic', 'icmp.receive': 'Receive', 'icmp.preamp': 'Preamp', 'icmp.filter': 'Filter', 'icmp.noiseNotch': 'Noise / Notch', 'icmp.autoNotch': 'Auto notch filter', 'icmp.apf': 'Audio peak filter (CW)', 'icmp.clarifiers': 'RIT / ΔTX', 'icmp.ritHint': 'Wheel or ± to shift · Ctrl+←/→ shifts RIT when active', 'icmp.bandsAntenna': 'Bands & Antenna', 'icmp.antenna': 'Antenna', 'icmp.passband': 'Passband / Notch', 'icmp.pbtCenter': 'Center PBT', 'icmp.manualNotch': 'Manual notch — MN on, then set position', 'icmp.squelch': 'Squelch', 'icmp.powerOnHint': 'Power the radio ON (boots ~15 s)', 'icmp.powerOffHint': 'Power the radio OFF', 'icmp.powerOffConfirm': 'Switch the radio OFF?', 'rst.clickToFill': 'Click to set RST tx from the signal', 'qrz.openTitle': 'Open {call} on QRZ.com', @@ -384,7 +384,7 @@ const fr: Dict = { 'dvkp.voiceKeyer': 'Manipulateur vocal', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Désactiver le manipulateur vocal', 'dvkp.noMsgPre': 'Aucun message enregistré. Ouvre', 'dvkp.settingsPath': 'Réglages → Périphériques audio & manipulateur vocal', 'dvkp.noMsgPost': 'pour enregistrer F1–F6.', 'dvkp.transmit': 'Émettre F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — vide', 'dvkp.message': 'message', 'agp.portDeselect': 'Port {letter} — clic pour désélectionner', 'agp.portSelect': 'Sélectionner sur le port {letter}', 'agp.online': 'en ligne', 'agp.offline': 'hors ligne', 'agp.close': 'Fermer', 'agp.connecting': 'Connexion…', 'agp.noAntennas': 'Aucune antenne configurée.', 'agp.filterOnHint': 'Antennes du {band} uniquement — clic pour afficher toutes les bandes', 'agp.filterOffHint': 'Toutes les antennes affichées — clic pour n’afficher que la bande courante', 'flxp.smartsdrRemote': 'Contrôle à distance SmartSDR', 'flxp.offline': 'HORS LIGNE', 'flxp.waiting': 'En attente du FlexRadio… (règle le CAT sur FlexRadio et connecte)', 'flxp.transmit': 'Émission', 'flxp.rfPower': 'Puissance RF', 'flxp.tunePwr': 'Puiss. TUNE', 'flxp.splitHint': 'Split : RX/TX sur des slices séparées. ON crée une slice TX +1 kHz (CW) / +5 kHz (SSB) au-dessus, comme SmartSDR.', 'flxp.sliceHint': 'Cliquer pour rendre cette slice active — fréquence, mode, DSP et clics de spot la suivent tous.', 'flxp.txSlice': 'Cette slice émet', 'flxp.setTxSlice': 'Déplacer le TX sur cette slice (émettre ici)', 'flxp.voxDly': 'Délai VOX', 'flxp.speed': 'Vitesse', 'flxp.pitch': 'Tonalité', 'flxp.delay': 'Délai', - 'flxp.receiveActive': 'Réception (slice active)', 'flxp.muted': 'Coupé — clic pour rétablir', 'flxp.mute': "Couper l'audio RX", 'flxp.filter': 'Filtre', 'flxp.amplifier': 'Amplificateur', 'flxp.ampInLine': 'Amplificateur en ligne (émission via le PA).', 'flxp.ampBypassed': 'Amplificateur en bypass (standby).', 'flxp.pgConnected': 'PowerGenius connecté', 'flxp.pgOffline': 'PowerGenius hors ligne', 'flxp.fan': 'Ventilo', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Diffusion', 'flxp.fault': 'DÉFAUT', 'flxp.meters': 'Mesures', 'flxp.noMeters': 'Aucune mesure — en attente du flux UDP de la radio…', 'flxp.amplifierHdr': 'AMPLIFICATEUR', + 'flxp.receiveActive': 'Réception (slice active)', 'flxp.muted': 'Coupé — clic pour rétablir', 'flxp.mute': "Couper l'audio RX", 'flxp.filter': 'Filtre', 'flxp.amplifier': 'Amplificateur', 'flxp.ampInLine': 'Amplificateur en ligne (émission via le PA).', 'flxp.ampBypassed': 'Amplificateur en bypass (standby).', 'flxp.pgConnected': 'PowerGenius connecté', 'flxp.pgOffline': 'PowerGenius hors ligne', 'flxp.fan': 'Ventilo', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Diffusion', 'flxp.fault': 'DÉFAUT', 'flxp.meters': 'Mesures', 'flxp.voltage': 'TENSION', 'flxp.paTemp': 'TEMP PA', 'flxp.txFilter': 'Filtre TX', 'flxp.micProfile': 'Profil micro', 'flxp.noMeters': 'Aucune mesure — en attente du flux UDP de la radio…', 'flxp.amplifierHdr': 'AMPLIFICATEUR', 'icmp.spectrum': 'Spectre', 'icmp.scopeFixed': 'Fixe — double-clic / molette pour accorder', 'icmp.scopeCenter': 'Centré — suit le VFO', 'icmp.scopeOff': 'Scope éteint', 'icmp.scopePanDown': 'Décaler le scope −50 kHz', 'icmp.scopePanUp': 'Décaler le scope +50 kHz', 'icmp.scopeCenterVfo': 'Centrer le scope sur la fréquence actuelle (±50 kHz)', 'icmp.notConnected': 'Icom non connecté. Active le backend CI-V Icom dans Réglages → CAT et connecte le port USB de la radio.', 'icmp.refresh': 'Rafraîchir', 'icmp.meters': 'Mesures', 'icmp.transmit': 'Émission', 'icmp.power': 'Puissance', 'icmp.mic': 'Micro', 'icmp.receive': 'Réception', 'icmp.preamp': 'Préampli', 'icmp.filter': 'Filtre', 'icmp.noiseNotch': 'Bruit / Notch', 'icmp.autoNotch': 'Filtre notch auto', 'icmp.apf': 'Filtre de pic audio (CW)', 'icmp.clarifiers': 'RIT / ΔTX', 'icmp.ritHint': 'Molette ou ± pour décaler · Ctrl+←/→ décale le RIT si actif', 'icmp.bandsAntenna': 'Bandes & Antenne', 'icmp.antenna': 'Antenne', 'icmp.passband': 'Passe-bande / Notch', 'icmp.pbtCenter': 'Centrer PBT', 'icmp.manualNotch': 'Notch manuel — active MN, puis règle la position', 'icmp.squelch': 'Squelch', 'icmp.powerOnHint': 'Allumer la radio (démarre en ~15 s)', 'icmp.powerOffHint': 'Éteindre la radio', 'icmp.powerOffConfirm': 'Éteindre la radio ?', 'rst.clickToFill': 'Clic pour remplir le RST tx depuis le signal', 'qrz.openTitle': 'Ouvrir {call} sur QRZ.com', diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 7b7e42b..9c54817 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1,6 +1,6 @@ // Single source of truth for the app version shown in the UI (header + About). // Bump this on a release (the release script updates it alongside telemetry.go). -export const APP_VERSION = '0.19.1'; +export const APP_VERSION = '0.19.2'; // Author / credits, shown in Help -> About. export const APP_AUTHOR = 'F4BPO'; diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 94a9fbb..b2a1118 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -202,6 +202,8 @@ export function FlexSetFilter(arg1:number,arg2:number):Promise; export function FlexSetMic(arg1:number):Promise; +export function FlexSetMicProfile(arg1:string):Promise; + export function FlexSetMon(arg1:boolean):Promise; export function FlexSetMonLevel(arg1:number):Promise; @@ -230,6 +232,8 @@ export function FlexSetSplit(arg1:boolean):Promise; export function FlexSetTXAntenna(arg1:string):Promise; +export function FlexSetTXFilter(arg1:number,arg2:number):Promise; + export function FlexSetTXSlice(arg1:number):Promise; export function FlexSetTunePower(arg1:number):Promise; @@ -240,6 +244,10 @@ export function FlexSetVoxDelay(arg1:number):Promise; export function FlexSetVoxLevel(arg1:number):Promise; +export function FlexSetWNB(arg1:boolean):Promise; + +export function FlexSetWNBLevel(arg1:number):Promise; + export function FlexTune(arg1:boolean):Promise; export function GetActiveProfile():Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 0132188..cff577d 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -366,6 +366,10 @@ export function FlexSetMic(arg1) { return window['go']['main']['App']['FlexSetMic'](arg1); } +export function FlexSetMicProfile(arg1) { + return window['go']['main']['App']['FlexSetMicProfile'](arg1); +} + export function FlexSetMon(arg1) { return window['go']['main']['App']['FlexSetMon'](arg1); } @@ -422,6 +426,10 @@ export function FlexSetTXAntenna(arg1) { return window['go']['main']['App']['FlexSetTXAntenna'](arg1); } +export function FlexSetTXFilter(arg1, arg2) { + return window['go']['main']['App']['FlexSetTXFilter'](arg1, arg2); +} + export function FlexSetTXSlice(arg1) { return window['go']['main']['App']['FlexSetTXSlice'](arg1); } @@ -442,6 +450,14 @@ export function FlexSetVoxLevel(arg1) { return window['go']['main']['App']['FlexSetVoxLevel'](arg1); } +export function FlexSetWNB(arg1) { + return window['go']['main']['App']['FlexSetWNB'](arg1); +} + +export function FlexSetWNBLevel(arg1) { + return window['go']['main']['App']['FlexSetWNBLevel'](arg1); +} + export function FlexTune(arg1) { return window['go']['main']['App']['FlexTune'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 92f6f14..2ef98b3 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -585,6 +585,10 @@ export namespace cat { mon: boolean; mon_level: number; mic_level: number; + tx_filter_low: number; + tx_filter_high: number; + mic_profile?: string; + mic_profiles?: string[]; atu_status?: string; atu_memories: boolean; rx_avail: boolean; @@ -605,6 +609,8 @@ export namespace cat { nr_level: number; anf: boolean; anf_level: number; + wnb: boolean; + wnb_level: number; mode?: string; cw_speed: number; cw_pitch: number; @@ -642,6 +648,10 @@ export namespace cat { this.mon = source["mon"]; this.mon_level = source["mon_level"]; this.mic_level = source["mic_level"]; + this.tx_filter_low = source["tx_filter_low"]; + this.tx_filter_high = source["tx_filter_high"]; + this.mic_profile = source["mic_profile"]; + this.mic_profiles = source["mic_profiles"]; this.atu_status = source["atu_status"]; this.atu_memories = source["atu_memories"]; this.rx_avail = source["rx_avail"]; @@ -662,6 +672,8 @@ export namespace cat { this.nr_level = source["nr_level"]; this.anf = source["anf"]; this.anf_level = source["anf_level"]; + this.wnb = source["wnb"]; + this.wnb_level = source["wnb_level"]; this.mode = source["mode"]; this.cw_speed = source["cw_speed"]; this.cw_pitch = source["cw_pitch"]; diff --git a/internal/cat/cat.go b/internal/cat/cat.go index 9cec63c..976aa43 100644 --- a/internal/cat/cat.go +++ b/internal/cat/cat.go @@ -275,6 +275,11 @@ type FlexTXState struct { Mon bool `json:"mon"` MonLevel int `json:"mon_level"` MicLevel int `json:"mic_level"` + TXFilterLow int `json:"tx_filter_low"` // TX filter low cut (Hz) + TXFilterHigh int `json:"tx_filter_high"` // TX filter high cut (Hz) + // Mic profiles (SmartSDR): the list of available profiles + the loaded one. + MicProfile string `json:"mic_profile,omitempty"` + MicProfiles []string `json:"mic_profiles,omitempty"` ATUStatus string `json:"atu_status,omitempty"` ATUMemories bool `json:"atu_memories"` // Active RX slice DSP controls. @@ -296,6 +301,8 @@ type FlexTXState struct { NRLevel int `json:"nr_level"` ANF bool `json:"anf"` ANFLevel int `json:"anf_level"` + WNB bool `json:"wnb"` + WNBLevel int `json:"wnb_level"` // CW / mode-specific controls. Mode string `json:"mode,omitempty"` // active slice mode (CW/USB/LSB/DIGU…) CWSpeed int `json:"cw_speed"` @@ -344,6 +351,8 @@ type FlexController interface { SetMon(bool) error SetMonLevel(int) error SetMic(int) error + SetTXFilter(low, high int) error // transmit-audio bandwidth (Hz) + SetMicProfile(string) error // load a SmartSDR mic profile by name ATUStart() error ATUBypass() error SetATUMemories(bool) error @@ -365,6 +374,8 @@ type FlexController interface { SetANFLevel(int) error SetAPF(bool) error SetAPFLevel(int) error + SetWNB(bool) error + SetWNBLevel(int) error // CW keyer + mode-specific controls. SetCWSpeed(int) error SetCWPitch(int) error diff --git a/internal/cat/flex.go b/internal/cat/flex.go index de47c08..85ba838 100644 --- a/internal/cat/flex.go +++ b/internal/cat/flex.go @@ -38,6 +38,8 @@ type Flex struct { slices map[int]*flexSlice tx flexTX // transmit/ATU state pushed by the radio (FlexRadio tab) amp flexAmp // external amplifier (PowerGenius XL) state + micProfiles []string // available mic profiles (SmartSDR "profile mic list") + micProfile string // currently loaded mic profile txSetAt map[string]time.Time // status field → when WE last set it (ignore the radio's lagging echo briefly) lastStateSig string // last logged derived-state signature (log only on change) boundClientID string // GUI client (SmartSDR) we bound to; "" until bound. Binding lets this non-GUI client receive GUI-tied data (CW pitch/speed, break-in delay, RF power). @@ -85,6 +87,8 @@ type flexSlice struct { anfLevel int apf bool // CW audio peaking filter apfLevel int + wnb bool // wideband noise blanker + wnbLevel int filterLo int // slice filter low cut (Hz) filterHi int // slice filter high cut (Hz) rxAnt string // selected RX antenna (e.g. ANT1, ANT2, RX_A) @@ -108,6 +112,8 @@ type flexTX struct { mon bool monLevel int micLevel int + filterLow int // TX filter low cut (Hz) + filterHigh int // TX filter high cut (Hz) atuStatus string atuMemories bool // CW keyer params (set via the top-level "cw" commands). @@ -209,6 +215,8 @@ func (f *Flex) Connect() error { f.send("sub amplifier all") // external amplifier (PowerGenius XL) operate/standby f.send("sub radio all") // radio-wide incl. interlock (TX/RX state) f.send("sub cwx all") // CWX: the LIVE CW speed/pitch/break-in (transmit holds only a static default) + f.send("sub profile all") // mic/global/tx profiles (for the mic-profile dropdown) + f.send("profile mic info") // request the current mic profile list + selection f.send("sub client all") // learn the GUI client (SmartSDR) so we can bind to it (below) f.startMeters(conn) // open the UDP VITA-49 stream for live meters if f.spotsEnabled { @@ -445,6 +453,12 @@ func (f *Flex) handleStatus(payload string) { f.tx.cwBreakInDelay = atoiDefault(val, f.tx.cwBreakInDelay) case "mic_level", "miclevel": f.tx.micLevel = atoiDefault(val, f.tx.micLevel) + // TX filter: the transmit STATUS reports the passband as lo/hi (the + // SET command is filter_low/filter_high — a SmartSDR quirk). + case "lo", "filter_low": + f.tx.filterLow = atoiDefault(val, f.tx.filterLow) + case "hi", "filter_high": + f.tx.filterHigh = atoiDefault(val, f.tx.filterHigh) } } f.mu.Unlock() @@ -526,6 +540,35 @@ func (f *Flex) handleStatus(payload string) { } f.mu.Unlock() } + // Mic-profile object — "profile mic list=A^B^C" (available profiles) and + // "profile mic current=" (loaded one). Names can contain spaces, so + // values are taken from the raw payload after the key. Logged once so the + // exact field names are confirmable on real hardware. + if len(fields) >= 2 && fields[0] == "profile" && fields[1] == "mic" { + debugLog.Printf("Flex: profile status: %s", payload) + if i := strings.Index(payload, "list="); i >= 0 { + var profs []string + for _, p := range strings.Split(payload[i+len("list="):], "^") { + if p = strings.TrimSpace(p); p != "" { + profs = append(profs, p) + } + } + f.mu.Lock() + f.micProfiles = profs + f.mu.Unlock() + } + // The loaded profile arrives as current= (some firmwares: + // selection=); accept either. + for _, key := range []string{"current=", "selection="} { + if i := strings.Index(payload, key); i >= 0 { + cur := strings.TrimSpace(payload[i+len(key):]) + f.mu.Lock() + f.micProfile = cur + f.mu.Unlock() + break + } + } + } // Interlock object — transmit state (RECEIVE / TRANSMITTING / …). if len(fields) >= 1 && fields[0] == "interlock" { f.mu.Lock() @@ -744,6 +787,10 @@ func (f *Flex) handleStatus(payload string) { s.apf = val == "1" case "apf_level": s.apfLevel = atoiDefault(val, s.apfLevel) + case "wnb": + s.wnb = val == "1" + case "wnb_level": + s.wnbLevel = atoiDefault(val, s.wnbLevel) case "filter_lo": s.filterLo = atoiDefault(val, s.filterLo) case "filter_hi": @@ -1164,6 +1211,10 @@ func (f *Flex) FlexState() FlexTXState { Mon: f.tx.mon, MonLevel: f.tx.monLevel, MicLevel: f.tx.micLevel, + TXFilterLow: f.tx.filterLow, + TXFilterHigh: f.tx.filterHigh, + MicProfile: f.micProfile, + MicProfiles: f.micProfiles, ATUStatus: f.tx.atuStatus, ATUMemories: f.tx.atuMemories, // CW keyer (defaults applied so the sliders show sane values pre-read). @@ -1214,6 +1265,8 @@ func (f *Flex) FlexState() FlexTXState { st.ANFLevel = rx.anfLevel st.APF = rx.apf st.APFLevel = rx.apfLevel + st.WNB = rx.wnb + st.WNBLevel = rx.wnbLevel st.FilterLo = rx.filterLo st.FilterHi = rx.filterHi st.RXAnt = rx.rxAnt @@ -1391,6 +1444,8 @@ func (f *Flex) SetANF(on bool) error { return f.sendSlice("anf", boolFlex(on) func (f *Flex) SetANFLevel(l int) error { return f.sendSlice("anf_level", clampLevel(l)) } func (f *Flex) SetAPF(on bool) error { return f.sendSlice("apf", boolFlex(on)) } func (f *Flex) SetAPFLevel(l int) error { return f.sendSlice("apf_level", clampLevel(l)) } +func (f *Flex) SetWNB(on bool) error { return f.sendSlice("wnb", boolFlex(on)) } +func (f *Flex) SetWNBLevel(l int) error { return f.sendSlice("wnb_level", clampLevel(l)) } // ── CW keyer controls (top-level "cw" commands) ── @@ -1608,6 +1663,45 @@ func (f *Flex) SetMic(l int) error { return f.txSet(fmt.Sprintf("transmit set miclevel=%d", l), "mic_level", func(t *flexTX) { t.micLevel = l }) } +// SetTXFilter sets the transmit-audio bandwidth (low + high cut, in Hz). SmartSDR +// clamps to legal values per mode; we just pass them through in one command so +// both edges move together. +func (f *Flex) SetTXFilter(low, high int) error { + if low < 0 { + low = 0 + } + if high < low { + high = low + } + // Guard both status field names (the echo comes back as lo/hi, not + // filter_low/high) so the radio's lagging echo doesn't snap the inputs back. + f.mu.Lock() + f.txSetAt["hi"] = time.Now() + f.mu.Unlock() + return f.txSet(fmt.Sprintf("transmit set filter_low=%d filter_high=%d", low, high), "lo", + func(t *flexTX) { t.filterLow, t.filterHigh = low, high }) +} + +// SetMicProfile loads a SmartSDR mic profile by name (quoted, as names may hold +// spaces). Optimistically caches the selection. +func (f *Flex) SetMicProfile(name string) error { + name = strings.TrimSpace(name) + if name == "" { + return fmt.Errorf("flex: empty mic profile") + } + f.mu.Lock() + connected := f.conn != nil + if connected { + f.micProfile = name + } + f.mu.Unlock() + if !connected { + return fmt.Errorf("flex: not connected") + } + f.send(fmt.Sprintf("profile mic load \"%s\"", name)) + return nil +} + func (f *Flex) ATUStart() error { if !f.connected() { return fmt.Errorf("flex: not connected") diff --git a/telemetry.go b/telemetry.go index 4c527fb..e9a0faa 100644 --- a/telemetry.go +++ b/telemetry.go @@ -21,7 +21,7 @@ import ( const ( // appVersion is stamped on every heartbeat (and could feed the About box). - appVersion = "0.19.1" + appVersion = "0.19.2" // posthogHost is the PostHog ingestion endpoint. EU cloud by default; change // to https://us.i.posthog.com for a US project. diff --git a/wiki/Amplifiers-and-Switches.md b/wiki/Amplifiers-and-Switches.md new file mode 100644 index 0000000..5ede70a --- /dev/null +++ b/wiki/Amplifiers-and-Switches.md @@ -0,0 +1,23 @@ +# Amplifiers and Switches + +## PowerGenius XL (4O3A) amplifier + +Direct TCP connection (Settings → PowerGenius): + +- **Operate / Standby** and **fault** display. +- **Fan-mode** selector (Standard / Contest / Broadcast). +- When a Flex is connected, the amp controls + its **FWD / ID / TEMP** meters + appear in the FlexRadio panel's Amplifier card. See [[FlexRadio]]. + +## Antenna Genius (4O3A) antenna switch + +Over TCP / GSCP — a docked **A/B antenna-switch** widget: + +- One row per configured antenna, with a **Port A** and **Port B** button. +- Colours: green = selected on port A, blue = selected on port B, red (pulsing) = + that port is transmitting. Clicking a selected port deselects it (→ None). +- **Band filter** (funnel icon in the header): shows only the antennas configured + for the current band, like the native app. Toggle it off to see all antennas. + Antennas with an unknown band mask are always shown. + +Configure the host / password in Settings → Antenna Genius. diff --git a/wiki/Audio-and-Keyers.md b/wiki/Audio-and-Keyers.md new file mode 100644 index 0000000..a4f7f89 --- /dev/null +++ b/wiki/Audio-and-Keyers.md @@ -0,0 +1,36 @@ +# Audio and Keyers + +## CW keyer + +A CW keyer with **macros** and F-key macros. The keyer **engine** is selectable +(Settings → CW Keyer): + +- **WinKeyer** — K1EL WK1/2/3 over a COM port. +- **Icom** — the radio's own keyer over CI-V, no extra hardware. Works over the + remote link too. (Requires the Icom CAT backend; set break-in to SEMI/FULL or + the rig keys the sidetone but stays in RX.) +- **TCI** — for TCI backends. + +## Digital Voice Keyer (DVK) + +Record **F1–F6** voice messages and transmit them. Set the audio devices, PTT +method and gains in Settings → Audio. + +- **PTT** method: CAT, RTS, DTR, or none (VOX). +- **Test PTT** keys the rig briefly. On CAT/OmniRig the key is held ~1.5 s so the + transmit command is actually sent (OmniRig is asynchronous and coalesces rapid + writes). + +## QSO audio recording + +Continuous rolling capture. On **Log QSO** the contact is saved to a per-QSO WAV +(`CALL_YYYYMMDD_HHMMSS.wav`), mixing RX + mic. Configure the recordings folder, +format (WAV / MP3), pre-roll and gains in Settings → Audio. + +## RX audio monitor (USB rigs) + +Settings → Audio → **Listen to radio** pipes the rig's received audio (its "USB +Audio CODEC" input) through your speakers, so you hear the radio inside OpsLog. +**Talk to radio** keys PTT and pipes your mic to the rig's audio input. (Network +audio for remote Icom is a work in progress — see +[[Remote Icom over the Internet]].) diff --git a/wiki/Awards.md b/wiki/Awards.md new file mode 100644 index 0000000..0b557ba --- /dev/null +++ b/wiki/Awards.md @@ -0,0 +1,47 @@ +# Awards + +OpsLog has a flexible awards engine. Award **definitions** and reference lists are +shared **globally** across profiles. + +## Built-in awards + +DXCC, WAS / WAZ / WAC, WPX, IOTA / POTA / SOTA / WWFF, **DDFM** (French +departments), and more — tracked **worked / confirmed / validated** by band and +mode. + +## How matching works + +An award scans a **QSO field** for a reference, by one of: + +- **code** — the field value is the reference (e.g. state = `NY`). +- **description** — the reference's name appears in the field (substring, case- + insensitive but **space-sensitive** — "Hongkong" won't match "Hong Kong"). +- **pattern** — a per-reference regex is tested against the field. + +Plus **OR rules** (also match if any of these hit), a **leading/trailing** strip, +a **prefix** added to found references, and a **dynamic** mode (any value counts, +like POTA). + +> **Tip — Hong Kong / Macau style mismatches:** if the data says "Hong Kong" but +> the reference description is "Hongkong", either fix the description to match, or +> set that OR-search's *Match by* to **pattern** so the reference's own regex +> (e.g. `\bHong Kong\b`) is used. In *description* mode the per-reference pattern +> is ignored. + +## Live detection & manual refs + +- References are detected **live** as you enter a callsign. +- You can **manually assign** a reference to a QSO (stored in an ADIF extra so it + survives export/import — see [[Import and Export ADIF]]). + +## Reference lists & display + +- **Import reference lists** for totals and names. +- Per award, choose what the Recent-QSOs column shows: **reference**, **name**, + or **both**. Award columns are opt-in per the Columns picker + ([[Recent QSOs and Filters]]). + +## Rescan + +**Rescan** re-pulls the logbook and recomputes — it picks up fresh LoTW / QRZ / +eQSL confirmations (see [[QSL Management]]). diff --git a/wiki/CAT-Control.md b/wiki/CAT-Control.md new file mode 100644 index 0000000..2370366 --- /dev/null +++ b/wiki/CAT-Control.md @@ -0,0 +1,49 @@ +# CAT Control + +OpsLog has **four native CAT backends** (Settings → CAT). Each auto-reconnects +and connects *non-blocking*, so a powered-off radio never freezes the app. + +| Backend | Use it for | +|---------|-----------| +| **OmniRig** | Any OmniRig-supported rig (Rig 1 / Rig 2, hot-swap). | +| **FlexRadio (SmartSDR)** | FlexRadio 6000/8000 over the TCP API. → [[FlexRadio]] | +| **Icom CI-V** | Any Icom over **USB** *or* over the internet via its **LAN server**. → [[Icom]] · [[Remote Icom over the Internet]] | +| **TCI** | SunSDR / ExpertSDR2 and any TCI (WebSocket) server. | + +Once connected, frequency / band / mode follow the radio, clicking a cluster spot +tunes the rig, and (for Flex) cluster spots can be pushed to the panadapter. + +## OmniRig + +Pick **Rig 1** or **Rig 2** (the two OmniRig slots). Configure the actual COM +port / baud in OmniRig's own settings. Works with anything OmniRig supports; PTT +keying via CAT is supported where the rig's OmniRig profile exposes it. + +## FlexRadio + +Enter the radio's IP (UDP discovery assists). You get a full SmartSDR-style +control tab — see **[[FlexRadio]]**. + +## Icom CI-V (USB) + +- **Rig model** — pick your model from the dropdown; it sets the correct **CI-V + address** automatically (or choose *Other* and type the hex address). +- **COM port** + **baud** must match the radio (set *CI-V USB Echo Back* **OFF** + on the rig). +- The model choice also tailors the console (e.g. attenuator steps: 20 dB on an + IC-7300, 6/12/18 dB on an IC-7610). + +Full console details: **[[Icom]]**. + +## TCI + +Enter the TCI host + port (default 40001). Gives freq / mode / PTT / split and, +optionally, panorama spots. + +## Notes + +- The **digital sub-mode** (FT4 vs FT8) is inferred from the frequency. +- **Per-band Flex RX/TX antennas** can be configured and are applied + automatically on band change (Settings → FlexRadio). +- Split, RIT/XIT and other rig-specific features are exposed on each backend's + control tab where supported. diff --git a/wiki/Contest-Logging.md b/wiki/Contest-Logging.md new file mode 100644 index 0000000..c30ceae --- /dev/null +++ b/wiki/Contest-Logging.md @@ -0,0 +1,27 @@ +# Contest Logging + +**Contest tab.** Pick a contest and an exchange, and OpsLog handles the rest. + +## Setup + +- **Contest** — from the built-in ADIF `CONTEST_ID` list. +- **Exchange** — a **running serial** (auto-incrementing) or a **fixed exchange**. +- **Window** — start / end times; QSOs outside are flagged. + +## While operating + +- OpsLog auto-fills `CONTEST_ID` and the **sent / received serials** (`STX` / + `SRX`). +- **Dupe** detection flags a call already worked in the contest. +- A live **scoreboard** tracks your progress. + +## Export + +Export the contest log to **Cabrillo** for submission (Recent QSOs → export), or +to ADIF. See [[Import and Export ADIF]]. + +## Tip + +Use **Bulk edit field** (right-click in Recent QSOs) to fix a contest field +across many QSOs at once — e.g. set `CONTEST_ID` or an exchange string on an +imported batch. diff --git a/wiki/DX-Cluster-and-Spots.md b/wiki/DX-Cluster-and-Spots.md new file mode 100644 index 0000000..82567f4 --- /dev/null +++ b/wiki/DX-Cluster-and-Spots.md @@ -0,0 +1,40 @@ +# DX Cluster and Spots + +## Servers + +Add multiple cluster servers (Settings). They auto-reconnect; one is the +**master** used for sending commands. The Cluster tab and the Main-view cluster +pane share the same live feed. + +## Filter sidebar + +A show/hide **filter sidebar** (shared by the Cluster tab and the Main pane): + +- **Callsign** search, **hide worked**, **group duplicates**. +- Filter by **band / mode / status / source**. + +## Per-spot status and tuning + +Each spot is coloured by **status** relative to your log: + +- **new** (entity never worked), **new-band**, **new-slot**, **worked**. + +Click a spot to **tune the rig** (fills the callsign into the entry strip). A +multi-band **Band Map** shows panadapter-style strips per band. + +**POTA** spots are tagged with their park reference (via `api.pota.app`). + +## Sending a spot + +Use the **Send Spot** window to announce a DX spot on the master cluster: +callsign + frequency + a message. The frequency defaults to the **live CAT +frequency** (full resolution, including sub-kHz). + +## Spot alerts + +**Tools → Alert management** — Log4OM-style alert rules on **call / country / +band / mode / spotter**, with **sound**, **visual** and **e-mail** notification. +Active alerts appear as a discreet bell in the entry strip; click it for the +list. Alerts are kept briefly and only shown when active. + +See also: [[Maps and Antennas]] for turning the beam to a spot. diff --git a/wiki/FlexRadio.md b/wiki/FlexRadio.md new file mode 100644 index 0000000..a2775c5 --- /dev/null +++ b/wiki/FlexRadio.md @@ -0,0 +1,54 @@ +# FlexRadio + +When the CAT backend is a **FlexRadio (SmartSDR)**, OpsLog shows a full +SmartSDR-style control panel (as a Main-view pane or the FlexRadio tab). It talks +the radio's TCP API directly and streams live meters over UDP (VITA-49). + +## Slices (multi-slice) + +FlexRadios can run several receiver slices (A/B/C/D…). OpsLog shows **every +in-use slice** as a row of buttons under the header: + +- Click a slice to make it the **active** slice — the main frequency, mode, DSP + and spot-clicks all follow it. +- Click a slice's **TX** badge to move the transmitter onto it. +- **Split** is detected only when the TX slice and a same-band RX slice form a + genuine split; independent slices on other bands are not shown as split. + +Everything you do targets the **active** slice, so a second slice monitoring +another band never hijacks your main frequency. + +## Header + +Model + RX/TX state, plus compact **PA voltage** and **PA temperature** readouts +(temperature turns amber/red as it climbs). + +## Transmit + +RF power, tune power, **TUNE**, **MOX**, **SPLIT**, speech processor (NOR / DX / +DX+), VOX (+ level + delay), monitor (+ level), mic gain, **TX filter** low/high +cut, and the **mic profile** selector. On CW the panel switches to keyer controls +(speed / pitch / break-in / sidetone / filter). + +## Receive (active slice) + +RX/TX antenna, AGC mode + threshold, audio level, and the noise/notch filters: +**NB**, **WNB** (wideband noise blanker), **NR**, **ANF**, and **APF** on CW — +each a toggle plus level. Filter-width presets per mode. + +## Amplifier (PowerGenius XL) + +If a PowerGenius XL is detected, its **OPERATE / STANDBY**, **fan mode** and +**fault** show in an Amplifier card, with the amp's FWD / ID / TEMP meters +inline. See [[Amplifiers and Switches]]. + +## Meters + +Live UDP meters under the slices: **S-meter** (S-units, click → fill RST), +**forward power** (W), **SWR**, **mic** level (−40…+10 dB, red = overdrive) and +**compression**. PA voltage and temperature are in the header. + +## Antennas per band + +Configure Flex **RX/TX antennas per band** (Settings → FlexRadio); OpsLog applies +them automatically on band change. diff --git a/wiki/Getting-Started.md b/wiki/Getting-Started.md new file mode 100644 index 0000000..fe204ea --- /dev/null +++ b/wiki/Getting-Started.md @@ -0,0 +1,59 @@ +# Getting Started + +This walks you from a fresh install to your first logged QSO. + +## 1. Set your station + +**Settings → Station**: enter your **callsign**, **grid locator** and **name**. +These feed callsign lookups, the map, awards, the QSL card and the "my station" +ADIF fields on every QSO. + +## 2. (Optional) Connect a radio + +**Settings → CAT**, pick a backend and fill its fields — see [[CAT Control]]. +Once connected, the frequency, band and mode fill in automatically as you tune, +and clicking a cluster spot tunes the rig. + +You can log perfectly well **without** a radio — just type the band, mode and +frequency. + +## 3. The entry strip + +The strip across the top is the heart of OpsLog (Log4OM-style): + +- **Callsign** — type it and the contacted entity's **flag** appears next to the + RST fields; the country, CQ/ITU zones and continent resolve offline from + `cty.dat`. If QRZ/HamQTH lookup is configured, name / QTH / grid auto-fill and + a photo shows in the F2 tab. +- **RST sent / rcvd**, **Name**, **QTH**, **Grid**. +- **Band / Mode / Frequency** (TX and, for split, RX). +- **Start / End time**, **Comment / Note**. + +Press **Enter** in any text field to **log** the QSO. The contact drops into +**Recent QSOs** below. + +> Mode comes from the radio; the digital sub-mode (FT4 vs FT8) is inferred from +> the frequency. + +## 4. Look around + +- **Recent QSOs** — your log; double-click a row to edit every field. + See [[Recent QSOs and Filters]]. +- **Worked-before matrix** — as you type a call, see if/when you worked that + entity, per band/mode slot. +- **Main view** — two configurable panes (Settings → General → *Main view*): a + great-circle map, the cluster, the FlexRadio or Icom console, Net control, and + more. See [[Maps and Antennas]]. + +## 5. Profiles + +Every setting is **per-profile**. A profile can log to the local SQLite file or +a shared **MySQL** database (multi-operator). Create one per call / station / +event. See [[Profiles and Databases]]. + +## Next steps + +- Chase spots → [[DX Cluster and Spots]] +- Track awards → [[Awards]] +- Send confirmations → [[QSL Management]] +- Design a card → [[QSL Card Designer]] diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..e412a3f --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,41 @@ +# OpsLog + +A modern, fast ham-radio logger for **Windows** — Log4OM-style entry, real-time +CAT for **OmniRig**, native **FlexRadio/SmartSDR**, native **Icom CI-V** (USB +*and* remote-over-internet, replacing RS-BA1) and **TCI** (SunSDR / Expert +Electronics), a DX cluster with spot alerts, awards tracking, maps, contest +logging, QSL management and a QSL-card designer. + +Built with **Wails v2** (Go backend + React/TypeScript frontend), **pure Go** +(no CGO). SQLite holds the configuration; the logbook can be the local SQLite +file *or* a **shared MySQL** database so several operators run one log. Fully +themeable and bilingual (English / French). + +Developed by **F4BPO**. + +## New here? Read these first + +1. **[[Installation]]** — download / build and first launch. +2. **[[Getting Started]]** — the entry strip, logging your first QSO, choosing a + profile. +3. **[[CAT Control]]** — connect your radio (OmniRig, FlexRadio, Icom, TCI). + +## What OpsLog does + +| Area | Pages | +|------|-------| +| Enter and manage QSOs | [[Logging Basics]] · [[Recent QSOs and Filters]] · [[Import and Export ADIF]] | +| Control your radio | [[CAT Control]] · [[FlexRadio]] · [[Icom]] · [[Remote Icom over the Internet]] | +| Chase DX | [[DX Cluster and Spots]] · [[Maps and Antennas]] | +| Station hardware | [[Amplifiers and Switches]] · [[Audio and Keyers]] | +| Events & contests | [[Contest Logging]] · [[Net Control]] · [[Multi-Operator Live Status]] | +| Confirmations | [[Awards]] · [[QSL Management]] · [[QSL Card Designer]] | +| Configuration | [[Settings and Data]] · [[Security]] · [[Profiles and Databases]] | +| Something wrong? | [[Troubleshooting]] | + +## Design in one line + +Everything you configure (settings, profiles, rigs, cluster nodes, award lists, +QSL templates, lookup cache) lives in a **local SQLite** file so the UI is +instant. Only the **QSOs** live where the active profile points them — local +SQLite or a shared MySQL. See [[Profiles and Databases]]. diff --git a/wiki/Icom.md b/wiki/Icom.md new file mode 100644 index 0000000..3d386f8 --- /dev/null +++ b/wiki/Icom.md @@ -0,0 +1,55 @@ +# Icom + +When the CAT backend is **Icom CI-V** (USB or network), OpsLog shows a full +RS-BA1-style console — the same panel whether you're on USB or remote. + +For setup, see [[CAT Control]]; for the internet link, see +[[Remote Icom over the Internet]]. + +## Choosing your model + +Pick your model from the **Rig model** dropdown in Settings → CAT — it sets the +CI-V address and tailors the console to the radio (e.g. attenuator steps). +The model is also auto-detected from the radio's CI-V ID. + +## Console + +- **Twin VFO readout** (MAIN / SUB): big tabular frequency, mode badge, band, and + RIT/ΔTX offset, with a **mode-button row** (SSB / CW / RTTY / PSK / AM / FM). +- **Spectrum scope + waterfall**: ON/OFF, CTR/FIX, double-click to tune, and + **◀ ⊙ ▶** to centre the scope on the current frequency (±50 kHz) and pan. +- **Live meters** always visible: S-meter (click → fill RST), power in watts, SWR. +- **Receive DSP:** AF / RF gain, squelch, AGC, preamp, **attenuator** (steps match + the model — e.g. 20 dB on an IC-7300, 6/12/18 dB on an IC-7610), filter + (FIL1/2/3), NB, NR, ANF, and — **on CW only** — the **APF** (audio peak filter). +- **Passband / notch:** Twin PBT (inner / outer) and manual notch + position. +- **Transmit:** RF power, MOX, TUNE, **split with automatic offset** (+5 kHz SSB / + +1 kHz CW), monitor. On **voice modes only**: mic gain, compressor, VOX (+ gain + + anti-VOX). Controls that don't apply to the current mode are hidden. +- **Bands & antenna:** one-touch band buttons, ANT1/ANT2. +- **Clarifiers:** RIT and ΔTX, wheel / ± tuning (Ctrl + ← / → nudges RIT). + +## Power ON / OFF + +- **Network:** ON and OFF buttons are shown — the radio's LAN server answers even + in standby, so ON works. +- **USB:** the buttons are **hidden**. Over USB the CI-V interface is unpowered + while the rig is off, so a power-ON command can't reach it — turn the radio on + physically. + +The app never wakes the rig on connect (manual by design). + +## CW keying + +CW can run through the **radio's own keyer** over CI-V — no extra hardware, and +it works over the remote link too. See [[Audio and Keyers]]. + +## Model-specific notes + +- **IC-7300 / IC-705 / IC-9100:** single-receiver, single-step attenuator + (20 dB); the IC-9100 is USB-only (no LAN server). +- **IC-7610 / IC-9700:** dual scope (main/sub); stepped attenuator; LAN server + for remote. +- If the **scope is blank on a single-scope rig run at a non-default CI-V + address**, OpsLog now detects the model from the CI-V ID, not the configured + address, so it decodes single-scope frames correctly. diff --git a/wiki/Import-and-Export-ADIF.md b/wiki/Import-and-Export-ADIF.md new file mode 100644 index 0000000..118c142 --- /dev/null +++ b/wiki/Import-and-Export-ADIF.md @@ -0,0 +1,32 @@ +# Import and Export ADIF + +OpsLog is **ADIF 3.1.7 compliant**: a full field dictionary, 30 promoted columns, +a generic "extra fields" editor for anything not promoted, and standard/all +export modes. + +## Import + +Import an `.adi` / `.adif` file into the active profile's logbook. On import, +OpsLog parses every field, maps standard fields to columns and keeps unknown but +valid ADIF fields in a per-QSO **extras** store (editable later). Country / zones +are enriched from `cty.dat` where missing. + +## Export + +- **Whole log**, **selected rows**, or the **filtered view** (no limit) — see + [[Recent QSOs and Filters]]. +- **Standard** mode exports the common promoted fields; **All** mode also exports + every extra field. +- **Cabrillo** export for contest submission. + +## Round-trip safety + +Award references you assign manually are stored in an ADIF extra field +(`APP_OPSLOG_AWARDREFS`), so exporting your full log and re-importing it does not +lose your award-reference assignments. + +## Tips + +- To move a batch of QSOs between logs, export selected → import into the other + profile. +- Use **Find duplicates** (Tools) after importing to catch overlaps. diff --git a/wiki/Installation.md b/wiki/Installation.md new file mode 100644 index 0000000..f9cafd7 --- /dev/null +++ b/wiki/Installation.md @@ -0,0 +1,44 @@ +# Installation + +OpsLog is a single Windows executable — no installer, no runtime to add. Pure Go ++ an embedded WebView2 (present on Windows 10/11 by default). + +## Download and run + +1. Grab the latest `OpsLog.exe` from the project's **Releases** page. +2. Put it in its own folder (it will create a `data/` folder next to itself). +3. Double-click to run. On first launch you pick a **language** (English / + French) and OpsLog creates its local database. + +> **Where is my data?** A `data/` folder is created next to the executable, +> holding the SQLite config + local logbook, QSL templates, recordings and +> backups. Keep the exe and `data/` together, or back up `data/`. See +> [[Settings and Data]]. + +## Building from source (developers) + +OpsLog is built with **Wails v2.11**. + +```bash +# Dev — Vite hot-reload; Go methods reachable at http://localhost:34115 +wails dev + +# Production build → build/bin/OpsLog.exe +wails build + +# Regenerate Go↔TS bindings after changing exported App methods +wails generate module +``` + +Prerequisites: Go, Node.js, and the Wails CLI (`~/go/bin/wails.exe`). No CGO / +C toolchain is needed. + +## First-run checklist + +- **Settings → General** — language, theme, and the two Main-view panes. +- **Settings → Station** — your callsign, grid, name (used for lookups, awards, + QSL cards and the map). +- **Settings → CAT** — connect your radio ([[CAT Control]]). +- **Settings → Lookup** — QRZ.com / HamQTH credentials for callsign lookup. + +Continue with **[[Getting Started]]**. diff --git a/wiki/Logging-Basics.md b/wiki/Logging-Basics.md new file mode 100644 index 0000000..7bd4bc3 --- /dev/null +++ b/wiki/Logging-Basics.md @@ -0,0 +1,45 @@ +# Logging Basics + +## The entry strip + +The entry strip logs a QSO from any `` inside it — press **Enter** to +save. Fields: callsign, RST tx/rx, name, QTH, grid, band, mode, TX/RX frequency +(split), start/end time, comment and note. + +- **Callsign** drives everything: the **flag**, offline DXCC resolution (country, + CQ/ITU zones, continent) from `cty.dat` including `/MM` `/AM` `/B` and call-area + handling (`/8`, `/W6`), plus **ClubLog** DXpedition date overrides. +- **Callsign lookup** (QRZ.com / HamQTH) fills name / QTH / grid and shows the + photo and a QRZ.com tab. Configure it in Settings → Lookup. +- **ESC** clears the callsign (and stops CW). + +## Editing a QSO + +Double-click any row in **Recent QSOs** (or Worked-before) to open the full +editor — every ADIF field across tabs: QSO info, contacted station's details, +award refs, QSL info, contest, sat/prop, my station, and a generic **extra +fields** editor for any ADIF field not promoted to a column. + +## Bulk operations + +Select rows in Recent QSOs, right-click: + +- **Fix country & zones** from `cty.dat`, or **update from QRZ / ClubLog**. +- **Bulk edit field** — set one field across many QSOs (QSL/upload status, my + station, contest, propagation, contacted-station refs, comment…). Great for + flipping a batch to *Requested* before an upload. +- **Send to** a QSL service, **export** selected / filtered to ADIF or Cabrillo, + **delete**. + +## Find duplicates + +**Tools → Find duplicates** groups QSOs by same **call + band + mode** (optionally +same day or same minute) and lets you pick which to delete. + +## Notes + +- **Worked-before matrix** shows prior contacts with the entity per band/mode + slot as you type. +- Awards references are detected **live** as you enter a call (see [[Awards]]). + +See also: [[Recent QSOs and Filters]] · [[Import and Export ADIF]] diff --git a/wiki/Maps-and-Antennas.md b/wiki/Maps-and-Antennas.md new file mode 100644 index 0000000..4d76ae3 --- /dev/null +++ b/wiki/Maps-and-Antennas.md @@ -0,0 +1,38 @@ +# Maps and Antennas + +## Main view + +The Main view is **two configurable panes** (per profile, Settings → General → +*Main view*). Choose from: great-circle map, locator (street) map, the cluster +grid, the worked-before grid, recent QSOs, the **FlexRadio** controls, the +**Icom** console, or the **Net control** panel. + +## Great-circle map + +- Short/long-path **distance & azimuth** to the worked station. +- Selectable, **key-free** basemaps: Light / Voyager / Street / Satellite (all + labelled). +- The **antenna beam lobe(s)** are drawn from the rotor azimuth. + +## Locator (street) map + +An OSM street map centred on the contacted grid square — handy for local / +portable contacts. No API key. + +## Rotor compass + +An azimuthal-equidistant **click-to-turn** compass driven by **PstRotator**. The +current heading and target are shown; click a bearing to turn the rotor. + +## Ultrabeam + +Ultrabeam antennas are supported with three modes — **Normal**, **180° reverse**, +**Bidirectional**: + +- The **radiating direction** is shown in green and the **mechanical boom** in + grey, on both the compass and the map, so you always know where the antenna + actually points. +- The antenna **follows the rig frequency** (band change → retune), and retunes + immediately when you click a spot. + +Related hardware: [[Amplifiers and Switches]] (Antenna Genius switch). diff --git a/wiki/Multi-Operator-Live-Status.md b/wiki/Multi-Operator-Live-Status.md new file mode 100644 index 0000000..85b5c78 --- /dev/null +++ b/wiki/Multi-Operator-Live-Status.md @@ -0,0 +1,42 @@ +# Multi-Operator Live Status + +For a **multi-op special-event call** on a shared MySQL logbook (e.g. TM74TFR), +OpsLog can publish who is on the air, on which band/mode/frequency, as a live +page you embed on the station's **QRZ.com** bio. + +## Enable it + +1. Point the profile's logbook at a **shared MySQL** database + ([[Profiles and Databases]]). +2. **Settings → General → Publish live operator status.** + +Each OpsLog instance then heartbeats its current activity (operator call, band, +frequency, mode) into a `live_status` table every ~15 s. OpsLog only **writes** +to the database — it is not a web server. + +## The web page + +A small PHP renderer, `docs/livestatus/tm74-status.php`, reads that table and +produces a live page/image. Put it on **your own** web server (the one reachable +from the internet): + +1. Edit the credentials at the top: `$DB_HOST`, `$DB_PORT`, `$DB_NAME`, + `$DB_USER`, `$DB_PASS` — pointing at the **same** MySQL as OpsLog's logbook. +2. Embed it on QRZ (QRZ strips `