chore: release v0.21.3

This commit is contained in:
2026-07-26 16:57:19 +02:00
parent 4fd70f6a9d
commit 91b5af1c7b
46 changed files with 2491 additions and 355 deletions
+62 -18
View File
@@ -79,23 +79,23 @@ type IcomSerial struct {
// leading main/sub selector byte (IC-7610/9700). scopeAmp is the latest
// reassembled sweep; scopeMu guards it (written by the scope goroutine, read
// via ScopeData from the binding goroutine).
dualScope bool
scopeMu sync.Mutex
scopeAmp []byte
scopeLow int64 // spectrum left-edge frequency (from the sweep's header frame)
scopeHigh int64 // spectrum right-edge frequency
scopeSeq int
scopeOn bool
dualScope bool
scopeMu sync.Mutex
scopeAmp []byte
scopeLow int64 // spectrum left-edge frequency (from the sweep's header frame)
scopeHigh int64 // spectrum right-edge frequency
scopeSeq int
scopeOn bool
scopeFixed bool // true = fixed-span mode (tracked optimistically)
scopeSeen bool // logged the first sweep's structure once (on-rig verification)
scopeSeen bool // logged the first sweep's structure once (on-rig verification)
curFreq int64 // last frequency read (for sideband choice)
curModeByte byte // last raw Icom mode byte (for filter re-send)
pollN int // ReadState cycle counter (staggers slow reads)
splitOn bool // last read split state (refreshed every few cycles)
splitTXFreq int64 // last read unselected/TX VFO freq while in split
readFails int // consecutive ReadState freq-read failures (transient tolerance)
dspLoaded bool // readDSP has run since the rig became responsive (loads all
curFreq int64 // last frequency read (for sideband choice)
curModeByte byte // last raw Icom mode byte (for filter re-send)
pollN int // ReadState cycle counter (staggers slow reads)
splitOn bool // last read split state (refreshed every few cycles)
splitTXFreq int64 // last read unselected/TX VFO freq while in split
readFails int // consecutive ReadState freq-read failures (transient tolerance)
dspLoaded bool // readDSP has run since the rig became responsive (loads all
// the panel's set-once controls once the rig actually answers)
lastSetFreq int64 // last frequency commanded (spot click: freq then mode)
lastSetFreqAt time.Time
@@ -341,7 +341,7 @@ func (b *IcomSerial) ReadState() (RigState, error) {
}
if b.splitOn && b.splitTXFreq > 0 && b.splitTXFreq != s.FreqHz {
s.Split = true
s.RxFreqHz = s.FreqHz // selected VFO = RX
s.RxFreqHz = s.FreqHz // selected VFO = RX
s.FreqHz = b.splitTXFreq // unselected VFO = TX
}
@@ -374,10 +374,54 @@ func (b *IcomSerial) ReadState() (RigState, error) {
if !b.dspLoaded {
b.readDSP()
b.dspLoaded = true
} else {
b.refreshFrontPanel()
}
return s, nil
}
// refreshFrontPanel re-reads the few controls an operator actually reaches for on
// the rig itself, so the console follows the radio instead of only driving it.
//
// readDSP loads everything but runs ONCE per connection (dspLoaded), which left
// the panel showing whatever was set at connect: switch AGC from FAST to MID on
// the front panel and OpsLog still said FAST, indefinitely. Commands worked, so
// the link was plainly fine — only this direction was missing.
//
// ONE read per poll cycle, in rotation. The full snapshot is ~30 CI-V round trips
// and refreshing it wholesale would hog the CAT thread for seconds at a time —
// including the operator's own Set* commands, which is far worse than a stale
// label. The rotation completes in 8 cycles; anything not covered here is still a
// connect-time or ↻ Refresh read.
func (b *IcomSerial) refreshFrontPanel() {
switch b.pollN % 8 {
case 1:
if v, ok := b.readSwitch(civ.SubSwAGC); ok {
b.dspMu.Lock()
b.dsp.AGC = agcName(v)
b.dspMu.Unlock()
}
case 3:
if v, ok := b.readAtt(); ok {
b.dspMu.Lock()
b.dsp.Att = v
b.dspMu.Unlock()
}
case 5:
if v, ok := b.readSwitch(civ.SubSwPreamp); ok {
b.dspMu.Lock()
b.dsp.Preamp = int(v)
b.dspMu.Unlock()
}
case 7:
if _, f, ok := b.readModeFilter(); ok {
b.dspMu.Lock()
b.dsp.Filter = int(f)
b.dspMu.Unlock()
}
}
}
func (b *IcomSerial) SetFrequency(hz int64) error {
if hz <= 0 {
return fmt.Errorf("invalid frequency")
@@ -583,8 +627,8 @@ func (b *IcomSerial) drainResp() {
func (b *IcomSerial) scopeLoop(spec chan civ.Decoded, done chan struct{}) {
regions := make(map[byte][]byte)
var total byte
rawN := 0 // diagnostic: dump the first few raw 0x27 frames
loggedCfg := map[byte]bool{} // one-shot dump of each config read response
rawN := 0 // diagnostic: dump the first few raw 0x27 frames
loggedCfg := map[byte]bool{} // one-shot dump of each config read response
for {
select {
case <-done: