fix: zoom taking too long after clicking a spot
This commit is contained in:
@@ -7440,6 +7440,13 @@ func (a *App) FlexSetTXAntenna(ant string) error {
|
|||||||
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetTXAntenna(ant) })
|
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetTXAntenna(ant) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) FlexSetSplit(on bool) error {
|
||||||
|
if a.cat == nil {
|
||||||
|
return fmt.Errorf("cat not initialized")
|
||||||
|
}
|
||||||
|
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetSplit(on) })
|
||||||
|
}
|
||||||
|
|
||||||
// keyFlexBandAnt stores the per-band RX/TX antenna map (global, machine-local).
|
// keyFlexBandAnt stores the per-band RX/TX antenna map (global, machine-local).
|
||||||
const keyFlexBandAnt = "flex.band_antennas"
|
const keyFlexBandAnt = "flex.band_antennas"
|
||||||
|
|
||||||
|
|||||||
@@ -818,6 +818,7 @@ export default function App() {
|
|||||||
// worked-before grid. Per-profile (stored via SetUIPref → profile-prefixed),
|
// worked-before grid. Per-profile (stored via SetUIPref → profile-prefixed),
|
||||||
// so it's loaded async on mount and re-read on profile:changed below.
|
// so it's loaded async on mount and re-read on profile:changed below.
|
||||||
type MainPaneKind = 'map1' | 'map2' | 'cluster' | 'worked' | 'flex' | 'recent';
|
type MainPaneKind = 'map1' | 'map2' | 'cluster' | 'worked' | 'flex' | 'recent';
|
||||||
|
const [mapZoomSignal, setMapZoomSignal] = useState(0); // bump → world map auto-zooms now
|
||||||
const [mainPaneLeft, setMainPaneLeft] = useState<MainPaneKind>('map1');
|
const [mainPaneLeft, setMainPaneLeft] = useState<MainPaneKind>('map1');
|
||||||
const [mainPaneRight, setMainPaneRight] = useState<MainPaneKind>('map2');
|
const [mainPaneRight, setMainPaneRight] = useState<MainPaneKind>('map2');
|
||||||
const loadMainPanes = useCallback(async () => {
|
const loadMainPanes = useCallback(async () => {
|
||||||
@@ -2028,6 +2029,9 @@ export default function App() {
|
|||||||
qsl_via: d.qsl_via || (r.qsl_via ?? ''),
|
qsl_via: d.qsl_via || (r.qsl_via ?? ''),
|
||||||
}));
|
}));
|
||||||
if (r.dxcc && r.dxcc > 0) runWorkedBefore(call, r.dxcc);
|
if (r.dxcc && r.dxcc > 0) runWorkedBefore(call, r.dxcc);
|
||||||
|
// The DX location is now known (grid set above) — force the world map to
|
||||||
|
// auto-zoom right away, so it doesn't lag behind the resolved QSO.
|
||||||
|
setMapZoomSignal((n) => n + 1);
|
||||||
// Recording: tie it to the resolved callsign. Start once a real (≥3-char)
|
// Recording: tie it to the resolved callsign. Start once a real (≥3-char)
|
||||||
// call resolves — covers the fast CW workflow (type → Enter, no blur). If
|
// call resolves — covers the fast CW workflow (type → Enter, no blur). If
|
||||||
// we're already recording a DIFFERENT call (the operator edited the
|
// we're already recording a DIFFERENT call (the operator edited the
|
||||||
@@ -2823,6 +2827,7 @@ export default function App() {
|
|||||||
toLabel={callsign}
|
toLabel={callsign}
|
||||||
beamAzimuths={showBeamOnMap ? beamHeadings : []}
|
beamAzimuths={showBeamOnMap ? beamHeadings : []}
|
||||||
boomAzimuth={showBeamOnMap && ubPattern && ubPattern !== 'normal' ? boomHeading : null}
|
boomAzimuth={showBeamOnMap && ubPattern && ubPattern !== 'normal' ? boomHeading : null}
|
||||||
|
zoomSignal={mapZoomSignal}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'map2':
|
case 'map2':
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic,
|
FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic,
|
||||||
FlexMox, FlexAmpOperate,
|
FlexMox, FlexAmpOperate,
|
||||||
GetPGXLStatus, PGXLSetFanMode,
|
GetPGXLStatus, PGXLSetFanMode,
|
||||||
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna,
|
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna, FlexSetSplit,
|
||||||
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, FlexSetFilter,
|
FlexSetCWSidetone, FlexSetSidetoneLevel, FlexSetCWFilter, FlexSetFilter,
|
||||||
@@ -21,6 +21,7 @@ type FlexState = {
|
|||||||
atu_status?: string; atu_memories: boolean;
|
atu_status?: string; atu_memories: boolean;
|
||||||
rx_avail: boolean; agc_mode?: string; agc_threshold: number; audio_level: number; mute: boolean;
|
rx_avail: boolean; agc_mode?: string; agc_threshold: number; audio_level: number; mute: boolean;
|
||||||
rx_ant?: string; tx_ant?: string; ant_list?: string[]; tx_ant_list?: string[];
|
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;
|
nb: boolean; nb_level: number; nr: boolean; nr_level: number; anf: boolean; anf_level: number;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
cw_speed: number; cw_pitch: number; cw_break_in_delay: number; cw_sidetone: boolean; cw_mon_level: number;
|
cw_speed: number; cw_pitch: number; cw_break_in_delay: number; cw_sidetone: boolean; cw_mon_level: number;
|
||||||
@@ -35,7 +36,7 @@ const ZERO: FlexState = {
|
|||||||
available: false, rf_power: 0, tune_power: 0, tune: false, transmitting: false,
|
available: false, rf_power: 0, tune_power: 0, tune: false, transmitting: false,
|
||||||
vox_enable: false, vox_level: 0, vox_delay: 0, proc_enable: false, proc_level: 0,
|
vox_enable: false, vox_level: 0, vox_delay: 0, proc_enable: false, proc_level: 0,
|
||||||
mon: false, mon_level: 0, mic_level: 0, atu_memories: false,
|
mon: false, mon_level: 0, mic_level: 0, atu_memories: false,
|
||||||
rx_avail: false, agc_threshold: 0, audio_level: 0, mute: 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,
|
nb: false, nb_level: 0, nr: false, nr_level: 0, anf: false, anf_level: 0,
|
||||||
cw_speed: 25, cw_pitch: 600, cw_break_in_delay: 30, cw_sidetone: true, cw_mon_level: 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,
|
apf: false, apf_level: 0, filter_lo: 0, filter_hi: 0,
|
||||||
@@ -306,6 +307,21 @@ export function FlexPanel() {
|
|||||||
<Power className="size-4 inline mr-1 -mt-0.5" /> MOX
|
<Power className="size-4 inline mr-1 -mt-0.5" /> MOX
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button type="button" disabled={off}
|
||||||
|
title="Split: RX/TX on separate slices. ON creates a TX slice +1 kHz (CW) / +5 kHz (SSB) up, like SmartSDR."
|
||||||
|
onClick={() => change('split', !st.split, () => FlexSetSplit(!st.split))}
|
||||||
|
className={cn('px-3 py-1.5 rounded-lg text-sm font-extrabold tracking-wide border-2 transition-all disabled:opacity-30',
|
||||||
|
st.split ? 'bg-sky-600 text-white border-sky-600 shadow-[0_0_12px] shadow-sky-600/50' : 'bg-card text-sky-700 border-sky-400 hover:bg-sky-50')}>
|
||||||
|
SPLIT
|
||||||
|
</button>
|
||||||
|
{st.split && !!st.tx_freq_hz && (
|
||||||
|
<span className="text-[11px] font-mono text-muted-foreground whitespace-nowrap">
|
||||||
|
TX {(st.tx_freq_hz / 1e6).toFixed(3)}
|
||||||
|
{!!st.rx_freq_hz && ` (${(st.tx_freq_hz - st.rx_freq_hz) >= 0 ? '+' : ''}${((st.tx_freq_hz - st.rx_freq_hz) / 1000).toFixed(1)} kHz)`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</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">
|
||||||
|
|||||||
@@ -81,10 +81,11 @@ interface WorldProps {
|
|||||||
beamAzimuths?: number[]; // radiating heading(s) (deg) → draw a beam lobe each
|
beamAzimuths?: number[]; // radiating heading(s) (deg) → draw a beam lobe each
|
||||||
beamWidth?: number; // beamwidth (deg), default 30
|
beamWidth?: number; // beamwidth (deg), default 30
|
||||||
boomAzimuth?: number | null; // mechanical boom (rotor) heading → grey reference line
|
boomAzimuth?: number | null; // mechanical boom (rotor) heading → grey reference line
|
||||||
|
zoomSignal?: number; // bump to force an auto-zoom now (e.g. QRZ lookup finished)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorldMap — great-circle path + beam lobe(s), the "map1" pane.
|
// WorldMap — great-circle path + beam lobe(s), the "map1" pane.
|
||||||
export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, beamWidth, boomAzimuth }: WorldProps) {
|
export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, beamWidth, boomAzimuth, zoomSignal }: WorldProps) {
|
||||||
const worldRef = useRef<HTMLDivElement>(null);
|
const worldRef = useRef<HTMLDivElement>(null);
|
||||||
const worldMap = useRef<L.Map | null>(null);
|
const worldMap = useRef<L.Map | null>(null);
|
||||||
const worldOverlay = useRef<L.LayerGroup | null>(null);
|
const worldOverlay = useRef<L.LayerGroup | null>(null);
|
||||||
@@ -225,7 +226,7 @@ export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, b
|
|||||||
}
|
}
|
||||||
setTimeout(() => { wm.invalidateSize(); }, 0);
|
setTimeout(() => { wm.invalidateSize(); }, 0);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [fromGrid, toGrid, fromLabel, toLabel, (beamAzimuths ?? []).map((a) => Math.round(a)).join(','), beamWidth, boomAzimuth, autoZoom]);
|
}, [fromGrid, toGrid, fromLabel, toLabel, (beamAzimuths ?? []).map((a) => Math.round(a)).join(','), beamWidth, boomAzimuth, autoZoom, zoomSignal]);
|
||||||
|
|
||||||
const path = pathBetween(fromGrid, toGrid);
|
const path = pathBetween(fromGrid, toGrid);
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
@@ -196,6 +196,8 @@ export function FlexSetRXAntenna(arg1:string):Promise<void>;
|
|||||||
|
|
||||||
export function FlexSetSidetoneLevel(arg1:number):Promise<void>;
|
export function FlexSetSidetoneLevel(arg1:number):Promise<void>;
|
||||||
|
|
||||||
|
export function FlexSetSplit(arg1:boolean):Promise<void>;
|
||||||
|
|
||||||
export function FlexSetTXAntenna(arg1:string):Promise<void>;
|
export function FlexSetTXAntenna(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function FlexSetTunePower(arg1:number):Promise<void>;
|
export function FlexSetTunePower(arg1:number):Promise<void>;
|
||||||
|
|||||||
@@ -358,6 +358,10 @@ export function FlexSetSidetoneLevel(arg1) {
|
|||||||
return window['go']['main']['App']['FlexSetSidetoneLevel'](arg1);
|
return window['go']['main']['App']['FlexSetSidetoneLevel'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function FlexSetSplit(arg1) {
|
||||||
|
return window['go']['main']['App']['FlexSetSplit'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function FlexSetTXAntenna(arg1) {
|
export function FlexSetTXAntenna(arg1) {
|
||||||
return window['go']['main']['App']['FlexSetTXAntenna'](arg1);
|
return window['go']['main']['App']['FlexSetTXAntenna'](arg1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -512,6 +512,9 @@ export namespace cat {
|
|||||||
atu_status?: string;
|
atu_status?: string;
|
||||||
atu_memories: boolean;
|
atu_memories: boolean;
|
||||||
rx_avail: boolean;
|
rx_avail: boolean;
|
||||||
|
split: boolean;
|
||||||
|
rx_freq_hz?: number;
|
||||||
|
tx_freq_hz?: number;
|
||||||
agc_mode?: string;
|
agc_mode?: string;
|
||||||
agc_threshold: number;
|
agc_threshold: number;
|
||||||
audio_level: number;
|
audio_level: number;
|
||||||
@@ -565,6 +568,9 @@ export namespace cat {
|
|||||||
this.atu_status = source["atu_status"];
|
this.atu_status = source["atu_status"];
|
||||||
this.atu_memories = source["atu_memories"];
|
this.atu_memories = source["atu_memories"];
|
||||||
this.rx_avail = source["rx_avail"];
|
this.rx_avail = source["rx_avail"];
|
||||||
|
this.split = source["split"];
|
||||||
|
this.rx_freq_hz = source["rx_freq_hz"];
|
||||||
|
this.tx_freq_hz = source["tx_freq_hz"];
|
||||||
this.agc_mode = source["agc_mode"];
|
this.agc_mode = source["agc_mode"];
|
||||||
this.agc_threshold = source["agc_threshold"];
|
this.agc_threshold = source["agc_threshold"];
|
||||||
this.audio_level = source["audio_level"];
|
this.audio_level = source["audio_level"];
|
||||||
|
|||||||
+5
-1
@@ -247,7 +247,10 @@ type FlexTXState struct {
|
|||||||
ATUStatus string `json:"atu_status,omitempty"`
|
ATUStatus string `json:"atu_status,omitempty"`
|
||||||
ATUMemories bool `json:"atu_memories"`
|
ATUMemories bool `json:"atu_memories"`
|
||||||
// Active RX slice DSP controls.
|
// Active RX slice DSP controls.
|
||||||
RXAvail bool `json:"rx_avail"` // an RX slice exists
|
RXAvail bool `json:"rx_avail"` // an RX slice exists
|
||||||
|
Split bool `json:"split"` // RX/TX on separate slices
|
||||||
|
RXFreqHz int64 `json:"rx_freq_hz,omitempty"` // RX slice freq when split
|
||||||
|
TXFreqHz int64 `json:"tx_freq_hz,omitempty"` // TX slice freq when split
|
||||||
AGCMode string `json:"agc_mode,omitempty"`
|
AGCMode string `json:"agc_mode,omitempty"`
|
||||||
AGCThreshold int `json:"agc_threshold"`
|
AGCThreshold int `json:"agc_threshold"`
|
||||||
AudioLevel int `json:"audio_level"`
|
AudioLevel int `json:"audio_level"`
|
||||||
@@ -320,6 +323,7 @@ type FlexController interface {
|
|||||||
SetMute(bool) error
|
SetMute(bool) error
|
||||||
SetRXAntenna(string) error
|
SetRXAntenna(string) error
|
||||||
SetTXAntenna(string) error
|
SetTXAntenna(string) error
|
||||||
|
SetSplit(bool) error
|
||||||
SetNB(bool) error
|
SetNB(bool) error
|
||||||
SetNBLevel(int) error
|
SetNBLevel(int) error
|
||||||
SetNR(bool) error
|
SetNR(bool) error
|
||||||
|
|||||||
+85
-9
@@ -54,6 +54,7 @@ type Flex struct {
|
|||||||
spotsEnabled bool // push cluster spots + manage the panadapter overlay
|
spotsEnabled bool // push cluster spots + manage the panadapter overlay
|
||||||
spotIdx map[int]bool // panadapter spot indices currently known to the radio
|
spotIdx map[int]bool // panadapter spot indices currently known to the radio
|
||||||
pendingSpot map[int]string // seq → callsign, awaiting the spot index in the R response
|
pendingSpot map[int]string // seq → callsign, awaiting the spot index in the R response
|
||||||
|
pendingSplit map[int]bool // seq → awaiting the new TX slice's index (split create)
|
||||||
spotCall map[int]string // spot index → callsign (to fill the call on a panadapter click)
|
spotCall map[int]string // spot index → callsign (to fill the call on a panadapter click)
|
||||||
sentCmds map[int]string // seq → command text, so an R<seq> error names the command
|
sentCmds map[int]string // seq → command text, so an R<seq> error names the command
|
||||||
|
|
||||||
@@ -148,7 +149,7 @@ func NewFlex(host string, port int, spotsEnabled bool) *Flex {
|
|||||||
return &Flex{
|
return &Flex{
|
||||||
host: strings.TrimSpace(host), port: port,
|
host: strings.TrimSpace(host), port: port,
|
||||||
slices: map[int]*flexSlice{}, spotsEnabled: spotsEnabled,
|
slices: map[int]*flexSlice{}, spotsEnabled: spotsEnabled,
|
||||||
spotIdx: map[int]bool{}, pendingSpot: map[int]string{}, spotCall: map[int]string{},
|
spotIdx: map[int]bool{}, pendingSpot: map[int]string{}, spotCall: map[int]string{}, pendingSplit: map[int]bool{},
|
||||||
meterMeta: map[int]meterInfo{}, meterVal: map[int]float64{}, meterSub: map[int]bool{},
|
meterMeta: map[int]meterInfo{}, meterVal: map[int]float64{}, meterSub: map[int]bool{},
|
||||||
sentCmds: map[int]string{}, txSetAt: map[string]time.Time{},
|
sentCmds: map[int]string{}, txSetAt: map[string]time.Time{},
|
||||||
}
|
}
|
||||||
@@ -314,7 +315,18 @@ func (f *Flex) reader(conn net.Conn) {
|
|||||||
f.spotIdx[idx] = true
|
f.spotIdx[idx] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A successful "slice create" for split returns the new slice's index;
|
||||||
|
// make that slice the transmitter so RX/TX are on separate slices.
|
||||||
|
splitSeq := f.pendingSplit[seq]
|
||||||
|
if splitSeq {
|
||||||
|
delete(f.pendingSplit, seq)
|
||||||
|
}
|
||||||
f.mu.Unlock()
|
f.mu.Unlock()
|
||||||
|
if splitSeq && ok && len(parts) >= 3 {
|
||||||
|
if idx, e := strconv.Atoi(strings.TrimSpace(parts[2])); e == nil {
|
||||||
|
f.send(fmt.Sprintf("slice s %d tx=1", idx))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Connection ended.
|
// Connection ended.
|
||||||
@@ -1050,6 +1062,11 @@ func (f *Flex) FlexState() FlexTXState {
|
|||||||
CWSidetone: f.tx.cwSidetone,
|
CWSidetone: f.tx.cwSidetone,
|
||||||
CWMonLevel: f.tx.cwMonLevel,
|
CWMonLevel: f.tx.cwMonLevel,
|
||||||
}
|
}
|
||||||
|
if rx, tx := f.pickSlicesLocked(); rx != nil && tx != nil && rx != tx && rx.freqHz != tx.freqHz {
|
||||||
|
st.Split = true
|
||||||
|
st.RXFreqHz = rx.freqHz
|
||||||
|
st.TXFreqHz = tx.freqHz
|
||||||
|
}
|
||||||
if _, rx := f.rxSliceLocked(); rx != nil {
|
if _, rx := f.rxSliceLocked(); rx != nil {
|
||||||
st.RXAvail = true
|
st.RXAvail = true
|
||||||
st.Mode = strings.ToUpper(rx.mode)
|
st.Mode = strings.ToUpper(rx.mode)
|
||||||
@@ -1170,14 +1187,73 @@ func (f *Flex) SetAudioLevel(l int) error { return f.sendSlice("audio_level",
|
|||||||
func (f *Flex) SetMute(on bool) error { return f.sendSlice("audio_mute", boolFlex(on)) }
|
func (f *Flex) SetMute(on bool) error { return f.sendSlice("audio_mute", boolFlex(on)) }
|
||||||
func (f *Flex) SetRXAntenna(a string) error { return f.sendSlice("rxant", a) }
|
func (f *Flex) SetRXAntenna(a string) error { return f.sendSlice("rxant", a) }
|
||||||
func (f *Flex) SetTXAntenna(a string) error { return f.sendSlice("txant", a) }
|
func (f *Flex) SetTXAntenna(a string) error { return f.sendSlice("txant", a) }
|
||||||
func (f *Flex) SetNB(on bool) error { return f.sendSlice("nb", boolFlex(on)) }
|
|
||||||
func (f *Flex) SetNBLevel(l int) error { return f.sendSlice("nb_level", clampLevel(l)) }
|
// SetSplit toggles 2-slice split like SmartSDR's SPLIT button. ON creates a TX
|
||||||
func (f *Flex) SetNR(on bool) error { return f.sendSlice("nr", boolFlex(on)) }
|
// slice at RX freq + offset (+1 kHz on CW, +5 kHz otherwise) and makes it the
|
||||||
func (f *Flex) SetNRLevel(l int) error { return f.sendSlice("nr_level", clampLevel(l)) }
|
// transmitter; OFF removes the extra TX slice and returns to simplex (RX slice
|
||||||
func (f *Flex) SetANF(on bool) error { return f.sendSlice("anf", boolFlex(on)) }
|
// transmits again).
|
||||||
func (f *Flex) SetANFLevel(l int) error { return f.sendSlice("anf_level", clampLevel(l)) }
|
func (f *Flex) SetSplit(on bool) error {
|
||||||
func (f *Flex) SetAPF(on bool) error { return f.sendSlice("apf", boolFlex(on)) }
|
f.mu.Lock()
|
||||||
func (f *Flex) SetAPFLevel(l int) error { return f.sendSlice("apf_level", clampLevel(l)) }
|
if f.conn == nil {
|
||||||
|
f.mu.Unlock()
|
||||||
|
return fmt.Errorf("flex: not connected")
|
||||||
|
}
|
||||||
|
rx, tx := f.pickSlicesLocked()
|
||||||
|
rxIdx, txIdx := -1, -1
|
||||||
|
for i, s := range f.slices {
|
||||||
|
if s == rx {
|
||||||
|
rxIdx = i
|
||||||
|
}
|
||||||
|
if s == tx {
|
||||||
|
txIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alreadySplit := rx != nil && tx != nil && rx != tx
|
||||||
|
var rxFreq int64
|
||||||
|
var rxMode, rxAnt string
|
||||||
|
if rx != nil {
|
||||||
|
rxFreq, rxMode, rxAnt = rx.freqHz, rx.mode, rx.rxAnt
|
||||||
|
}
|
||||||
|
f.mu.Unlock()
|
||||||
|
|
||||||
|
if on {
|
||||||
|
if alreadySplit || rx == nil {
|
||||||
|
return nil // already split (or no slice)
|
||||||
|
}
|
||||||
|
offset := int64(5000)
|
||||||
|
if strings.HasPrefix(strings.ToUpper(rxMode), "CW") {
|
||||||
|
offset = 1000
|
||||||
|
}
|
||||||
|
cmd := fmt.Sprintf("slice create freq=%.6f mode=%s", float64(rxFreq+offset)/1e6, rxMode)
|
||||||
|
if rxAnt != "" {
|
||||||
|
cmd += " ant=" + rxAnt
|
||||||
|
}
|
||||||
|
if seq := f.send(cmd); seq > 0 {
|
||||||
|
f.mu.Lock()
|
||||||
|
f.pendingSplit[seq] = true // mark the new slice TX once its index returns
|
||||||
|
f.mu.Unlock()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !alreadySplit {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if txIdx >= 0 {
|
||||||
|
f.send(fmt.Sprintf("slice remove %d", txIdx))
|
||||||
|
}
|
||||||
|
if rxIdx >= 0 {
|
||||||
|
f.send(fmt.Sprintf("slice s %d tx=1", rxIdx))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *Flex) SetNB(on bool) error { return f.sendSlice("nb", boolFlex(on)) }
|
||||||
|
func (f *Flex) SetNBLevel(l int) error { return f.sendSlice("nb_level", clampLevel(l)) }
|
||||||
|
func (f *Flex) SetNR(on bool) error { return f.sendSlice("nr", boolFlex(on)) }
|
||||||
|
func (f *Flex) SetNRLevel(l int) error { return f.sendSlice("nr_level", clampLevel(l)) }
|
||||||
|
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)) }
|
||||||
|
|
||||||
// ── CW keyer controls (top-level "cw" commands) ──
|
// ── CW keyer controls (top-level "cw" commands) ──
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user