chore: release v0.19.2
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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 (
|
||||
<div onClick={onClick} title={title}
|
||||
className={cn('rounded-lg border border-border/70 px-2.5 py-2 bg-gradient-to-b from-card to-muted/40 shadow-sm min-w-0',
|
||||
className={cn('rounded-lg border border-border/70 bg-gradient-to-b from-card to-muted/40 shadow-sm min-w-0',
|
||||
compact ? 'px-2 py-1' : 'px-2.5 py-2',
|
||||
onClick && 'cursor-pointer hover:border-primary/60 hover:from-muted/40')}>
|
||||
<div className="flex items-baseline justify-between gap-1 mb-1.5">
|
||||
<div className={cn('flex items-baseline justify-between gap-1', compact ? 'mb-1' : 'mb-1.5')}>
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground truncate">{label}</span>
|
||||
<span className="text-sm font-mono font-bold tabular-nums whitespace-nowrap text-foreground/90">
|
||||
<span className={cn('font-mono font-bold tabular-nums whitespace-nowrap text-foreground/90', compact ? 'text-xs' : 'text-sm')}>
|
||||
{display !== undefined ? display : (
|
||||
<>{Math.abs(value) >= 100 ? value.toFixed(0) : value.toFixed(1)}<span className="text-muted-foreground text-[10px] ml-0.5">{unit}</span></>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{/* LED bar — recessed track + gradient segments for a cleaner instrument look. */}
|
||||
<div className="flex gap-[2px] h-3 items-stretch rounded-[3px] bg-black/10 p-[2px]">
|
||||
<div className={cn('flex gap-[2px] items-stretch rounded-[3px] bg-black/10 p-[2px]', compact ? 'h-2' : 'h-3')}>
|
||||
{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
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{extra && <div className="text-[10px] text-muted-foreground/70 mt-1 text-right font-mono">{extra}</div>}
|
||||
{extra && !compact && <div className="text-[10px] text-muted-foreground/70 mt-1 text-right font-mono">{extra}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="h-full min-h-0 overflow-auto bg-background">
|
||||
<div className="max-w-5xl mx-auto p-3 space-y-3">
|
||||
@@ -274,6 +286,23 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
<span className="text-[11px] text-slate-400">{t('flxp.smartsdrRemote')}</span>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
{/* Compact PA voltage + temperature readouts (numbers) to save space. */}
|
||||
{!off && (voltM || patempM) && (
|
||||
<div className="flex items-center gap-4 mr-1 font-mono tabular-nums">
|
||||
{voltM && (
|
||||
<span className="flex flex-col items-end leading-none">
|
||||
<span className="text-[9px] uppercase tracking-wider text-slate-400 font-sans">{t('flxp.voltage')}</span>
|
||||
<span className="text-sm font-bold">{voltM.value.toFixed(1)}<span className="text-[9px] text-slate-400 ml-0.5">V</span></span>
|
||||
</span>
|
||||
)}
|
||||
{patempM && (
|
||||
<span className="flex flex-col items-end leading-none">
|
||||
<span className="text-[9px] uppercase tracking-wider text-slate-400 font-sans">{t('flxp.paTemp')}</span>
|
||||
<span className={cn('text-sm font-bold', patempM.value >= 70 ? 'text-red-400' : patempM.value >= 55 ? 'text-amber-400' : 'text-slate-100')}>{patempM.value.toFixed(0)}<span className="text-[9px] text-slate-400 ml-0.5">°C</span></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className={cn('inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-extrabold tracking-wider',
|
||||
!st.available ? 'bg-muted text-muted-foreground'
|
||||
: st.transmitting ? 'bg-danger text-danger-foreground shadow-[0_0_16px] shadow-danger/60' : 'bg-success text-success-foreground')}>
|
||||
@@ -315,6 +344,58 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Live meters (UDP VITA-49 stream) — placed right under the slices. */}
|
||||
<Card icon={Gauge} title={t('flxp.meters')}>
|
||||
{(() => {
|
||||
const meters = st.meters || [];
|
||||
if (off || meters.length === 0) {
|
||||
return <p className="text-[11px] text-muted-foreground text-center py-2">{t('flxp.noMeters')}</p>;
|
||||
}
|
||||
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 (
|
||||
<MeterBar key="s" label="S-METER" value={s.bar} lo={0} hi={19} accent="#16a34a" display={s.display} extra={`${dbm.toFixed(1)} dBm`}
|
||||
title={onReportRST ? t('rst.clickToFill') : undefined}
|
||||
onClick={onReportRST ? () => 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 (
|
||||
<MeterBar key="p" label="PWR" unit="W" lo={0} hi={120} accent="#dc2626"
|
||||
value={w} extra={isDbm(fwd) ? `${fwd.value.toFixed(1)} dBm` : undefined} />
|
||||
); })(),
|
||||
swr && <MeterBar key="w" label="SWR" value={peakHold('w', swr.value)} unit="" lo={1} hi={3} accent="#d97706" />,
|
||||
// Mic input level: fixed -40..+10 dB scale, RED from 0 dB up (overdrive).
|
||||
mic && <MeterBar key="mic" label="MIC" value={peakHold('mic', mic.value)} unit={mic.unit || 'dB'} lo={-40} hi={10} accent="#16a34a"
|
||||
segColor={(fr) => (fr >= 0.8 ? '#dc2626' : fr >= 0.7 ? '#f59e0b' : '#16a34a')} />,
|
||||
// Speech compression (dB of gain reduction).
|
||||
comp && <MeterBar key="comp" label="COMP" value={peakHold('comp', comp.value)} unit={comp.unit || 'dB'} lo={0} hi={(comp.hi && comp.hi > 0) ? comp.hi : 25} accent="#0891b2" />,
|
||||
].filter(Boolean);
|
||||
return (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">{cur}</div>
|
||||
);
|
||||
})()}
|
||||
</Card>
|
||||
|
||||
{off && (
|
||||
<div className="text-center text-sm text-muted-foreground py-6">
|
||||
{t('flxp.waiting')}
|
||||
@@ -391,6 +472,39 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
<Slider value={st.mic_level} disabled={off} accent="#2563eb" onChange={(v) => change('mic_level', v, () => FlexSetMic(v))} />
|
||||
<span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.mic_level}</span>
|
||||
</div>
|
||||
{/* 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. */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">{t('flxp.txFilter')}</span>
|
||||
{(() => {
|
||||
const editTX = (patch: Partial<FlexState>) => { 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 (<>
|
||||
<input type="number" step={50} min={0} disabled={off} value={st.tx_filter_low || 0} className={inp}
|
||||
onChange={(e) => editTX({ tx_filter_low: parseInt(e.target.value, 10) || 0 })}
|
||||
onBlur={commitTX} onKeyDown={(e) => { if (e.key === 'Enter') { (e.target as HTMLInputElement).blur(); } }} />
|
||||
<span className="text-muted-foreground text-xs">–</span>
|
||||
<input type="number" step={50} min={0} disabled={off} value={st.tx_filter_high || 0} className={inp}
|
||||
onChange={(e) => editTX({ tx_filter_high: parseInt(e.target.value, 10) || 0 })}
|
||||
onBlur={commitTX} onKeyDown={(e) => { if (e.key === 'Enter') { (e.target as HTMLInputElement).blur(); } }} />
|
||||
<span className="text-[10px] text-muted-foreground/70 font-mono">Hz</span>
|
||||
</>);
|
||||
})()}
|
||||
</div>
|
||||
{/* Mic profile — SmartSDR named mic-EQ/processor presets. */}
|
||||
{!!st.mic_profiles && st.mic_profiles.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">{t('flxp.micProfile')}</span>
|
||||
<select disabled={off} value={st.mic_profile || ''}
|
||||
onChange={(e) => { const v = e.target.value; hold.current['mic_profile'] = Date.now() + 2000; setSt((p) => ({ ...p, mic_profile: v })); FlexSetMicProfile(v).catch(() => {}); }}
|
||||
className="flex-1 min-w-0 h-8 rounded-md border border-input bg-background px-2 text-xs">
|
||||
{!st.mic_profiles.includes(st.mic_profile || '') && <option value="">—</option>}
|
||||
{st.mic_profiles.map((p) => <option key={p} value={p}>{p}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* 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
|
||||
<LevelRow label="NB" on={st.nb} disabled={rxOff} value={st.nb_level} accent="amber" sliderAccent="#d97706"
|
||||
onToggle={() => 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). */}
|
||||
<LevelRow label="WNB" on={st.wnb} disabled={rxOff} value={st.wnb_level} accent="amber" sliderAccent="#f59e0b"
|
||||
onToggle={() => change('wnb', !st.wnb, () => FlexSetWNB(!st.wnb))}
|
||||
onLevel={(v) => change('wnb_level', v, () => FlexSetWNBLevel(v))} />
|
||||
<LevelRow label="NR" on={st.nr} disabled={rxOff} value={st.nr_level} accent="cyan" sliderAccent="#0891b2"
|
||||
onToggle={() => 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
|
||||
<span className="px-2 py-1 rounded bg-danger-muted text-danger-muted-foreground text-xs font-bold">{t('flxp.fault')}: {st.amp_fault}</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 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 (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mt-2 pt-2 border-t border-border/50">
|
||||
{amp.map((m) => {
|
||||
if (/fwd|pwr/i.test(m.name || '') && /dbm/i.test(m.unit || '')) {
|
||||
return <MeterBar key={m.id} compact label={m.name || `AMP ${m.id}`} value={peakHold(`amp${m.id}`, dbmToW(m.value))} unit="W" lo={0} hi={2000} accent="#dc2626" />;
|
||||
}
|
||||
const acc = /temp|degc|degf/i.test(`${m.unit}${m.name}`) ? '#ea580c' : /volt/i.test(m.unit || '') ? '#2563eb' : '#16a34a';
|
||||
return <MeterBar key={m.id} compact label={m.name || `AMP ${m.id}`} value={peakHold(`amp${m.id}`, m.value)} unit={m.unit} lo={m.lo} hi={m.hi} accent={acc} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Live meters (UDP VITA-49 stream) */}
|
||||
<Card icon={Gauge} title={t('flxp.meters')}>
|
||||
{(() => {
|
||||
const meters = st.meters || [];
|
||||
if (off || meters.length === 0) {
|
||||
return <p className="text-[11px] text-muted-foreground text-center py-2">{t('flxp.noMeters')}</p>;
|
||||
}
|
||||
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 (
|
||||
<MeterBar key="s" label="S-METER" value={s.bar} lo={0} hi={19} accent="#16a34a" display={s.display} extra={`${dbm.toFixed(1)} dBm`}
|
||||
title={onReportRST ? t('rst.clickToFill') : undefined}
|
||||
onClick={onReportRST ? () => 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 (
|
||||
<MeterBar key="p" label="PWR" unit="W" lo={0} hi={120} accent="#dc2626"
|
||||
value={w} extra={isDbm(fwd) ? `${fwd.value.toFixed(1)} dBm` : undefined} />
|
||||
); })(),
|
||||
swr && <MeterBar key="w" label="SWR" value={peakHold('w', swr.value)} unit="" lo={1} hi={3} accent="#d97706" />,
|
||||
].filter(Boolean);
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">{cur}</div>
|
||||
{amp.length > 0 && (
|
||||
<div className="border-t border-border/60 pt-2 mt-1">
|
||||
<div className="text-[10px] font-bold tracking-wider text-muted-foreground mb-1.5">{t('flxp.amplifierHdr')}</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{amp.map((m) => {
|
||||
if (/fwd|pwr/i.test(m.name || '') && isDbm(m)) {
|
||||
return <MeterBar key={m.id} label={m.name || `AMP ${m.id}`} value={peakHold(`amp${m.id}`, dbmToW(m.value))} unit="W" lo={0} hi={2000} accent="#dc2626" extra={`${m.value.toFixed(1)} dBm`} />;
|
||||
}
|
||||
return <MeterBar key={m.id} label={m.name || `AMP ${m.id}`} value={peakHold(`amp${m.id}`, m.value)} unit={m.unit} lo={m.lo} hi={m.hi} accent={accentFor(m)} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
Vendored
+8
@@ -202,6 +202,8 @@ export function FlexSetFilter(arg1:number,arg2:number):Promise<void>;
|
||||
|
||||
export function FlexSetMic(arg1:number):Promise<void>;
|
||||
|
||||
export function FlexSetMicProfile(arg1:string):Promise<void>;
|
||||
|
||||
export function FlexSetMon(arg1:boolean):Promise<void>;
|
||||
|
||||
export function FlexSetMonLevel(arg1:number):Promise<void>;
|
||||
@@ -230,6 +232,8 @@ export function FlexSetSplit(arg1:boolean):Promise<void>;
|
||||
|
||||
export function FlexSetTXAntenna(arg1:string):Promise<void>;
|
||||
|
||||
export function FlexSetTXFilter(arg1:number,arg2:number):Promise<void>;
|
||||
|
||||
export function FlexSetTXSlice(arg1:number):Promise<void>;
|
||||
|
||||
export function FlexSetTunePower(arg1:number):Promise<void>;
|
||||
@@ -240,6 +244,10 @@ export function FlexSetVoxDelay(arg1:number):Promise<void>;
|
||||
|
||||
export function FlexSetVoxLevel(arg1:number):Promise<void>;
|
||||
|
||||
export function FlexSetWNB(arg1:boolean):Promise<void>;
|
||||
|
||||
export function FlexSetWNBLevel(arg1:number):Promise<void>;
|
||||
|
||||
export function FlexTune(arg1:boolean):Promise<void>;
|
||||
|
||||
export function GetActiveProfile():Promise<profile.Profile>;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=<name>" (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=<name> (some firmwares:
|
||||
// selection=<name>); 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")
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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]].)
|
||||
@@ -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]]).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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]]
|
||||
@@ -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]].
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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]]**.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Logging Basics
|
||||
|
||||
## The entry strip
|
||||
|
||||
The entry strip logs a QSO from any `<input>` 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]]
|
||||
@@ -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).
|
||||
@@ -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 `<script>`/`<iframe>`, so use an image):
|
||||
|
||||
```html
|
||||
<img src="https://your-server/tm74-status.php?img=1">
|
||||
```
|
||||
|
||||
or link the real-time page: `https://your-server/tm74-status.php`.
|
||||
|
||||
## Troubleshooting the PHP page
|
||||
|
||||
- **`Connection refused`** — MySQL is refusing the connection. Most common cause:
|
||||
the server's `bind-address` is `127.0.0.1` (localhost only). Set it to the LAN
|
||||
IP / `0.0.0.0`, grant the `opslog` user for the web server's IP, and open the
|
||||
firewall.
|
||||
- **Runs on a non-standard MySQL port** — set `$DB_PORT` and pass it as the 5th
|
||||
`mysqli` argument (the shipped script already does).
|
||||
- Redeploy the current `tm74-status.php` if you edited an old copy — the shipped
|
||||
version fails gracefully instead of throwing a PHP fatal error.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Net Control
|
||||
|
||||
**Directed-net logging** (Tools → Net) — run a directed net or round-table and
|
||||
log the whole thing at once, Log4OM-style.
|
||||
|
||||
## How it works
|
||||
|
||||
- **Roster** (right) — the net's registered stations, stored globally in
|
||||
`nets.json` (shared across profiles). Add contacts with a QRZ/HamQTH prefill.
|
||||
- **On-air** (left) — the active session (in memory): put a station on the air
|
||||
(check it in), and log the whole net when done.
|
||||
- Frequency / band / mode of each logged QSO come from **live CAT** at log time
|
||||
(falling back to the last known values if CAT is off).
|
||||
|
||||
## Working a net
|
||||
|
||||
1. Open / create a net and **Open** it.
|
||||
2. Double-click a roster station (or use *Put on air*) to put it on the air —
|
||||
this starts its QSO (time on).
|
||||
3. Double-click an on-air row to open the full QSO editor (edit any field).
|
||||
4. **Log & end** a station to write its QSO to the logbook (time off = now).
|
||||
|
||||
## Worked-before
|
||||
|
||||
Click a station (on air or in the roster) to see prior contacts with it in the
|
||||
**worked-before** panel below the on-air column. The panel height is adjustable
|
||||
(drag its top edge).
|
||||
|
||||
> The active session is in-memory only — closing the app mid-net drops
|
||||
> un-logged on-air stations.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Profiles and Databases
|
||||
|
||||
## Profiles
|
||||
|
||||
Every setting in OpsLog is **per-profile**: station identity, CAT, lookup, QSL
|
||||
defaults, awards display, antennas, and which logbook to use. Create one profile
|
||||
per callsign / station / event.
|
||||
|
||||
Switch or manage profiles in Settings. The active profile decides which logbook
|
||||
your QSOs go to.
|
||||
|
||||
## Config vs. logbook — two different stores
|
||||
|
||||
| Store | What | Where |
|
||||
|-------|------|-------|
|
||||
| **Config** | settings, profiles, rigs/antennas, cluster nodes, lookup cache, award lists, QSL templates | always the **local SQLite** file under `data/` |
|
||||
| **Logbook** | your QSOs | where the active profile points — local SQLite **or** shared MySQL |
|
||||
|
||||
Keeping config local means the UI is instant even when the logbook is a far-away
|
||||
MySQL server.
|
||||
|
||||
## Shared MySQL logbook (multi-operator)
|
||||
|
||||
Point a profile's logbook at a **MySQL** database so several operators run **one
|
||||
log** — e.g. a multi-op special-event call. Configure the host / database /
|
||||
credentials in the profile's database settings.
|
||||
|
||||
This is also what powers [[Multi-Operator Live Status]] (each instance heartbeats
|
||||
its activity into a `live_status` table).
|
||||
|
||||
## Backups
|
||||
|
||||
Optional **database + ADIF backup at shutdown** (Settings → Backup). Regardless,
|
||||
copying the `data/` folder backs up your config and local logbook. See
|
||||
[[Settings and Data]].
|
||||
@@ -0,0 +1,35 @@
|
||||
# QSL Card Designer
|
||||
|
||||
**Tools → QSL Card Designer…** turns a few photos into a polished eQSL card.
|
||||
|
||||
## Design flow
|
||||
|
||||
1. **Pick 1–6 photos** (jpeg/png). OpsLog analyses them offline (detail /
|
||||
luminance grid) and proposes **3 designs** — callsign placed in the calmest
|
||||
zone of the best photo, operator name, CQ/ITU zones + locator line, country
|
||||
flag, the other photos as bordered inserts, and a per-QSO confirmation box.
|
||||
2. **Fine-tune**: click an element to select, drag to move; change font / style
|
||||
preset (gel gold, gel silver, classic white outline, script, flat) and the
|
||||
per-preset knobs on the right. The **QSO info box** has opacity, background
|
||||
colour, corner radius, title and text-colour controls.
|
||||
3. **Save** the template. Photos are copied into
|
||||
`data/qsl/templates/<id>/` (the originals can move afterwards). One template
|
||||
can be the **default per profile**.
|
||||
|
||||
## Sending
|
||||
|
||||
Right-click a QSO → **Send eQSL by e-mail**. The card is rendered with that QSO's
|
||||
data, rasterised to a ≤ 800 KB JPEG, archived in `data/qsl/outbox/`, and sent via
|
||||
the configured SMTP account to the address from the QRZ/HamQTH lookup. On success
|
||||
the QSO is stamped `EQSL_SENT=Y`.
|
||||
|
||||
The e-mail subject/body templates live in the designer, with
|
||||
`{CALL} {DATE} {BAND} {MODE} {MYCALL}` variables.
|
||||
|
||||
## Fonts & flags
|
||||
|
||||
Fonts: Archivo Black, Lilita One, Baloo 2, Oswald, Great Vibes, Allura (all OFL,
|
||||
embedded); Cooper Black is offered when MS Office installed it. Flags:
|
||||
flag-icons (MIT), embedded for the commonly-worked DXCC entities.
|
||||
|
||||
See also: [[QSL Management]].
|
||||
@@ -0,0 +1,44 @@
|
||||
# QSL Management
|
||||
|
||||
**Tools → QSL Manager.** Upload and download confirmations for the online QSL
|
||||
services, and manage paper QSL.
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Upload | Download confirmations |
|
||||
|---------|:------:|:----------------------:|
|
||||
| **LoTW** | ✔ (via TQSL) | ✔ |
|
||||
| **QRZ.com** | ✔ | ✔ |
|
||||
| **eQSL** | ✔ | ✔ |
|
||||
| **ClubLog** | ✔ (batched ADIF) | — |
|
||||
| **HRDLog** | ✔ | — |
|
||||
| **POTA** | hunter-log sync | ✔ (park tags) |
|
||||
| **Paper** | — | set sent/received + via |
|
||||
|
||||
## Uploading
|
||||
|
||||
Pick a service, find the QSOs (by callsign or a filter), select and upload.
|
||||
**Bulk edit field** (right-click in Recent QSOs) flips a batch's upload status to
|
||||
*Requested* first, if you gate uploads that way.
|
||||
|
||||
## Downloading confirmations
|
||||
|
||||
Pick a service and a **date window** (All / Since last download / a date), then
|
||||
**Download confirmations**. OpsLog matches each confirmation to your log
|
||||
(call + minute + band + mode), stamps it (`LOTW_RCVD`, `EQSL_RCVD`,
|
||||
`QRZ download`…), and optionally **adds not-found** QSOs. Downloading
|
||||
auto-refreshes the **award** stats.
|
||||
|
||||
- **LoTW** and **paper QSL** are the award-valid confirmations for ARRL awards;
|
||||
eQSL / QRZ count only against their own confirmations.
|
||||
- **LoTW upload** goes through **TQSL** (ARRL's signer) — set the TQSL path and
|
||||
station location in Settings.
|
||||
|
||||
## E-mail eQSL
|
||||
|
||||
Right-click a QSO → **Send eQSL by e-mail** via the configured SMTP account. The
|
||||
[[QSL Card Designer]] card is rendered with that QSO's data and sent to the
|
||||
address found by the QRZ/HamQTH lookup.
|
||||
|
||||
> Outlook/Hotmail disable basic-auth SMTP — use **Gmail with an app password**,
|
||||
> or a Microsoft app password.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Publishing this wiki to GitHub
|
||||
|
||||
These files are the source for the **OpsLog GitHub Wiki**. GitHub stores a wiki
|
||||
as its own git repository (`<repo>.wiki.git`), so publishing means pushing these
|
||||
`.md` files into it.
|
||||
|
||||
## Automatic — via the release script (recommended)
|
||||
|
||||
The release task (`.vscode/release.ps1`, VS Code → *Tasks: Run Task* → *Release
|
||||
OpsLog*) **publishes the wiki automatically** at the end of every release: it
|
||||
clones `github.com/GregTroar/OpsLog.wiki.git` to a temp folder, copies every page
|
||||
from this `wiki/` folder (except this guide), and pushes only if something
|
||||
changed. Auth reuses your GitHub git credential (set up by `gh auth login`).
|
||||
|
||||
**One-time:** on GitHub, open the repo → **Wiki** tab → *Create the first page* →
|
||||
Save. This initialises the wiki repo so the script's clone succeeds.
|
||||
|
||||
## Manual (if you want to push between releases)
|
||||
|
||||
```bash
|
||||
# One-time: clone the wiki repo NEXT TO the project (not inside it)
|
||||
git clone https://github.com/GregTroar/OpsLog.wiki.git
|
||||
|
||||
# Then, to update:
|
||||
cp wiki/*.md ../OpsLog.wiki/ # (skip README-PUBLISHING.md if you like)
|
||||
cd ../OpsLog.wiki
|
||||
git add . && git commit -m "Update wiki" && git push
|
||||
```
|
||||
|
||||
The pages appear immediately under the repository's **Wiki** tab.
|
||||
|
||||
## How the pages work
|
||||
|
||||
- Each `.md` file is one wiki page. The **file name** becomes the page title and
|
||||
URL: `Getting-Started.md` → page *Getting Started* at `.../wiki/Getting-Started`.
|
||||
- **`_Sidebar.md`** renders as the navigation sidebar on every page.
|
||||
- **`_Footer.md`** renders at the bottom of every page.
|
||||
- Link between pages with `[[Page Name]]` (wiki-link) or a normal relative link
|
||||
`[text](Getting-Started)`.
|
||||
- `Home.md` is the wiki landing page.
|
||||
|
||||
## Keep it in sync with the README
|
||||
|
||||
The wiki mirrors [`README.md`](../README.md). When you add a feature, update the
|
||||
matching wiki page and the README together so they don't drift.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Recent QSOs and Filters
|
||||
|
||||
## The Recent QSOs grid
|
||||
|
||||
The main log table. Double-click a row to edit; right-click for bulk actions
|
||||
(see [[Logging Basics]]). Columns are configurable — click **Columns** to choose
|
||||
which are visible; your selection is saved (per profile).
|
||||
|
||||
### Award columns
|
||||
|
||||
Each defined award can show a column with the reference the QSO counts for. These
|
||||
columns are **hidden by default** and opt-in from the Columns picker — turn on
|
||||
only the awards you're chasing. Your choice persists across restarts.
|
||||
|
||||
## Advanced filter builder
|
||||
|
||||
Click the filter button to open the **QSO filter builder** (Log4OM-style):
|
||||
|
||||
- Add one or more **conditions**: *field · operator · value*.
|
||||
- Operators: equals, not-equal, contains, starts/ends with, greater/less
|
||||
(or equal), is empty, is not empty.
|
||||
- Join conditions with **ALL (AND)** or **ANY (OR)**.
|
||||
- Save named **presets** (they travel with the `data/` folder).
|
||||
- The field dropdown is sorted alphabetically to find things fast.
|
||||
|
||||
The filtered view can be exported: **selected rows** or the **whole filtered
|
||||
view** (no row limit) → ADIF or Cabrillo.
|
||||
|
||||
## Worked-before matrix
|
||||
|
||||
As you type a callsign, the worked-before grid shows prior contacts with that
|
||||
**entity**, arranged by band/mode slot, so you instantly see what's new (new
|
||||
DXCC / new band / new slot) versus already worked.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Remote Icom over the Internet
|
||||
|
||||
OpsLog speaks the Icom radio's **built-in network protocol** directly, so it
|
||||
**replaces both the Icom Remote Utility and RS-BA1**. Radios with a LAN server —
|
||||
**IC-7610, IC-9700, IC-705, IC-7760, IC-905** — can be operated over the
|
||||
LAN/internet with the full [[Icom]] console.
|
||||
|
||||
> The **IC-9100 has no LAN server** — it is USB-only.
|
||||
|
||||
## On the radio (one-time)
|
||||
|
||||
In the radio's **Network** menu:
|
||||
|
||||
1. Enable the network / *Remote settings*.
|
||||
2. Set a **Network User1** name and password.
|
||||
3. Note the radio's **IP address** and its **CI-V address**.
|
||||
4. Make sure the Icom **Remote Utility is closed** on every PC — OpsLog binds the
|
||||
same local UDP ports, so they can't both run.
|
||||
|
||||
## In OpsLog
|
||||
|
||||
Settings → CAT → backend **Icom CI-V (network / remote)**:
|
||||
|
||||
- **Host** — the radio's IP or hostname.
|
||||
- **User / Password** — the Network User1 credentials.
|
||||
- **Rig model / CI-V address** — pick the model.
|
||||
|
||||
Save. OpsLog logs in, obtains a session **token** (auto-renewed so control never
|
||||
drops after ~2 minutes), opens the **CI-V tunnel**, and the whole console works
|
||||
over the net — freq / mode / DSP / TX / RIT / CW / scope.
|
||||
|
||||
## How it stays solid
|
||||
|
||||
- **Token renewal** every ~45 s keeps the session alive.
|
||||
- **Receive-side retransmit**: under the heavy panadapter stream, lost UDP
|
||||
packets are re-requested, so the link doesn't drop when the scope is on.
|
||||
- **Standby-tolerant**: the radio's server answers pings even when the radio is
|
||||
off, so the session doesn't flap — the console + power ON button stay reachable.
|
||||
- **Manual power** ON / OFF (the app never wakes the rig on connect).
|
||||
|
||||
## Audio
|
||||
|
||||
Network **audio** is a work in progress. For now, use the radio in **USB + a
|
||||
voice link** (e.g. Mumble) alongside the network CAT, or run OpsLog on the
|
||||
station PC via remote desktop. See [[Audio and Keyers]] for the USB-monitor
|
||||
option.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Login rejected"** → wrong Network User1 name/password.
|
||||
- **Can't bind local port / dial fails** → the Icom **Remote Utility** is still
|
||||
running; close it.
|
||||
- **Link drops when the scope turns on** → should be fixed by receive-side
|
||||
retransmit; if it persists, check the log (`icom net:` lines) and network
|
||||
quality. See [[Troubleshooting]].
|
||||
@@ -0,0 +1,24 @@
|
||||
# Security
|
||||
|
||||
## Secret vault
|
||||
|
||||
Opt-in **passphrase encryption** of the stored passwords (QRZ, cluster, eQSL,
|
||||
SMTP, Icom network, etc.) using **AES-GCM + PBKDF2**.
|
||||
|
||||
- Enable it in Settings; you set a passphrase.
|
||||
- Encrypted values are marked `enc:v1:` and are **portable** (they travel with
|
||||
the `data/` folder).
|
||||
- A single **unlock prompt at launch** decrypts them for the session.
|
||||
|
||||
If you forget the passphrase, the encrypted secrets can't be recovered — re-enter
|
||||
them.
|
||||
|
||||
## What OpsLog sends where
|
||||
|
||||
- **Callsign lookups** go to QRZ.com / HamQTH with your credentials.
|
||||
- **QSL uploads/downloads** go to the services you configure (LoTW via TQSL, QRZ,
|
||||
eQSL, ClubLog, HRDLog).
|
||||
- **Telemetry** is a once-a-day anonymous heartbeat (random install ID + version
|
||||
+ OS — no callsign, no QSO data). Opt out in Preferences.
|
||||
- Everything else stays local (or on your own MySQL / web server for the
|
||||
multi-op live status).
|
||||
@@ -0,0 +1,51 @@
|
||||
# Settings and Data
|
||||
|
||||
## Where everything lives
|
||||
|
||||
A `data/` folder is created **next to `OpsLog.exe`**:
|
||||
|
||||
- **Config** (SQLite): settings, profiles, rigs/antennas, cluster nodes, lookup
|
||||
cache, award lists, QSL templates. Always local, so the UI is instant.
|
||||
- **Local logbook** (SQLite): QSOs, when the profile logs locally.
|
||||
- `data/qsl/templates/…` — QSL card templates and their photos.
|
||||
- `data/qsl/outbox/…` — sent QSL card JPEGs.
|
||||
- Recordings folder (configurable) — per-QSO WAV/MP3.
|
||||
- Backups.
|
||||
|
||||
Back up your setup by copying `data/` (and keep it with the exe). See
|
||||
[[Profiles and Databases]] for local-vs-MySQL logbooks.
|
||||
|
||||
## Settings map
|
||||
|
||||
| Section | What |
|
||||
|---------|------|
|
||||
| **General** | language, theme, Main-view panes, live-status publish |
|
||||
| **Station** | your callsign, grid, name, address (my-station ADIF) |
|
||||
| **CAT** | radio backend + connection ([[CAT Control]]) |
|
||||
| **Lookup** | QRZ.com / HamQTH credentials |
|
||||
| **FlexRadio** | per-band RX/TX antennas |
|
||||
| **Audio** | DVK / recorder devices, PTT, gains, RX monitor |
|
||||
| **CW Keyer** | WinKeyer / Icom / TCI engine + macros |
|
||||
| **External services** | LoTW (TQSL), QRZ, eQSL, ClubLog, HRDLog, POTA |
|
||||
| **Email** | SMTP for eQSL-by-email |
|
||||
| **PowerGenius / Antenna Genius / Rotator / Ultrabeam** | station hardware |
|
||||
| **Backup** | database + ADIF backup at shutdown |
|
||||
|
||||
## Appearance & language
|
||||
|
||||
Four themes (Warm light, Warm dark, Graphite dark, High contrast) plus **Auto**
|
||||
(follows the OS). Full **English / French** UI with a first-run flag chooser and
|
||||
a switcher in Settings → General.
|
||||
|
||||
## Integrations (outbound UDP)
|
||||
|
||||
Push the current frequency to **PstRotator**, radio info in **N1MM `RadioInfo`**
|
||||
format, or an **ADIF record on each logged QSO** — so external tools stay in sync.
|
||||
|
||||
## Other
|
||||
|
||||
- **Autostart** external programs (WSJT-X, JTAlert, rotator control…) at launch,
|
||||
skipping any already running.
|
||||
- **Update check** at startup (toggleable).
|
||||
- **Anonymous usage telemetry** — a once-a-day heartbeat (random install ID +
|
||||
version + OS; no callsign or QSO data). Opt out in Preferences.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Where's the log file?
|
||||
|
||||
OpsLog writes a diagnostic log. The path is shown in Settings (and can be opened
|
||||
from there). When reporting a problem, include the relevant lines — they're
|
||||
prefixed by subsystem (`Flex:`, `icom net:`, `icom scope`, `antgenius:`,
|
||||
`cluster:`, `qslmgr:log`, `audio:`…).
|
||||
|
||||
## CAT
|
||||
|
||||
- **Radio not connecting** — check the backend fields (COM port + baud for USB;
|
||||
IP/credentials for network). OpsLog connects non-blocking, so it won't freeze —
|
||||
watch the CAT status.
|
||||
- **Icom: no CAT at all over USB** — the CI-V address must match the radio; pick
|
||||
your **model** from the dropdown (it sets the address) and set *CI-V USB Echo
|
||||
Back* **OFF** on the rig.
|
||||
- **Icom scope blank on an IC-7300** — this was a bug where the scope decoder was
|
||||
chosen from the configured CI-V address; it now uses the model detected from the
|
||||
CI-V ID, so a single-scope rig run at address `0x98` decodes correctly. Update
|
||||
to the latest build.
|
||||
- **Icom attenuator button does nothing** — the dB steps are model-specific; make
|
||||
sure the right model is selected (IC-7300 = 20 dB, IC-7610 = 6/12/18 dB).
|
||||
|
||||
## Remote Icom
|
||||
|
||||
- **Login rejected** → wrong Network User1 name/password.
|
||||
- **Can't bind local port** → the Icom **Remote Utility is still running** —
|
||||
close it.
|
||||
- **Link drops with the scope on** → receive-side retransmit should hold it; if
|
||||
not, check network quality and the `icom net:` log lines. See
|
||||
[[Remote Icom over the Internet]].
|
||||
|
||||
## FlexRadio
|
||||
|
||||
- **A control reads 0 / stale at startup** — some SmartSDR status fields use short
|
||||
names (e.g. the TX filter is reported as `lo`/`hi`, not `filter_low/high`). If a
|
||||
value never populates, capture the `Flex: … status` log line and report it.
|
||||
- **Antenna wrong band on a spot click** — fixed: `SetFrequency`/`SetMode` update
|
||||
the slice cache immediately so the panel and the Ultrabeam follow don't chase a
|
||||
stale frequency.
|
||||
|
||||
## Multi-op live status PHP
|
||||
|
||||
`Connection refused` from the PHP page is almost always MySQL `bind-address =
|
||||
127.0.0.1` or a firewall/grant issue — see
|
||||
[[Multi-Operator Live Status]].
|
||||
|
||||
## DVK / PTT
|
||||
|
||||
- **Test PTT does nothing on CAT/OmniRig** — the key is held ~1.5 s so the async
|
||||
OmniRig write is actually sent; if it still doesn't key, the rig's OmniRig
|
||||
profile may not expose CAT TX keying — use RTS/DTR or VOX.
|
||||
|
||||
## Still stuck?
|
||||
|
||||
Open an issue with: what you did, what happened, your radio/model, and the
|
||||
relevant log lines.
|
||||
@@ -0,0 +1,2 @@
|
||||
---
|
||||
**OpsLog** — a modern ham-radio logger by **F4BPO** · [Home](Home) · [Troubleshooting](Troubleshooting) · A French README is in `README.fr.md`.
|
||||
@@ -0,0 +1,37 @@
|
||||
### OpsLog Wiki
|
||||
|
||||
**Start here**
|
||||
- [[Home]]
|
||||
- [[Installation]]
|
||||
- [[Getting Started]]
|
||||
|
||||
**Logging**
|
||||
- [[Logging Basics]]
|
||||
- [[Recent QSOs and Filters]]
|
||||
- [[Import and Export ADIF]]
|
||||
- [[Profiles and Databases]]
|
||||
|
||||
**Radio control (CAT)**
|
||||
- [[CAT Control]]
|
||||
- [[FlexRadio]]
|
||||
- [[Icom]]
|
||||
- [[Remote Icom over the Internet]]
|
||||
|
||||
**Operating**
|
||||
- [[DX Cluster and Spots]]
|
||||
- [[Maps and Antennas]]
|
||||
- [[Amplifiers and Switches]]
|
||||
- [[Audio and Keyers]]
|
||||
- [[Contest Logging]]
|
||||
- [[Net Control]]
|
||||
- [[Multi-Operator Live Status]]
|
||||
|
||||
**QSL & Awards**
|
||||
- [[Awards]]
|
||||
- [[QSL Management]]
|
||||
- [[QSL Card Designer]]
|
||||
|
||||
**Reference**
|
||||
- [[Settings and Data]]
|
||||
- [[Security]]
|
||||
- [[Troubleshooting]]
|
||||
Reference in New Issue
Block a user