From 9e2ffdb758e917d609c0b16bbf6db5a7a9261b1e Mon Sep 17 00:00:00 2001 From: rouggy Date: Tue, 14 Jul 2026 23:54:06 +0200 Subject: [PATCH] feat: Adding XIT RIT in the Flex panel --- app.go | 30 +++++++++++ frontend/src/components/FlexPanel.tsx | 77 +++++++++++++++++++++++++++ frontend/src/lib/i18n.tsx | 2 + frontend/wailsjs/go/main/App.d.ts | 8 +++ frontend/wailsjs/go/main/App.js | 16 ++++++ frontend/wailsjs/go/models.ts | 8 +++ internal/cat/cat.go | 11 ++++ internal/cat/flex.go | 48 ++++++++++++++++- 8 files changed, 198 insertions(+), 2 deletions(-) diff --git a/app.go b/app.go index 7584e63..67c776d 100644 --- a/app.go +++ b/app.go @@ -9684,6 +9684,36 @@ func (a *App) FlexApplyBandAntenna(band string) error { }) } +// RIT/XIT on the active slice. The offset is kept by the radio when the switch is +// off, so turning it back on restores it — the UI must not zero it on toggle. +func (a *App) FlexSetRIT(on bool) error { + if a.cat == nil { + return fmt.Errorf("cat not initialized") + } + return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetRIT(on) }) +} + +func (a *App) FlexSetRITFreq(hz int) error { + if a.cat == nil { + return fmt.Errorf("cat not initialized") + } + return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetRITFreq(hz) }) +} + +func (a *App) FlexSetXIT(on bool) error { + if a.cat == nil { + return fmt.Errorf("cat not initialized") + } + return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetXIT(on) }) +} + +func (a *App) FlexSetXITFreq(hz int) error { + if a.cat == nil { + return fmt.Errorf("cat not initialized") + } + return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetXITFreq(hz) }) +} + func (a *App) FlexSetNB(on bool) error { if a.cat == nil { return fmt.Errorf("cat not initialized") diff --git a/frontend/src/components/FlexPanel.tsx b/frontend/src/components/FlexPanel.tsx index 8f70131..9ef0618 100644 --- a/frontend/src/components/FlexPanel.tsx +++ b/frontend/src/components/FlexPanel.tsx @@ -6,6 +6,7 @@ import { FlexMox, FlexAmpOperate, GetPGXLStatus, PGXLSetFanMode, FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna, FlexSetSplit, FlexSetActiveSlice, FlexSetTXSlice, + FlexSetRIT, FlexSetRITFreq, FlexSetXIT, FlexSetXITFreq, FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel, FlexSetWNB, FlexSetWNBLevel, FlexSetTXFilter, FlexSetMicProfile, FlexSetAPF, FlexSetAPFLevel, FlexSetCWSpeed, FlexSetCWPitch, FlexSetCWBreakInDelay, @@ -25,6 +26,7 @@ type FlexState = { 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[]; split: boolean; rx_freq_hz?: number; tx_freq_hz?: number; + rit: boolean; rit_freq: number; xit: boolean; xit_freq: 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[]; @@ -44,6 +46,7 @@ const ZERO: FlexState = { 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, rx_avail: false, agc_threshold: 0, audio_level: 0, mute: false, split: false, + rit: false, rit_freq: 0, xit: false, xit_freq: 0, 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, @@ -143,6 +146,67 @@ function LevelRow({ label, on, onToggle, value, onLevel, disabled, accent, slide ); } +// OffsetRow — RIT / XIT: a switch plus one signed offset field you scrub with the +// wheel (or ± , or the arrow keys once focused; hold Ctrl/Shift for 100 Hz steps). +// Deliberately the same control as the Icom panel's ShiftRow, so the two rigs are +// driven identically. +// +// The offset is NOT cleared when the switch goes off: SmartSDR keeps it, so +// flipping RIT back on returns you exactly where you were, like the radio's own +// knob. Zeroing it is a separate, deliberate act — that is what the 0 button is for. +const OFFSET_MAX = 99999; // SmartSDR's limit +function OffsetRow({ label, on, onToggle, hz, onHz, disabled, title }: { + label: string; on: boolean; onToggle: () => void; hz: number; onHz: (v: number) => void; + disabled?: boolean; title?: string; +}) { + const ref = useRef(null); + const step = useRef((_d: number) => {}); + step.current = (d: number) => onHz(Math.max(-OFFSET_MAX, Math.min(OFFSET_MAX, (hz || 0) + d))); + + // React's onWheel is passive, so preventDefault() there is ignored and the panel + // scrolls under the cursor instead of the value changing. Bind it natively. + useEffect(() => { + const el = ref.current; + if (!el) return; + const onWheel = (e: WheelEvent) => { + e.preventDefault(); + const mag = e.ctrlKey || e.shiftKey ? 100 : 10; + step.current(e.deltaY < 0 ? mag : -mag); + }; + el.addEventListener('wheel', onWheel, { passive: false }); + return () => el.removeEventListener('wheel', onWheel); + }, []); + + const onKey = (e: React.KeyboardEvent) => { + const up = e.key === 'ArrowUp' || e.key === 'ArrowRight'; + const dn = e.key === 'ArrowDown' || e.key === 'ArrowLeft'; + if (!up && !dn) return; + e.preventDefault(); + const mag = e.ctrlKey || e.shiftKey ? 100 : 10; + step.current(up ? mag : -mag); + }; + + return ( +
+ +
+ + + {hz > 0 ? '+' : hz < 0 ? '−' : ''}{Math.abs(hz || 0)} Hz + + +
+ +
+ ); +} + // MeterBar — a segmented "LED" instrument bar (radio look) scaled by lo/hi. // `display` overrides the numeric readout; `segColor` colours segments by their // 0..1 position (zones); the top ~18% light red by default (overload/peak). @@ -454,6 +518,7 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number )} + {!isCW ? (
@@ -560,6 +625,18 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
)} + {/* RIT / XIT — on the RECEIVE side: RIT is what you reach for while + listening (chasing a station that drifts off your transmit frequency), + so it belongs with the other things you touch mid-QSO. Both act on the + ACTIVE slice and follow slice focus like every control here. */} +
+ change('rit', !st.rit, () => FlexSetRIT(!st.rit))} + onHz={(v) => change('rit_freq', v, () => FlexSetRITFreq(v))} /> + change('xit', !st.xit, () => FlexSetXIT(!st.xit))} + onHz={(v) => change('xit_freq', v, () => FlexSetXITFreq(v))} /> +
AGC ; export function FlexSetProcessorLevel(arg1:number):Promise; +export function FlexSetRIT(arg1:boolean):Promise; + +export function FlexSetRITFreq(arg1:number):Promise; + export function FlexSetRXAntenna(arg1:string):Promise; export function FlexSetSidetoneLevel(arg1:number):Promise; @@ -264,6 +268,10 @@ export function FlexSetWNB(arg1:boolean):Promise; export function FlexSetWNBLevel(arg1:number):Promise; +export function FlexSetXIT(arg1:boolean):Promise; + +export function FlexSetXITFreq(arg1:number):Promise; + export function FlexTune(arg1:boolean):Promise; export function GetActiveProfile():Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index d43431a..1cf24e2 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -438,6 +438,14 @@ export function FlexSetProcessorLevel(arg1) { return window['go']['main']['App']['FlexSetProcessorLevel'](arg1); } +export function FlexSetRIT(arg1) { + return window['go']['main']['App']['FlexSetRIT'](arg1); +} + +export function FlexSetRITFreq(arg1) { + return window['go']['main']['App']['FlexSetRITFreq'](arg1); +} + export function FlexSetRXAntenna(arg1) { return window['go']['main']['App']['FlexSetRXAntenna'](arg1); } @@ -486,6 +494,14 @@ export function FlexSetWNBLevel(arg1) { return window['go']['main']['App']['FlexSetWNBLevel'](arg1); } +export function FlexSetXIT(arg1) { + return window['go']['main']['App']['FlexSetXIT'](arg1); +} + +export function FlexSetXITFreq(arg1) { + return window['go']['main']['App']['FlexSetXITFreq'](arg1); +} + export function FlexTune(arg1) { return window['go']['main']['App']['FlexTune'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 1f9b704..d07290d 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -722,6 +722,10 @@ export namespace cat { anf_level: number; wnb: boolean; wnb_level: number; + rit: boolean; + rit_freq: number; + xit: boolean; + xit_freq: number; mode?: string; cw_speed: number; cw_pitch: number; @@ -785,6 +789,10 @@ export namespace cat { this.anf_level = source["anf_level"]; this.wnb = source["wnb"]; this.wnb_level = source["wnb_level"]; + this.rit = source["rit"]; + this.rit_freq = source["rit_freq"]; + this.xit = source["xit"]; + this.xit_freq = source["xit_freq"]; this.mode = source["mode"]; this.cw_speed = source["cw_speed"]; this.cw_pitch = source["cw_pitch"]; diff --git a/internal/cat/cat.go b/internal/cat/cat.go index 5ed747f..0e3c328 100644 --- a/internal/cat/cat.go +++ b/internal/cat/cat.go @@ -304,6 +304,13 @@ type FlexTXState struct { ANFLevel int `json:"anf_level"` WNB bool `json:"wnb"` WNBLevel int `json:"wnb_level"` + // RIT/XIT — offsets applied to the active slice's RX / TX frequency without + // moving the slice. The offset survives the switch being turned off, so + // re-enabling restores it, exactly like the radio's own knob. + RIT bool `json:"rit"` + RITFreq int `json:"rit_freq"` + XIT bool `json:"xit"` + XITFreq int `json:"xit_freq"` // CW / mode-specific controls. Mode string `json:"mode,omitempty"` // active slice mode (CW/USB/LSB/DIGU…) CWSpeed int `json:"cw_speed"` @@ -378,6 +385,10 @@ type FlexController interface { SetAPFLevel(int) error SetWNB(bool) error SetWNBLevel(int) error + SetRIT(bool) error + SetRITFreq(int) error + SetXIT(bool) error + SetXITFreq(int) error // CW keyer + mode-specific controls. SetCWSpeed(int) error SetCWPitch(int) error diff --git a/internal/cat/flex.go b/internal/cat/flex.go index 6af4e4c..c76d211 100644 --- a/internal/cat/flex.go +++ b/internal/cat/flex.go @@ -90,8 +90,12 @@ type flexSlice struct { apfLevel int wnb bool // wideband noise blanker wnbLevel int - filterLo int // slice filter low cut (Hz) - filterHi int // slice filter high cut (Hz) + rit bool // receive incremental tuning enabled + ritFreq int // RIT offset in Hz (negative = down) + xit bool // transmit incremental tuning enabled + xitFreq int // XIT offset in Hz + 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) txAnt string // selected TX antenna antList []string // antennas valid for this slice (RX side) @@ -804,6 +808,14 @@ func (f *Flex) handleStatus(payload string) { s.wnb = val == "1" case "wnb_level": s.wnbLevel = atoiDefault(val, s.wnbLevel) + case "rit_on": + s.rit = val == "1" + case "rit_freq": + s.ritFreq = atoiDefault(val, s.ritFreq) + case "xit_on": + s.xit = val == "1" + case "xit_freq": + s.xitFreq = atoiDefault(val, s.xitFreq) case "filter_lo": s.filterLo = atoiDefault(val, s.filterLo) case "filter_hi": @@ -1315,6 +1327,10 @@ func (f *Flex) FlexState() FlexTXState { st.APFLevel = rx.apfLevel st.WNB = rx.wnb st.WNBLevel = rx.wnbLevel + st.RIT = rx.rit + st.RITFreq = rx.ritFreq + st.XIT = rx.xit + st.XITFreq = rx.xitFreq st.FilterLo = rx.filterLo st.FilterHi = rx.filterHi st.RXAnt = rx.rxAnt @@ -1383,6 +1399,14 @@ func (f *Flex) sendSlice(param string, val any) error { rx.rxAnt = fmt.Sprint(val) case "txant": rx.txAnt = fmt.Sprint(val) + case "rit_on": + rx.rit = val == "1" + case "rit_freq": + rx.ritFreq = toInt(val) + case "xit_on": + rx.xit = val == "1" + case "xit_freq": + rx.xitFreq = toInt(val) } } f.mu.Unlock() @@ -1490,6 +1514,26 @@ 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)) } +// RIT/XIT — an offset applied to the RX (RIT) or TX (XIT) frequency of the active +// slice, without moving the slice itself. SmartSDR keeps the offset even while the +// switch is off, so turning RIT back on restores the last offset — same as the +// radio's own knob. +func (f *Flex) SetRIT(on bool) error { return f.sendSlice("rit_on", boolFlex(on)) } +func (f *Flex) SetRITFreq(hz int) error { return f.sendSlice("rit_freq", clampOffset(hz)) } +func (f *Flex) SetXIT(on bool) error { return f.sendSlice("xit_on", boolFlex(on)) } +func (f *Flex) SetXITFreq(hz int) error { return f.sendSlice("xit_freq", clampOffset(hz)) } + +// clampOffset keeps a RIT/XIT offset inside what SmartSDR accepts (±99 999 Hz). +func clampOffset(hz int) int { + if hz > 99999 { + return 99999 + } + if hz < -99999 { + return -99999 + } + return hz +} + 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)) }