feat: icom Scope in Icom Tab

This commit is contained in:
2026-07-04 22:06:26 +02:00
parent abbdde9367
commit dd4b0004a5
8 changed files with 918 additions and 56 deletions
+58 -2
View File
@@ -376,8 +376,17 @@ type IcomTXState struct {
Available bool `json:"available"`
Model string `json:"model,omitempty"`
Mode string `json:"mode,omitempty"`
AFGain int `json:"af_gain"`
RFGain int `json:"rf_gain"`
// Transmit + live status (polled).
Transmitting bool `json:"transmitting"`
Split bool `json:"split"`
SMeter int `json:"s_meter"` // 0-100 (raw 0-255; S9≈120)
PowerMeter int `json:"power_meter"` // 0-100 (TX Po)
SWRMeter int `json:"swr_meter"` // 0-100 (TX SWR)
// Set controls.
RFPower int `json:"rf_power"` // 0-100 (TX output)
MicGain int `json:"mic_gain"` // 0-100
AFGain int `json:"af_gain"`
RFGain int `json:"rf_gain"`
NB bool `json:"nb"`
NBLevel int `json:"nb_level"`
NR bool `json:"nr"`
@@ -406,6 +415,25 @@ type IcomController interface {
SetPreamp(int) error
SetAtt(int) error
SetIcomFilter(int) error
SetRFPower(int) error
SetMicGain(int) error
SetIcomSplit(bool) error
TuneATU() error
SetScope(bool) error // enable/disable the spectrum-scope waveform stream
SetScopeMode(bool) error // true = fixed span, false = center-on-VFO
ScopeData() ScopeSweep // latest assembled sweep (empty until enabled)
}
// ScopeSweep is one complete spectrum-scope sweep reassembled from the Icom's
// divided 0x27 waveform frames. Amp holds one amplitude byte per pixel (raw rig
// scale, typically 0-160). Seq increments on every completed sweep so the UI can
// tell fresh data from a repeated poll.
type ScopeSweep struct {
Amp []int `json:"amp"` // []int (not []byte) so it marshals as a JSON number array
Seq int `json:"seq"`
LowHz int64 `json:"low_hz"` // left edge frequency (0 when unknown)
HighHz int64 `json:"high_hz"` // right edge frequency (0 when unknown)
Fixed bool `json:"fixed"` // true = fixed-span mode, false = center-on-VFO
}
// IcomState returns the current Icom DSP state, or (zero, false) when the active
@@ -420,6 +448,19 @@ func (m *Manager) IcomState() (IcomTXState, bool) {
return IcomTXState{}, false
}
// IcomScope returns the latest spectrum-scope sweep, or (zero, false) when the
// active backend isn't an Icom. The sweep is mutex-guarded in the backend, so
// this reads it directly (no CAT-goroutine round trip) — cheap enough to poll.
func (m *Manager) IcomScope() (ScopeSweep, bool) {
m.mu.RLock()
b := m.backend
m.mu.RUnlock()
if ic, ok := b.(IcomController); ok {
return ic.ScopeData(), true
}
return ScopeSweep{}, false
}
// IcomDo dispatches an Icom control onto the CAT goroutine. Errors if the
// active backend isn't an Icom.
func (m *Manager) IcomDo(fn func(IcomController) error) error {
@@ -490,6 +531,21 @@ func (m *Manager) run(b Backend, stop, done chan struct{}, cmds chan func(), pol
fn()
m.applyCommandDelay()
case <-ticker.C:
// Drain any queued commands before polling. A serial backend reads
// many registers per ReadState, so without this the shared select's
// fairness lets polls repeatedly win and a user's Set* can lag by
// seconds. Servicing commands first bounds that latency to a single
// ReadState.
for {
select {
case fn := <-cmds:
fn()
m.applyCommandDelay()
continue
default:
}
break
}
if !connected {
tryConnect()
continue