diff --git a/internal/cat/civ/civ.go b/internal/cat/civ/civ.go index 38a6d2f..15adb41 100644 --- a/internal/cat/civ/civ.go +++ b/internal/cat/civ/civ.go @@ -32,6 +32,8 @@ const ( CmdReadMode = 0x04 CmdSetFreq = 0x05 CmdSetMode = 0x06 + CmdSplit = 0x0F // read/set split & duplex (0x00 off, 0x01 split on) + CmdVfoFreq = 0x25 // read a specific VFO's freq (sub 0x00 selected, 0x01 unselected) CmdPTT = 0x1C // sub 0x00 = PTT CmdExtra = 0x1A // sub 0x06 = data mode on modern Icoms CmdReadID = 0x19 // sub 0x00 = rig's own CI-V address (identifies model) @@ -40,8 +42,10 @@ const ( CmdLevel = 0x14 // analogue levels (sub + 2 BCD bytes, 0000-0255) CmdSwitch = 0x16 // on/off + multi-state DSP settings (sub + 1 byte) - SubDataMode = 0x06 - SubPTT = 0x00 + SubDataMode = 0x06 + SubPTT = 0x00 + SubVfoSelected = 0x00 // CmdVfoFreq: the active/RX VFO + SubVfoUnselected = 0x01 // CmdVfoFreq: the other VFO (TX in split) // CmdLevel sub-commands. SubLevelAF = 0x01 // AF (volume) diff --git a/internal/cat/icomserial.go b/internal/cat/icomserial.go index 9503a63..9a524a9 100644 --- a/internal/cat/icomserial.go +++ b/internal/cat/icomserial.go @@ -28,6 +28,7 @@ type IcomSerial struct { curFreq int64 // last frequency read (for sideband choice) curModeByte byte // last raw Icom mode byte (for filter re-send) + readFails int // consecutive ReadState freq-read failures (transient tolerance) lastSetFreq int64 // last frequency commanded (spot click: freq then mode) lastSetFreqAt time.Time @@ -77,6 +78,12 @@ func (b *IcomSerial) Connect() error { // Short read timeout so recv() polls in a tight loop without blocking the // CAT goroutine when the rig is silent. _ = port.SetReadTimeout(60 * time.Millisecond) + // Deassert DTR/RTS. Icom USB rigs (IC-7610, IC-7300…) let "USB SEND" and + // "USB Keying (CW)" be mapped to the RTS or DTR line: if the port opens with + // those asserted, the rig keys into TRANSMIT. PTT here is CI-V only, so both + // hardware lines must stay low. + _ = port.SetDTR(false) + _ = port.SetRTS(false) b.port = port b.rx = b.rx[:0] b.model = civ.ModelName(b.rigAddr) @@ -110,8 +117,25 @@ func (b *IcomSerial) ReadState() (RigState, error) { hz, err := b.readFreq() if err != nil { + // The rig briefly stops answering CI-V while it switches band/VFO. Treat a + // few consecutive misses as transient — keep the connection and report the + // last known state — so a band change doesn't trigger a full disconnect + + // 5 s reconnect (which showed the new frequency ~10 s late). Only after + // several failures do we declare the rig lost so the Manager reconnects. + b.readFails++ + if b.readFails <= 6 && b.curFreq > 0 { + s.FreqHz = b.curFreq + if b.curModeByte != 0 { + s.Mode = civ.ModeToADIF(b.curModeByte, false) + if s.Mode == "DATA" { + s.Mode = b.digital + } + } + return s, nil + } return RigState{}, err } + b.readFails = 0 s.FreqHz = hz b.curFreq = hz @@ -126,6 +150,16 @@ func (b *IcomSerial) ReadState() (RigState, error) { b.dsp.Mode = s.Mode b.dspMu.Unlock() } + + // Split: the selected VFO (read above) is RX; the unselected VFO is TX. ADIF + // convention → FreqHz = TX, RxFreqHz = RX. + if on, ok := b.readSplit(); ok && on { + if txHz, ok2 := b.readTXFreq(); ok2 && txHz > 0 && txHz != s.FreqHz { + s.Split = true + s.RxFreqHz = s.FreqHz // selected VFO = RX + s.FreqHz = txHz // unselected VFO = TX + } + } return s, nil } @@ -232,6 +266,36 @@ func (b *IcomSerial) readFreq() (int64, error) { return civ.BCDToFreq(f.Data), nil } +// readSplit reads the rig's split state (CI-V 0x0F). 0x01 = split on; 0x10/0x11 +// are repeater duplex (not split) and 0x00 is off. +func (b *IcomSerial) readSplit() (on bool, ok bool) { + if err := b.write(civ.CmdSplit); err != nil { + return false, false + } + f, err := b.recv(icomReadTimeout, func(d civ.Decoded) bool { + return d.Cmd == civ.CmdSplit && len(d.Data) >= 1 + }) + if err != nil { + return false, false + } + return f.Data[0] == 0x01, true +} + +// readTXFreq reads the UNSELECTED VFO's frequency (CI-V 0x25/01) — the TX VFO +// when the rig is in split. Supported on IC-7610/7300/7851/705/9700 and similar. +func (b *IcomSerial) readTXFreq() (int64, bool) { + if err := b.write(civ.CmdVfoFreq, civ.SubVfoUnselected); err != nil { + return 0, false + } + f, err := b.recv(icomReadTimeout, func(d civ.Decoded) bool { + return d.Cmd == civ.CmdVfoFreq && len(d.Data) >= 6 && d.Data[0] == civ.SubVfoUnselected + }) + if err != nil { + return 0, false + } + return civ.BCDToFreq(f.Data[1:]), true +} + func (b *IcomSerial) readMode() (byte, bool) { if err := b.write(civ.CmdReadMode); err != nil { return 0, false