feat: Implemented scope on Ethernet for Icom
This commit is contained in:
+313
-105
@@ -26,6 +26,7 @@ import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -53,11 +54,31 @@ func NewIcomNet(host, user, pass string, civAddr int, digitalDefault string) *Ic
|
||||
if strings.TrimSpace(host) == "" {
|
||||
return nil, fmt.Errorf("no rig host configured")
|
||||
}
|
||||
return dialIcomNet(host, user, pass, "OpsLog", b.rigAddr)
|
||||
b.dialMu.Lock()
|
||||
cancel := b.dialCancel
|
||||
b.dialMu.Unlock()
|
||||
return dialIcomNet(host, user, pass, "OpsLog", b.rigAddr, cancel)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// errDialCanceled is returned by dialIcomNet when Interrupt() aborts the dial
|
||||
// (Stop/Start). The Manager treats it like any connect error and simply stops.
|
||||
var errDialCanceled = fmt.Errorf("dial canceled")
|
||||
|
||||
// icnCanceled reports whether the dial has been asked to abort.
|
||||
func icnCanceled(cancel <-chan struct{}) bool {
|
||||
if cancel == nil {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-cancel:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// icomNet is the connected network transport. It satisfies civTransport.
|
||||
type icomNet struct {
|
||||
ctrl *net.UDPConn // control stream (50001)
|
||||
@@ -71,7 +92,9 @@ type icomNet struct {
|
||||
vTracked uint16
|
||||
vCivSeq uint16
|
||||
|
||||
rx chan []byte // CI-V byte chunks from civPump → Read
|
||||
rx chan []byte // CI-V byte chunks from civPump → Read (control replies)
|
||||
scopeRx chan []byte // scope (0x27) frames, kept off rx so the panadapter
|
||||
// stream can't crowd control replies out (→ ScopeChan)
|
||||
leftover []byte // partial chunk not yet returned by Read (Read-only)
|
||||
readTO time.Duration // Read timeout (SetReadTimeout)
|
||||
|
||||
@@ -81,14 +104,77 @@ type icomNet struct {
|
||||
sentMu sync.Mutex
|
||||
sentBuf map[uint16][]byte
|
||||
|
||||
// Control-stream auth state, carried out of dial so ctrlPump can RENEW the
|
||||
// login token every ~45 s. The rig invalidates the session ~2 min after login
|
||||
// without renewal (this was the "loses control after 2 min" drop — RS-BA1/the
|
||||
// Remote Utility renew too). Owned solely by ctrlPump after dial → no lock.
|
||||
cTracked uint16 // control-stream tracked seq (continues after dial)
|
||||
cAuthSeq uint16 // token-packet innerseq
|
||||
cToken uint32 // login token (opaque, echoed back verbatim)
|
||||
cTokReq uint16 // token-request id (echoed)
|
||||
cSentBuf map[uint16][]byte // control-stream retransmit buffer (token renewals)
|
||||
|
||||
// Receive-side retransmit (CI-V stream): track the rig's data-packet send seq
|
||||
// and ask it to resend any gap. Under the scope stream, UDP drops are common;
|
||||
// without recovering them the gaps accumulate and the rig drops the WHOLE
|
||||
// session after ~20 s (RS-BA1/wfview request retransmits, which is why they
|
||||
// stay up with the panadapter on). Owned solely by civPump → no lock.
|
||||
rxHaveSeq bool
|
||||
rxLastSeq uint16
|
||||
rxMissing map[uint16]int
|
||||
|
||||
// lastRx is the UnixNano of the last packet received from the rig (any type),
|
||||
// updated by both pumps. The rig's network server answers pings/idles even
|
||||
// when the RADIO is in standby, so this tracks the CONTROL-LINK liveness
|
||||
// independently of whether CI-V is replying — letting ReadState tell "rig off
|
||||
// but link fine" (stay connected) from "link dead" (reconnect). See Alive().
|
||||
lastRx atomic.Int64
|
||||
|
||||
done chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// ScopeChan exposes the raw scope (0x27) CI-V frames for the scope feeder.
|
||||
// Satisfies scopeTransport in icomserial.go.
|
||||
func (n *icomNet) ScopeChan() <-chan []byte { return n.scopeRx }
|
||||
|
||||
// icnEnqueueDrop pushes onto a bounded channel, discarding the oldest entry when
|
||||
// full — a lagging consumer never blocks the producer (used for the scope stream,
|
||||
// where only the latest sweep matters).
|
||||
func icnEnqueueDrop(ch chan []byte, v []byte) {
|
||||
select {
|
||||
case ch <- v:
|
||||
default:
|
||||
select {
|
||||
case <-ch:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case ch <- v:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *icomNet) SetReadTimeout(d time.Duration) error { n.readTO = d; return nil }
|
||||
func (n *icomNet) SetDTR(bool) error { return nil } // n/a on the network
|
||||
func (n *icomNet) SetRTS(bool) error { return nil }
|
||||
|
||||
// markRx records that a packet just arrived from the rig (control-link liveness).
|
||||
func (n *icomNet) markRx() { n.lastRx.Store(time.Now().UnixNano()) }
|
||||
|
||||
// Alive reports whether the rig's network server is still talking to us. The rig
|
||||
// pings/idles continuously (even in standby), so a gap means the link — not just
|
||||
// the radio — is gone. Independent of CI-V replies, so a powered-off rig still
|
||||
// reads as Alive and the session isn't torn down. Satisfies aliveTransport.
|
||||
func (n *icomNet) Alive() bool {
|
||||
last := n.lastRx.Load()
|
||||
if last == 0 {
|
||||
return true // just connected, nothing received yet — give it a chance
|
||||
}
|
||||
return time.Since(time.Unix(0, last)) < 6*time.Second
|
||||
}
|
||||
|
||||
// Read returns tunnelled CI-V bytes, mimicking a serial port: (0,nil) on
|
||||
// timeout, (n,nil) with data, (0,err) when the link is closed.
|
||||
func (n *icomNet) Read(p []byte) (int, error) {
|
||||
@@ -129,7 +215,8 @@ func (n *icomNet) Write(p []byte) (int, error) {
|
||||
n.vCivSeq++
|
||||
n.sentMu.Lock()
|
||||
n.sentBuf[seq] = pkt
|
||||
delete(n.sentBuf, seq-256) // keep the buffer bounded (~last 256 packets)
|
||||
delete(n.sentBuf, seq-1024) // keep the buffer bounded (~last 1024 packets) so
|
||||
// the rig's retransmit requests still hit even under sustained CW + poll load
|
||||
n.sentMu.Unlock()
|
||||
if _, err := n.civ.Write(pkt); err != nil {
|
||||
return 0, err
|
||||
@@ -137,28 +224,43 @@ func (n *icomNet) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// icnTrace toggles verbose CI-V request/reply logging for diagnosing the
|
||||
// network transport (temporary).
|
||||
var icnTrace = true
|
||||
// icnTrace toggles verbose per-frame CI-V request/reply logging for diagnosing
|
||||
// the network transport. Off by default (the connect-step logs stay); flip to
|
||||
// true to trace every TX/RX again.
|
||||
var icnTrace = false
|
||||
|
||||
func (n *icomNet) Close() error {
|
||||
n.closeOnce.Do(func() {
|
||||
close(n.done)
|
||||
// Best-effort clean teardown.
|
||||
_, _ = n.civ.Write(icnOpenClose(n.vTracked, n.vID, n.vRemote, n.vCivSeq, 0x00)) // close
|
||||
_, _ = n.civ.Write(icnCtrl(0x05, 0, n.vID, n.vRemote)) // disconnect
|
||||
_, _ = n.ctrl.Write(icnCtrl(0x05, 0, n.cID, n.cRemote))
|
||||
// Tell the rig we're leaving so it frees its SINGLE control session at
|
||||
// once. If it never gets a disconnect it holds the session for minutes and
|
||||
// refuses every new login — which is why a lost link (or a hard app exit)
|
||||
// left the rig un-reconnectable, even from the Icom Remote Utility. UDP is
|
||||
// lossy, so send openClose(close) + disconnect on both streams a few times.
|
||||
// The whole teardown is bounded to ~90 ms so it never stalls the caller
|
||||
// (Settings "Save & Close" / a reconnect's Disconnect).
|
||||
for i := 0; i < 3; i++ {
|
||||
_, _ = n.civ.Write(icnOpenClose(n.vTracked, n.vID, n.vRemote, n.vCivSeq, 0x00)) // close CI-V
|
||||
_, _ = n.civ.Write(icnCtrl(0x05, 0, n.vID, n.vRemote)) // disconnect civ
|
||||
_, _ = n.ctrl.Write(icnCtrl(0x05, 0, n.cID, n.cRemote)) // disconnect ctrl
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
}
|
||||
debugLog.Printf("icom net: sent disconnect to rig (session released)")
|
||||
_ = n.civ.Close()
|
||||
_ = n.ctrl.Close()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ctrlPump keeps the control stream (50001) alive: replies to the rig's pings
|
||||
// and sends idle keepalives. Its own goroutine so it never throttles civPump.
|
||||
// ctrlPump keeps the control stream (50001) alive: replies to the rig's pings,
|
||||
// sends idle keepalives, RENEWS the login token every ~45 s (without this the rig
|
||||
// invalidates the session after ~2 min → total loss of control), and answers the
|
||||
// rig's retransmit requests for those tracked control packets. Its own goroutine
|
||||
// so it never throttles civPump.
|
||||
func (n *icomNet) ctrlPump() {
|
||||
buf := make([]byte, 4096)
|
||||
lastIdle := time.Now()
|
||||
lastToken := time.Now() // token was just granted during dial
|
||||
for {
|
||||
select {
|
||||
case <-n.done:
|
||||
@@ -167,19 +269,49 @@ func (n *icomNet) ctrlPump() {
|
||||
}
|
||||
_ = n.ctrl.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
if k, err := n.ctrl.Read(buf); err == nil && k >= 16 {
|
||||
n.markRx()
|
||||
switch icnLE.Uint16(buf[4:]) {
|
||||
case 0x07: // ping
|
||||
_, _ = n.ctrl.Write(icnPingReply(buf[:k], n.cID, n.cRemote))
|
||||
case 0x01: // retransmit request
|
||||
case 0x01: // retransmit request — resend from the CONTROL sent-buffer
|
||||
if k >= 8 {
|
||||
n.resend(icnLE.Uint16(buf[6:]))
|
||||
n.ctrlResend(icnLE.Uint16(buf[6:]))
|
||||
}
|
||||
case 0x05: // rig-initiated disconnect — it dropped US
|
||||
debugLog.Printf("icom net: rig sent DISCONNECT on control stream — session dropped by the rig")
|
||||
}
|
||||
}
|
||||
if time.Since(lastIdle) > 150*time.Millisecond {
|
||||
if time.Since(lastIdle) > 100*time.Millisecond {
|
||||
_, _ = n.ctrl.Write(icnCtrl(0x00, 0, n.cID, n.cRemote))
|
||||
lastIdle = time.Now()
|
||||
}
|
||||
if time.Since(lastToken) > 45*time.Second {
|
||||
n.renewToken()
|
||||
lastToken = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renewToken re-authorizes the session (control 0x40 token packet, requesttype
|
||||
// 0x05). Tracked so a lost renewal can be retransmitted. Runs only on ctrlPump,
|
||||
// the sole owner of the control-stream auth state, so no locking is needed.
|
||||
func (n *icomNet) renewToken() {
|
||||
seq := n.cTracked
|
||||
pkt := icnTokenRenew(seq, n.cAuthSeq, n.cTokReq, n.cID, n.cRemote, n.cToken)
|
||||
n.cTracked++
|
||||
n.cAuthSeq++
|
||||
n.cSentBuf[seq] = pkt
|
||||
delete(n.cSentBuf, seq-256)
|
||||
_, _ = n.ctrl.Write(pkt)
|
||||
debugLog.Printf("icom net: token renewed (seq %d)", seq)
|
||||
}
|
||||
|
||||
// ctrlResend answers a control-stream retransmit request from the control
|
||||
// sent-buffer (token renewals). Separate from resend(), which owns the CI-V
|
||||
// buffer — the two streams have independent sequence spaces.
|
||||
func (n *icomNet) ctrlResend(seq uint16) {
|
||||
if pkt := n.cSentBuf[seq]; pkt != nil {
|
||||
_, _ = n.ctrl.Write(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +322,7 @@ func (n *icomNet) ctrlPump() {
|
||||
func (n *icomNet) civPump() {
|
||||
buf := make([]byte, 8192)
|
||||
lastIdle := time.Now()
|
||||
lastReq := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-n.done:
|
||||
@@ -198,6 +331,7 @@ func (n *icomNet) civPump() {
|
||||
}
|
||||
_ = n.civ.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
if k, err := n.civ.Read(buf); err == nil && k >= 16 {
|
||||
n.markRx()
|
||||
switch typ := icnLE.Uint16(buf[4:]); {
|
||||
case typ == 0x07: // ping
|
||||
_, _ = n.civ.Write(icnPingReply(buf[:k], n.vID, n.vRemote))
|
||||
@@ -205,29 +339,36 @@ func (n *icomNet) civPump() {
|
||||
if k >= 8 {
|
||||
n.resend(icnLE.Uint16(buf[6:]))
|
||||
}
|
||||
case typ == 0x05: // rig-initiated disconnect — it dropped US
|
||||
debugLog.Printf("icom net: rig sent DISCONNECT on CI-V stream — session dropped by the rig")
|
||||
case typ == 0x00 && k > 0x15 && buf[0x10] == 0xc1: // CI-V data
|
||||
n.trackRxSeq(icnLE.Uint16(buf[6:])) // note gaps for retransmit
|
||||
civBytes := buf[0x15:k]
|
||||
// Skip scope (0x27) frames: over the network the rig streams the
|
||||
// panadapter continuously as large frames that would crowd control
|
||||
// replies out of rx. (Network scope is handled separately.)
|
||||
if !(len(civBytes) >= 5 && civBytes[4] == 0x27) {
|
||||
if icnTrace {
|
||||
debugLog.Printf("icom net RX: % X", civBytes)
|
||||
cp := append([]byte(nil), civBytes...)
|
||||
// Scope (0x27) frames go to their OWN channel: the panadapter streams
|
||||
// continuously as large frames and would otherwise crowd control
|
||||
// replies out of rx (every command would then time out). The scope
|
||||
// feeder in IcomSerial picks them up. Everything else is a control
|
||||
// reply → rx → Read.
|
||||
if len(civBytes) >= 5 && civBytes[4] == 0x27 {
|
||||
icnEnqueueDrop(n.scopeRx, cp)
|
||||
break
|
||||
}
|
||||
if icnTrace {
|
||||
debugLog.Printf("icom net RX: % X", civBytes)
|
||||
}
|
||||
select {
|
||||
case n.rx <- cp:
|
||||
case <-n.done:
|
||||
return
|
||||
default: // buffer full — drop oldest, enqueue newest
|
||||
select {
|
||||
case <-n.rx:
|
||||
default:
|
||||
}
|
||||
cp := append([]byte(nil), civBytes...)
|
||||
select {
|
||||
case n.rx <- cp:
|
||||
case <-n.done:
|
||||
return
|
||||
default: // buffer full — drop oldest, enqueue newest
|
||||
select {
|
||||
case <-n.rx:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case n.rx <- cp:
|
||||
default:
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,6 +377,82 @@ func (n *icomNet) civPump() {
|
||||
_, _ = n.civ.Write(icnCtrl(0x00, 0, n.vID, n.vRemote))
|
||||
lastIdle = time.Now()
|
||||
}
|
||||
if time.Since(lastReq) > 100*time.Millisecond {
|
||||
n.sendRetransmitReq()
|
||||
lastReq = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// icnMaxMissing caps the outstanding retransmit backlog; a bigger jump is treated
|
||||
// as a wrap/desync and the tracker resets rather than requesting a storm.
|
||||
const icnMaxMissing = 50
|
||||
|
||||
// trackRxSeq records the rig's data-packet send seq (outer seq @0x06) and flags
|
||||
// any forward gap as missing so sendRetransmitReq can ask for it. Handles uint16
|
||||
// wrap via the signed distance; ignores duplicates and already-seen packets.
|
||||
func (n *icomNet) trackRxSeq(seq uint16) {
|
||||
if !n.rxHaveSeq {
|
||||
n.rxHaveSeq = true
|
||||
n.rxLastSeq = seq
|
||||
return
|
||||
}
|
||||
switch d := int16(seq - n.rxLastSeq); {
|
||||
case d == 0: // duplicate
|
||||
case d < 0: // an older seq arrived — a retransmit we were missing
|
||||
delete(n.rxMissing, seq)
|
||||
case d == 1: // in order
|
||||
n.rxLastSeq = seq
|
||||
case int(d) <= icnMaxMissing: // forward gap — mark the in-between seqs missing
|
||||
for f := n.rxLastSeq + 1; f != seq; f++ {
|
||||
n.rxMissing[f] = 0
|
||||
}
|
||||
n.rxLastSeq = seq
|
||||
default: // huge jump (wrap/desync) — reset to avoid a false retransmit storm
|
||||
n.rxMissing = make(map[uint16]int)
|
||||
n.rxLastSeq = seq
|
||||
}
|
||||
}
|
||||
|
||||
// sendRetransmitReq asks the rig to resend any CI-V data packets we detected as
|
||||
// missing. Each seq is requested up to 4 times then dropped. Mirrors the Remote
|
||||
// Utility/wfview format: a single miss = a 16-byte control (type 0x01, seq set);
|
||||
// several = a control header + a list of [lo hi lo hi] per seq.
|
||||
func (n *icomNet) sendRetransmitReq() {
|
||||
if len(n.rxMissing) == 0 {
|
||||
return
|
||||
}
|
||||
if len(n.rxMissing) > icnMaxMissing {
|
||||
n.rxMissing = make(map[uint16]int) // hopelessly behind — flush and move on
|
||||
return
|
||||
}
|
||||
var seqs []uint16
|
||||
for s, cnt := range n.rxMissing {
|
||||
if cnt >= 4 {
|
||||
delete(n.rxMissing, s)
|
||||
continue
|
||||
}
|
||||
n.rxMissing[s] = cnt + 1
|
||||
seqs = append(seqs, s)
|
||||
}
|
||||
switch {
|
||||
case len(seqs) == 0:
|
||||
return
|
||||
case len(seqs) == 1:
|
||||
_, _ = n.civ.Write(icnCtrl(0x01, seqs[0], n.vID, n.vRemote))
|
||||
default:
|
||||
b := make([]byte, 16+4*len(seqs))
|
||||
icnLE.PutUint32(b[0:], uint32(len(b)))
|
||||
icnLE.PutUint16(b[4:], 0x01) // type = retransmit request
|
||||
icnLE.PutUint32(b[8:], n.vID)
|
||||
icnLE.PutUint32(b[12:], n.vRemote)
|
||||
off := 16
|
||||
for _, s := range seqs {
|
||||
icnLE.PutUint16(b[off:], s)
|
||||
icnLE.PutUint16(b[off+2:], s)
|
||||
off += 4
|
||||
}
|
||||
_, _ = n.civ.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,12 +465,17 @@ func (n *icomNet) resend(seq uint16) {
|
||||
n.sentMu.Unlock()
|
||||
if pkt != nil {
|
||||
_, _ = n.civ.Write(pkt)
|
||||
} else {
|
||||
// The rig asked for a packet we've already evicted (>256 sent since). It
|
||||
// can't fill its gap → it eventually drops the session. If this shows up in
|
||||
// the log around a disconnect, the send buffer is too small for the load.
|
||||
debugLog.Printf("icom net: retransmit MISS for seq %d (already evicted)", seq)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------- connect -------------------------
|
||||
|
||||
func dialIcomNet(host, user, pass, compName string, rigAddr byte) (*icomNet, error) {
|
||||
func dialIcomNet(host, user, pass, compName string, rigAddr byte, cancel <-chan struct{}) (*icomNet, error) {
|
||||
debugLog.Printf("icom net: connecting to %s (user %q, comp %q, rig addr 0x%02X)", host, user, compName, rigAddr)
|
||||
// ---- control stream (50001): handshake → login → token → conninfo ----
|
||||
craddr, err := net.ResolveUDPAddr("udp4", net.JoinHostPort(host, "50001"))
|
||||
@@ -265,7 +487,7 @@ func dialIcomNet(host, user, pass, compName string, rigAddr byte) (*icomNet, err
|
||||
return nil, fmt.Errorf("dial control: %w", err)
|
||||
}
|
||||
cID := icnLocalID(ctrl)
|
||||
cRemote, err := icnHandshake(ctrl, cID)
|
||||
cRemote, err := icnHandshake(ctrl, cID, cancel)
|
||||
if err != nil {
|
||||
_ = ctrl.Close()
|
||||
debugLog.Printf("icom net: control handshake FAILED (rig unreachable at %s:50001?): %v", host, err)
|
||||
@@ -283,6 +505,10 @@ func dialIcomNet(host, user, pass, compName string, rigAddr byte) (*icomNet, err
|
||||
buf := make([]byte, 2048)
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for token == 0 && time.Now().Before(deadline) {
|
||||
if icnCanceled(cancel) {
|
||||
_ = ctrl.Close()
|
||||
return nil, errDialCanceled
|
||||
}
|
||||
p, ok := icnRecv(ctrl, 200, buf)
|
||||
if !ok {
|
||||
continue
|
||||
@@ -314,6 +540,10 @@ func dialIcomNet(host, user, pass, compName string, rigAddr byte) (*icomNet, err
|
||||
var rigMAC []byte
|
||||
macEnd := time.Now().Add(1200 * time.Millisecond)
|
||||
for time.Now().Before(macEnd) {
|
||||
if icnCanceled(cancel) {
|
||||
_ = ctrl.Close()
|
||||
return nil, errDialCanceled
|
||||
}
|
||||
p, ok := icnRecv(ctrl, 150, buf)
|
||||
if !ok {
|
||||
continue
|
||||
@@ -337,6 +567,10 @@ func dialIcomNet(host, user, pass, compName string, rigAddr byte) (*icomNet, err
|
||||
cInner++
|
||||
drainEnd := time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(drainEnd) {
|
||||
if icnCanceled(cancel) {
|
||||
_ = ctrl.Close()
|
||||
return nil, errDialCanceled
|
||||
}
|
||||
if p, ok := icnRecv(ctrl, 100, buf); ok && icnLE.Uint16(p[4:]) == 0x07 {
|
||||
_, _ = ctrl.Write(icnPingReply(p, cID, cRemote))
|
||||
}
|
||||
@@ -356,14 +590,14 @@ func dialIcomNet(host, user, pass, compName string, rigAddr byte) (*icomNet, err
|
||||
return nil, fmt.Errorf("dial CI-V (local :50002 — is the Icom Remote Utility still running?): %w", err)
|
||||
}
|
||||
vID := icnLocalID(civ)
|
||||
vRemote, err := icnHandshake(civ, vID)
|
||||
vRemote, err := icnHandshake(civ, vID, cancel)
|
||||
if err != nil {
|
||||
_ = civ.Close()
|
||||
_ = ctrl.Close()
|
||||
debugLog.Printf("icom net: CI-V handshake FAILED: %v", err)
|
||||
return nil, fmt.Errorf("CI-V handshake: %w", err)
|
||||
}
|
||||
debugLog.Printf("icom net: CI-V link up — sending power-on, waiting for the rig to boot (~15s)")
|
||||
debugLog.Printf("icom net: CI-V link up — opening the CI-V data flow (rig power left to the ON button)")
|
||||
|
||||
// Bigger receive buffers so a burst of scope/CI-V packets doesn't overflow
|
||||
// (dropped packets → the rig's retransmit requests → session drop).
|
||||
@@ -374,93 +608,46 @@ func dialIcomNet(host, user, pass, compName string, rigAddr byte) (*icomNet, err
|
||||
ctrl: ctrl, civ: civ,
|
||||
cID: cID, cRemote: cRemote, vID: vID, vRemote: vRemote,
|
||||
vTracked: 1, vCivSeq: 1,
|
||||
rx: make(chan []byte, 256),
|
||||
sentBuf: make(map[uint16][]byte),
|
||||
done: make(chan struct{}),
|
||||
rx: make(chan []byte, 256),
|
||||
scopeRx: make(chan []byte, 8),
|
||||
sentBuf: make(map[uint16][]byte),
|
||||
rxMissing: make(map[uint16]int),
|
||||
done: make(chan struct{}),
|
||||
// Auth state for periodic token renewal (see ctrlPump). cTracked/cAuthSeq
|
||||
// continue the control-stream sequences from where the dial's login/token/
|
||||
// conninfo left off.
|
||||
cTracked: cTracked, cAuthSeq: cInner,
|
||||
cToken: token, cTokReq: tokReq,
|
||||
cSentBuf: make(map[uint16][]byte),
|
||||
}
|
||||
// openClose(open) starts the CI-V data flow.
|
||||
n.markRx() // the successful handshake counts as initial rig activity
|
||||
// openClose(open) starts the CI-V data flow. We intentionally DO NOT power the
|
||||
// rig on here — that's a manual ON button now (the user asked not to wake the
|
||||
// rig at launch). If the rig is in standby the control/CI-V streams still stay
|
||||
// up and Alive() stays true (the rig's server answers pings even when the radio
|
||||
// is off), so the session doesn't flap; CI-V just stays silent until ON.
|
||||
ocPkt := icnOpenClose(n.vTracked, vID, vRemote, n.vCivSeq, 0x04)
|
||||
n.sentBuf[n.vTracked] = ocPkt
|
||||
_, _ = civ.Write(ocPkt)
|
||||
n.vTracked++
|
||||
n.vCivSeq++
|
||||
// Power-on (the rig may be in standby — it NG's every command until on):
|
||||
// an FE wake preamble then FE FE <rig> E0 18 01 FD. Harmless if already on.
|
||||
po := make([]byte, 0, 32)
|
||||
for i := 0; i < 25; i++ {
|
||||
po = append(po, 0xFE)
|
||||
}
|
||||
po = append(po, 0xFE, 0xFE, rigAddr, 0xE0, 0x18, 0x01, 0xFD)
|
||||
poPkt := icnCivData(n.vTracked, vID, vRemote, n.vCivSeq, po)
|
||||
n.sentBuf[n.vTracked] = poPkt
|
||||
_, _ = civ.Write(poPkt)
|
||||
n.vTracked++
|
||||
n.vCivSeq++
|
||||
|
||||
// Wait for the rig to finish booting: a rig woken from standby NG's/ignores
|
||||
// commands for ~10-15 s. Poll read-freq (keeping both streams alive) until it
|
||||
// answers, so Connect only returns a READY link — otherwise the manager's
|
||||
// read-timeouts would flap the connection during boot. Give up after 25 s and
|
||||
// return anyway (the rig may already be on and just quiet).
|
||||
if n.waitReady(rigAddr, 25*time.Second) {
|
||||
debugLog.Printf("icom net: rig is READY (answered read-freq) — connection up ✓")
|
||||
} else {
|
||||
debugLog.Printf("icom net: boot wait timed out (25s, no freq reply) — proceeding anyway")
|
||||
}
|
||||
|
||||
go n.ctrlPump()
|
||||
go n.civPump()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// waitReady polls read-freq until the rig replies (booted) or timeout, replying
|
||||
// to pings and sending idle keepalives so the session stays up. Runs before the
|
||||
// pump goroutine starts, so it owns the socket reads.
|
||||
func (n *icomNet) waitReady(rigAddr byte, timeout time.Duration) bool {
|
||||
cbuf := make([]byte, 4096)
|
||||
vbuf := make([]byte, 4096)
|
||||
readFreq := []byte{0xFE, 0xFE, rigAddr, 0xE0, 0x03, 0xFD}
|
||||
end := time.Now().Add(timeout)
|
||||
var lastPoll, lastIdle time.Time
|
||||
for time.Now().Before(end) {
|
||||
if p, ok := icnRecv(n.ctrl, 25, cbuf); ok && icnLE.Uint16(p[4:]) == 0x07 {
|
||||
_, _ = n.ctrl.Write(icnPingReply(p, n.cID, n.cRemote))
|
||||
}
|
||||
if p, ok := icnRecv(n.civ, 25, vbuf); ok {
|
||||
typ := icnLE.Uint16(p[4:])
|
||||
if typ == 0x07 {
|
||||
_, _ = n.civ.Write(icnPingReply(p, n.vID, n.vRemote))
|
||||
} else if typ == 0x00 && len(p) > 0x15 && p[0x10] == 0xc1 {
|
||||
f := p[0x15:]
|
||||
// A frequency reply from the rig: FE FE E0 <rig> 03 …
|
||||
if len(f) >= 6 && f[0] == 0xFE && f[1] == 0xFE && f[3] == rigAddr && f[4] == 0x03 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if time.Since(lastIdle) > 180*time.Millisecond {
|
||||
_, _ = n.ctrl.Write(icnCtrl(0x00, 0, n.cID, n.cRemote))
|
||||
_, _ = n.civ.Write(icnCtrl(0x00, 0, n.vID, n.vRemote))
|
||||
lastIdle = time.Now()
|
||||
}
|
||||
if time.Since(lastPoll) > 1000*time.Millisecond {
|
||||
_, _ = n.civ.Write(icnCivData(n.vTracked, n.vID, n.vRemote, n.vCivSeq, readFreq))
|
||||
n.vTracked++
|
||||
n.vCivSeq++
|
||||
lastPoll = time.Now()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// icnHandshake: areYouThere(seq0) → iAmHere → areYouReady(seq1) → iAmReady.
|
||||
func icnHandshake(c *net.UDPConn, myID uint32) (uint32, error) {
|
||||
func icnHandshake(c *net.UDPConn, myID uint32, cancel <-chan struct{}) (uint32, error) {
|
||||
buf := make([]byte, 2048)
|
||||
_, _ = c.Write(icnCtrl(0x03, 0, myID, 0))
|
||||
var remoteID uint32
|
||||
deadline := time.Now().Add(4 * time.Second)
|
||||
lastTry := time.Now()
|
||||
for time.Now().Before(deadline) {
|
||||
if icnCanceled(cancel) {
|
||||
return 0, errDialCanceled
|
||||
}
|
||||
p, ok := icnRecv(c, 200, buf)
|
||||
if !ok {
|
||||
if remoteID == 0 && time.Since(lastTry) > 500*time.Millisecond {
|
||||
@@ -564,6 +751,27 @@ func icnToken(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32) []byte
|
||||
return b
|
||||
}
|
||||
|
||||
// icnTokenRenew builds the periodic token-renewal packet (control 0x40). Same as
|
||||
// the login-time token confirm but requesttype 0x05 (renew) with the resetcap
|
||||
// field (0x0798 BE @0x24) the Remote Utility sends on renewals. Keeps the rig
|
||||
// from invalidating the session (~2-min timeout without renewal). Offsets per the
|
||||
// wfview token_packet struct (verified) — protocol facts, not copied code.
|
||||
func icnTokenRenew(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32) []byte {
|
||||
b := make([]byte, 0x40)
|
||||
icnLE.PutUint32(b[0:], 0x40)
|
||||
icnLE.PutUint16(b[6:], seq)
|
||||
icnLE.PutUint32(b[8:], sentid)
|
||||
icnLE.PutUint32(b[12:], rcvdid)
|
||||
icnBE.PutUint32(b[0x10:], 0x40-0x10)
|
||||
b[0x14] = 0x01 // requestreply = request
|
||||
b[0x15] = 0x05 // requesttype = token renewal
|
||||
icnBE.PutUint16(b[0x16:], innerSeq)
|
||||
icnLE.PutUint16(b[0x1a:], tokReq)
|
||||
icnLE.PutUint32(b[0x1c:], token)
|
||||
icnBE.PutUint16(b[0x24:], 0x0798) // resetcap
|
||||
return b
|
||||
}
|
||||
|
||||
func icnConnInfo(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, user string, rigMAC []byte, civPort, audioPort uint16) []byte {
|
||||
b := make([]byte, 0x90)
|
||||
icnLE.PutUint32(b[0:], 0x90)
|
||||
|
||||
Reference in New Issue
Block a user