feat: Added chat when MySQL is in use
This commit is contained in:
@@ -3766,7 +3766,11 @@ func (a *App) TestLookupProvider(name, callsign, user, password string) (lookup.
|
|||||||
}
|
}
|
||||||
r, err := p.Lookup(a.ctx, callsign)
|
r, err := p.Lookup(a.ctx, callsign)
|
||||||
if errors.Is(err, lookup.ErrNotFound) {
|
if errors.Is(err, lookup.ErrNotFound) {
|
||||||
return lookup.Result{}, fmt.Errorf("%s reachable but %q not found (creds look OK)", name, callsign)
|
// A "not found" (vs an auth error) means the login WORKED — the test
|
||||||
|
// callsign just isn't in this database, common for special-event calls
|
||||||
|
// (e.g. TM74TFR not on HamQTH). The credentials are valid, so lookups of
|
||||||
|
// other callsigns will work: report success, not failure.
|
||||||
|
return lookup.Result{Source: name, Callsign: callsign, Name: "credentials OK — " + callsign + " not in " + name}, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return lookup.Result{}, err
|
return lookup.Result{}, err
|
||||||
@@ -6990,6 +6994,12 @@ func (a *App) FlexSetCWFilter(bw int) error {
|
|||||||
}
|
}
|
||||||
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetCWFilter(bw) })
|
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetCWFilter(bw) })
|
||||||
}
|
}
|
||||||
|
func (a *App) FlexSetFilter(lo int, hi int) error {
|
||||||
|
if a.cat == nil {
|
||||||
|
return fmt.Errorf("cat not initialized")
|
||||||
|
}
|
||||||
|
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetFilter(lo, hi) })
|
||||||
|
}
|
||||||
|
|
||||||
// SwitchCATRig hot-swaps the active OmniRig slot (Rig1 ↔ Rig2) without
|
// SwitchCATRig hot-swaps the active OmniRig slot (Rig1 ↔ Rig2) without
|
||||||
// requiring a trip through the full Settings panel. Persists the choice
|
// requiring a trip through the full Settings panel. Persists the choice
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -157,6 +158,7 @@ func (a *App) GetOnlineOperators() ([]ChatPresence, error) {
|
|||||||
func (a *App) chatLoop() {
|
func (a *App) chatLoop() {
|
||||||
defer func() { _ = recover() }()
|
defer func() { _ = recover() }()
|
||||||
var lastID int64 = -1 // -1 = not yet baselined
|
var lastID int64 = -1 // -1 = not yet baselined
|
||||||
|
var lastDB *sql.DB // logbook the baseline belongs to
|
||||||
lastPresence := time.Time{}
|
lastPresence := time.Time{}
|
||||||
lastPurge := time.Time{}
|
lastPurge := time.Time{}
|
||||||
t := time.NewTicker(chatPollInterval)
|
t := time.NewTicker(chatPollInterval)
|
||||||
@@ -164,8 +166,15 @@ func (a *App) chatLoop() {
|
|||||||
for range t.C {
|
for range t.C {
|
||||||
if !a.chatActive() {
|
if !a.chatActive() {
|
||||||
lastID = -1 // re-baseline if the backend changes
|
lastID = -1 // re-baseline if the backend changes
|
||||||
|
lastDB = nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Profile switch swaps the logbook under us: re-baseline against the new
|
||||||
|
// DB so we don't query it with the previous log's id cursor.
|
||||||
|
if a.logDb != lastDB {
|
||||||
|
lastID = -1
|
||||||
|
lastDB = a.logDb
|
||||||
|
}
|
||||||
if err := a.ensureChatTables(); err != nil {
|
if err := a.ensureChatTables(); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-4
@@ -630,6 +630,7 @@ export default function App() {
|
|||||||
const [chatMsgs, setChatMsgs] = useState<ChatMsg[]>([]);
|
const [chatMsgs, setChatMsgs] = useState<ChatMsg[]>([]);
|
||||||
const [chatOnline, setChatOnline] = useState<ChatPresence[]>([]);
|
const [chatOnline, setChatOnline] = useState<ChatPresence[]>([]);
|
||||||
const [chatUnread, setChatUnread] = useState(0);
|
const [chatUnread, setChatUnread] = useState(0);
|
||||||
|
const [chatEpoch, setChatEpoch] = useState(0); // bumped on profile switch to reload the chat for the new logbook
|
||||||
const chatOpenRef = useRef(chatOpen); chatOpenRef.current = chatOpen;
|
const chatOpenRef = useRef(chatOpen); chatOpenRef.current = chatOpen;
|
||||||
const chatSeen = useRef<Set<number>>(new Set());
|
const chatSeen = useRef<Set<number>>(new Set());
|
||||||
// Availability (only on a shared MySQL logbook; re-checked as profiles switch).
|
// Availability (only on a shared MySQL logbook; re-checked as profiles switch).
|
||||||
@@ -667,7 +668,7 @@ export default function App() {
|
|||||||
lo();
|
lo();
|
||||||
const id = window.setInterval(lo, 15000);
|
const id = window.setInterval(lo, 15000);
|
||||||
return () => window.clearInterval(id);
|
return () => window.clearInterval(id);
|
||||||
}, [chatOpen]);
|
}, [chatOpen, chatEpoch]);
|
||||||
async function chatSend(t: string) {
|
async function chatSend(t: string) {
|
||||||
try {
|
try {
|
||||||
const m = (await SendChatMessage(t)) as any as ChatMsg;
|
const m = (await SendChatMessage(t)) as any as ChatMsg;
|
||||||
@@ -1455,6 +1456,11 @@ export default function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const off = EventsOn('profile:changed', () => {
|
const off = EventsOn('profile:changed', () => {
|
||||||
loadStation(); loadLists(); loadCATCfg(); reloadWk(); loadMainPanes();
|
loadStation(); loadLists(); loadCATCfg(); reloadWk(); loadMainPanes();
|
||||||
|
// The chat is per shared logbook — clear the previous profile's messages
|
||||||
|
// and reload for the new logbook (or hide if it isn't a MySQL log).
|
||||||
|
setChatMsgs([]); chatSeen.current.clear(); setChatOnline([]); setChatUnread(0);
|
||||||
|
ChatAvailable().then((v: any) => setChatAvailable(!!v)).catch(() => setChatAvailable(false));
|
||||||
|
setChatEpoch((e) => e + 1);
|
||||||
});
|
});
|
||||||
return () => { off(); };
|
return () => { off(); };
|
||||||
}, [loadStation, loadLists, loadCATCfg, reloadWk, loadMainPanes]);
|
}, [loadStation, loadLists, loadCATCfg, reloadWk, loadMainPanes]);
|
||||||
@@ -3209,9 +3215,14 @@ export default function App() {
|
|||||||
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath))) && (
|
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath))) && (
|
||||||
<div className="flex-1 min-w-0 min-h-0 flex gap-2.5 items-stretch">
|
<div className="flex-1 min-w-0 min-h-0 flex gap-2.5 items-stretch">
|
||||||
{chatShown && (
|
{chatShown && (
|
||||||
<div className="w-[280px] shrink-0 min-h-0">
|
// relative + absolute inner: the chat takes the row height (set by the
|
||||||
<ChatPanel msgs={chatMsgs} online={chatOnline} myCall={station.callsign}
|
// entry strip) WITHOUT its message list growing the row, like the
|
||||||
onSend={chatSend} onClose={() => setChatOpen(false)} />
|
// Stats panel. The list scrolls inside this fixed height.
|
||||||
|
<div className="w-[280px] shrink-0 min-h-0 relative">
|
||||||
|
<div className="absolute inset-0 flex flex-col min-h-0">
|
||||||
|
<ChatPanel msgs={chatMsgs} online={chatOnline} myCall={station.callsign}
|
||||||
|
onSend={chatSend} onClose={() => setChatOpen(false)} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Rotor compass: azimuth dial + needles + click-to-turn. Shows when a
|
{/* Rotor compass: azimuth dial + needles + click-to-turn. Shows when a
|
||||||
|
|||||||
@@ -36,10 +36,22 @@ export function ChatPanel({ msgs, online, myCall, onSend, onClose }: {
|
|||||||
<MessageSquare className="size-4 text-sky-600" />
|
<MessageSquare className="size-4 text-sky-600" />
|
||||||
<span className="text-xs font-bold uppercase tracking-wider text-foreground/80">Chat</span>
|
<span className="text-xs font-bold uppercase tracking-wider text-foreground/80">Chat</span>
|
||||||
<span className="flex-1" />
|
<span className="flex-1" />
|
||||||
<Users className="size-3.5 text-muted-foreground" />
|
{/* Online count — hover to see who's connected. */}
|
||||||
<span className="text-[11px] text-muted-foreground" title={online.map((o) => o.operator).join(', ')}>
|
<div className="relative group">
|
||||||
{online.length}
|
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground cursor-default">
|
||||||
</span>
|
<Users className="size-3.5" />{online.length}
|
||||||
|
</span>
|
||||||
|
{online.length > 0 && (
|
||||||
|
<div className="hidden group-hover:block absolute right-0 top-5 z-20 min-w-[130px] rounded-md border border-border bg-popover shadow-lg p-1.5">
|
||||||
|
<div className="text-[9px] uppercase tracking-wider text-muted-foreground mb-1">Online</div>
|
||||||
|
{online.map((o) => (
|
||||||
|
<div key={o.operator} className="font-mono text-[11px] whitespace-nowrap">
|
||||||
|
{o.operator}{o.station && o.station.toUpperCase() !== o.operator.toUpperCase() ? <span className="text-muted-foreground"> · {o.station}</span> : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button type="button" onClick={onClose} className="ml-1 text-muted-foreground hover:text-foreground" title="Close">
|
<button type="button" onClick={onClose} className="ml-1 text-muted-foreground hover:text-foreground" title="Close">
|
||||||
<X className="size-3.5" />
|
<X className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, detai
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
<div className={cn('flex-1 min-h-0', open === 'stats' ? 'overflow-hidden' : 'overflow-y-auto')}>
|
||||||
{open === 'stats' && (
|
{open === 'stats' && (
|
||||||
<div className="px-3 py-2.5">
|
<div className="px-3 py-2.5">
|
||||||
<BandSlotGrid wb={wb} busy={!!wbBusy} currentBand={band} currentMode={mode} bands={bands} hasCall={callsign.trim() !== ''} />
|
<BandSlotGrid wb={wb} busy={!!wbBusy} currentBand={band} currentMode={mode} bands={bands} hasCall={callsign.trim() !== ''} />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel,
|
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel,
|
||||||
FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel,
|
FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel,
|
||||||
FlexSetAPF, FlexSetAPFLevel, FlexSetCWSpeed, FlexSetCWPitch, FlexSetCWBreakInDelay,
|
FlexSetAPF, FlexSetAPFLevel, FlexSetCWSpeed, FlexSetCWPitch, FlexSetCWBreakInDelay,
|
||||||
FlexSetCWSidetone, FlexSetSidetoneLevel, FlexSetCWFilter,
|
FlexSetCWSidetone, FlexSetSidetoneLevel, FlexSetCWFilter, FlexSetFilter,
|
||||||
} from '../../wailsjs/go/main/App';
|
} from '../../wailsjs/go/main/App';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -150,14 +150,17 @@ function MeterBar({ label, value, unit, lo, hi, accent = '#16a34a', extra, displ
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-[2px] h-2.5 items-stretch">
|
{/* 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]">
|
||||||
{Array.from({ length: METER_SEGMENTS }).map((_, i) => {
|
{Array.from({ length: METER_SEGMENTS }).map((_, i) => {
|
||||||
const on = i < lit;
|
const on = i < lit;
|
||||||
const frac = i / METER_SEGMENTS;
|
const frac = i / METER_SEGMENTS;
|
||||||
const col = segColor ? segColor(frac) : (frac > 0.82 ? '#dc2626' : accent);
|
const col = segColor ? segColor(frac) : (frac > 0.82 ? '#dc2626' : accent);
|
||||||
return (
|
return (
|
||||||
<div key={i} className="flex-1 rounded-[1.5px] transition-colors duration-100"
|
<div key={i} className="flex-1 rounded-[2px] transition-colors duration-100"
|
||||||
style={on ? { background: col, boxShadow: `0 0 3px ${col}66` } : { background: '#cfc6ad', opacity: 0.45 }} />
|
style={on
|
||||||
|
? { background: `linear-gradient(to bottom, ${col}, ${col}cc)`, boxShadow: `0 0 4px ${col}88` }
|
||||||
|
: { background: '#cfc6ad', opacity: 0.35 }} />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -224,6 +227,7 @@ export function FlexPanel() {
|
|||||||
const PROC = [{ v: '0', l: 'NOR' }, { v: '1', l: 'DX' }, { v: '2', l: 'DX+' }];
|
const PROC = [{ v: '0', l: 'NOR' }, { v: '1', l: 'DX' }, { v: '2', l: 'DX+' }];
|
||||||
const AGC = [{ v: 'off', l: 'OFF' }, { v: 'slow', l: 'SLOW' }, { v: 'med', l: 'MED' }, { v: 'fast', l: 'FAST' }];
|
const AGC = [{ v: 'off', l: 'OFF' }, { v: 'slow', l: 'SLOW' }, { v: 'med', l: 'MED' }, { v: 'fast', l: 'FAST' }];
|
||||||
const CW_BW = [100, 200, 300, 400, 500];
|
const CW_BW = [100, 200, 300, 400, 500];
|
||||||
|
const SSB_BW = [1800, 2100, 2400, 2800, 3000, 4000, 6000];
|
||||||
const curBW = Math.max(0, (st.filter_hi || 0) - (st.filter_lo || 0));
|
const curBW = Math.max(0, (st.filter_hi || 0) - (st.filter_lo || 0));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -357,9 +361,12 @@ export function FlexPanel() {
|
|||||||
<LevelRow label="NR" on={st.nr} disabled={rxOff} value={st.nr_level} accent="cyan" sliderAccent="#0891b2"
|
<LevelRow label="NR" on={st.nr} disabled={rxOff} value={st.nr_level} accent="cyan" sliderAccent="#0891b2"
|
||||||
onToggle={() => change('nr', !st.nr, () => FlexSetNR(!st.nr))}
|
onToggle={() => change('nr', !st.nr, () => FlexSetNR(!st.nr))}
|
||||||
onLevel={(v) => change('nr_level', v, () => FlexSetNRLevel(v))} />
|
onLevel={(v) => change('nr_level', v, () => FlexSetNRLevel(v))} />
|
||||||
<LevelRow label="ANF" on={st.anf} disabled={rxOff} value={st.anf_level} accent="violet" sliderAccent="#7c3aed"
|
{/* ANF (auto notch) is for carriers in voice — meaningless on a CW tone, so hide it in CW. */}
|
||||||
onToggle={() => change('anf', !st.anf, () => FlexSetANF(!st.anf))}
|
{!isCW && (
|
||||||
onLevel={(v) => change('anf_level', v, () => FlexSetANFLevel(v))} />
|
<LevelRow label="ANF" on={st.anf} disabled={rxOff} value={st.anf_level} accent="violet" sliderAccent="#7c3aed"
|
||||||
|
onToggle={() => change('anf', !st.anf, () => FlexSetANF(!st.anf))}
|
||||||
|
onLevel={(v) => change('anf_level', v, () => FlexSetANFLevel(v))} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isCW && (
|
{isCW && (
|
||||||
<div className="border-t border-border/60 pt-3 space-y-3">
|
<div className="border-t border-border/60 pt-3 space-y-3">
|
||||||
@@ -382,6 +389,31 @@ export function FlexPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!isCW && (
|
||||||
|
<div className="border-t border-border/60 pt-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">Filter</span>
|
||||||
|
<div className="inline-flex rounded-md border border-border overflow-hidden">
|
||||||
|
{SSB_BW.map((bw) => (
|
||||||
|
<button key={bw} type="button" disabled={rxOff}
|
||||||
|
onClick={() => {
|
||||||
|
const lsb = (st.mode || '').toUpperCase().includes('LSB');
|
||||||
|
let lo: number, hi: number;
|
||||||
|
if (lsb) { const near = (st.filter_hi && st.filter_hi < 0) ? st.filter_hi : -100; hi = near; lo = near - bw; }
|
||||||
|
else { const near = (st.filter_lo && st.filter_lo > 0) ? st.filter_lo : 100; lo = near; hi = near + bw; }
|
||||||
|
setSt((p) => ({ ...p, filter_lo: lo, filter_hi: hi }));
|
||||||
|
FlexSetFilter(lo, hi).catch(() => {});
|
||||||
|
}}
|
||||||
|
className={cn('px-1.5 py-1 text-[11px] font-bold tracking-wide transition-colors disabled:opacity-30 border-l border-border first:border-l-0',
|
||||||
|
Math.abs(curBW - bw) <= 50 ? 'bg-primary text-primary-foreground' : 'bg-card text-muted-foreground hover:bg-muted')}>
|
||||||
|
{(bw / 1000).toFixed(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-muted-foreground/70 font-mono">kHz</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -403,7 +435,6 @@ export function FlexPanel() {
|
|||||||
<span className="px-2 py-1 rounded bg-rose-100 text-rose-800 text-xs font-bold">FAULT: {st.amp_fault}</span>
|
<span className="px-2 py-1 rounded bg-rose-100 text-rose-800 text-xs font-bold">FAULT: {st.amp_fault}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] text-muted-foreground">Amplifier power / SWR / temperature appear in the Meters panel below (src AMP).</p>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -439,7 +470,7 @@ export function FlexPanel() {
|
|||||||
const cur = [
|
const cur = [
|
||||||
sig && (() => { const dbm = peakHold('s', sig.value); const s = sUnit(dbm); return (
|
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`}
|
<MeterBar key="s" label="S-METER" value={s.bar} lo={0} hi={19} accent="#16a34a" display={s.display} extra={`${dbm.toFixed(1)} dBm`}
|
||||||
segColor={(fr) => { const sv = fr * 19; return sv < 9 ? '#16a34a' : sv < 14 ? '#f59e0b' : '#dc2626'; }} />
|
segColor={(fr) => { const sv = fr * 19; return sv < 9 ? '#16a34a' : sv < 12.33 ? '#f59e0b' : '#dc2626'; }} />
|
||||||
); })(),
|
); })(),
|
||||||
fwd && (() => { const w = peakHold('p', isDbm(fwd) ? dbmToW(fwd.value) : fwd.value); return (
|
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"
|
<MeterBar key="p" label="PWR" unit="W" lo={0} hi={120} accent="#dc2626"
|
||||||
|
|||||||
Vendored
+2
@@ -159,6 +159,8 @@ export function FlexSetCWSidetone(arg1:boolean):Promise<void>;
|
|||||||
|
|
||||||
export function FlexSetCWSpeed(arg1:number):Promise<void>;
|
export function FlexSetCWSpeed(arg1:number):Promise<void>;
|
||||||
|
|
||||||
|
export function FlexSetFilter(arg1:number,arg2:number):Promise<void>;
|
||||||
|
|
||||||
export function FlexSetMic(arg1:number):Promise<void>;
|
export function FlexSetMic(arg1:number):Promise<void>;
|
||||||
|
|
||||||
export function FlexSetMon(arg1:boolean):Promise<void>;
|
export function FlexSetMon(arg1:boolean):Promise<void>;
|
||||||
|
|||||||
@@ -290,6 +290,10 @@ export function FlexSetCWSpeed(arg1) {
|
|||||||
return window['go']['main']['App']['FlexSetCWSpeed'](arg1);
|
return window['go']['main']['App']['FlexSetCWSpeed'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function FlexSetFilter(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['FlexSetFilter'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
export function FlexSetMic(arg1) {
|
export function FlexSetMic(arg1) {
|
||||||
return window['go']['main']['App']['FlexSetMic'](arg1);
|
return window['go']['main']['App']['FlexSetMic'](arg1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ type FlexController interface {
|
|||||||
SetCWSidetone(bool) error
|
SetCWSidetone(bool) error
|
||||||
SetSidetoneLevel(int) error
|
SetSidetoneLevel(int) error
|
||||||
SetCWFilter(int) error
|
SetCWFilter(int) error
|
||||||
|
SetFilter(lo, hi int) error
|
||||||
// External amplifier (PowerGenius XL) operate/standby.
|
// External amplifier (PowerGenius XL) operate/standby.
|
||||||
SetAmpOperate(bool) error
|
SetAmpOperate(bool) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1220,6 +1220,27 @@ func (f *Flex) SetCWFilter(bw int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFilter sets the active RX slice's passband to an explicit low/high cut (Hz,
|
||||||
|
// audio offsets; negative for LSB). Used by the SSB width presets, where the
|
||||||
|
// frontend keeps the carrier-side edge and extends the far edge.
|
||||||
|
func (f *Flex) SetFilter(lo, hi int) error {
|
||||||
|
f.mu.Lock()
|
||||||
|
idx, rx := f.rxSliceLocked()
|
||||||
|
connected := f.conn != nil
|
||||||
|
if rx != nil {
|
||||||
|
rx.filterLo, rx.filterHi = lo, hi
|
||||||
|
}
|
||||||
|
f.mu.Unlock()
|
||||||
|
if !connected {
|
||||||
|
return fmt.Errorf("flex: not connected")
|
||||||
|
}
|
||||||
|
if rx == nil || idx < 0 {
|
||||||
|
return fmt.Errorf("flex: no receive slice")
|
||||||
|
}
|
||||||
|
f.send(fmt.Sprintf("filt %d %d %d", idx, lo, hi))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// boolWord renders a Flex on/off boolean as the word form some commands want.
|
// boolWord renders a Flex on/off boolean as the word form some commands want.
|
||||||
func boolWord(on bool) string {
|
func boolWord(on bool) string {
|
||||||
if on {
|
if on {
|
||||||
|
|||||||
Reference in New Issue
Block a user