feat: added full support in USB (local) & ethernet (local or remote) of audio for Icom
This commit is contained in:
+38
-8
@@ -36,7 +36,13 @@ var icnBE = binary.BigEndian
|
||||
// NewIcomNet builds an (unconnected) Icom backend whose transport is the network
|
||||
// stream. host is the rig's IP/hostname; user/pass are the rig's Network User1
|
||||
// credentials. Reuses the whole IcomSerial controller — only `open` differs.
|
||||
func NewIcomNet(host, user, pass string, civAddr int, digitalDefault string) *IcomSerial {
|
||||
//
|
||||
// audioSink (optional) enables the network RX audio stream (UDP 50003): when
|
||||
// non-nil the conninfo asks the rig to stream audio and each received payload is
|
||||
// passed to audioSink (the app decodes it via an audio.Codec and plays it). nil
|
||||
// = CI-V only (the proven default). The audio stream is fully separate from CAT,
|
||||
// so enabling it can't affect freq/mode/DSP control.
|
||||
func NewIcomNet(host, user, pass string, civAddr int, digitalDefault string, audioSink func([]byte)) *IcomSerial {
|
||||
if civAddr <= 0 || civAddr > 0xFF {
|
||||
civAddr = 0x98 // IC-7610
|
||||
}
|
||||
@@ -57,7 +63,7 @@ func NewIcomNet(host, user, pass string, civAddr int, digitalDefault string) *Ic
|
||||
b.dialMu.Lock()
|
||||
cancel := b.dialCancel
|
||||
b.dialMu.Unlock()
|
||||
return dialIcomNet(host, user, pass, "OpsLog", b.rigAddr, cancel)
|
||||
return dialIcomNet(host, user, pass, "OpsLog", b.rigAddr, cancel, audioSink)
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -130,6 +136,10 @@ type icomNet struct {
|
||||
// but link fine" (stay connected) from "link dead" (reconnect). See Alive().
|
||||
lastRx atomic.Int64
|
||||
|
||||
// audio is the optional RX audio stream (UDP 50003). nil when audio is off.
|
||||
// Torn down alongside the CI-V/control streams in Close.
|
||||
audio *icomAudio
|
||||
|
||||
done chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
@@ -232,6 +242,9 @@ var icnTrace = false
|
||||
func (n *icomNet) Close() error {
|
||||
n.closeOnce.Do(func() {
|
||||
close(n.done)
|
||||
if n.audio != nil {
|
||||
n.audio.Close()
|
||||
}
|
||||
// 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)
|
||||
@@ -475,8 +488,9 @@ func (n *icomNet) resend(seq uint16) {
|
||||
|
||||
// ------------------------- connect -------------------------
|
||||
|
||||
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)
|
||||
func dialIcomNet(host, user, pass, compName string, rigAddr byte, cancel <-chan struct{}, audioSink func([]byte)) (*icomNet, error) {
|
||||
wantAudio := audioSink != nil
|
||||
debugLog.Printf("icom net: connecting to %s (user %q, comp %q, rig addr 0x%02X, audio=%v)", host, user, compName, rigAddr, wantAudio)
|
||||
// ---- control stream (50001): handshake → login → token → conninfo ----
|
||||
craddr, err := net.ResolveUDPAddr("udp4", net.JoinHostPort(host, "50001"))
|
||||
if err != nil {
|
||||
@@ -562,7 +576,11 @@ func dialIcomNet(host, user, pass, compName string, rigAddr byte, cancel <-chan
|
||||
rigMAC = make([]byte, 6)
|
||||
}
|
||||
|
||||
_, _ = ctrl.Write(icnConnInfo(cTracked, cInner, tokReq, cID, cRemote, token, user, rigMAC, 50002, 50003))
|
||||
var rxEnable byte
|
||||
if wantAudio {
|
||||
rxEnable = 0x01 // ask the rig to stream RX audio on 50003
|
||||
}
|
||||
_, _ = ctrl.Write(icnConnInfo(cTracked, cInner, tokReq, cID, cRemote, token, user, rigMAC, 50002, 50003, rxEnable))
|
||||
cTracked++
|
||||
cInner++
|
||||
drainEnd := time.Now().Add(500 * time.Millisecond)
|
||||
@@ -634,6 +652,18 @@ func dialIcomNet(host, user, pass, compName string, rigAddr byte, cancel <-chan
|
||||
|
||||
go n.ctrlPump()
|
||||
go n.civPump()
|
||||
|
||||
// Optional RX audio stream (50003). The rig was told (conninfo rxEnable=1) to
|
||||
// stream audio; open the socket + handshake now. A failure here is NON-fatal:
|
||||
// CAT works without audio, so we log and continue rather than tear down a
|
||||
// perfectly good control/CI-V session.
|
||||
if wantAudio {
|
||||
if a, err := dialIcomAudio(host, audioSink, cancel); err != nil {
|
||||
debugLog.Printf("icom net: audio stream FAILED (CAT unaffected): %v", err)
|
||||
} else {
|
||||
n.audio = a
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -772,7 +802,7 @@ func icnTokenRenew(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32) [
|
||||
return b
|
||||
}
|
||||
|
||||
func icnConnInfo(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, user string, rigMAC []byte, civPort, audioPort uint16) []byte {
|
||||
func icnConnInfo(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, user string, rigMAC []byte, civPort, audioPort uint16, rxEnable byte) []byte {
|
||||
b := make([]byte, 0x90)
|
||||
icnLE.PutUint32(b[0:], 0x90)
|
||||
icnLE.PutUint16(b[6:], seq)
|
||||
@@ -788,8 +818,8 @@ func icnConnInfo(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, use
|
||||
copy(b[0x2a:0x30], rigMAC)
|
||||
copy(b[0x40:0x60], []byte("IC-7610"))
|
||||
copy(b[0x60:0x70], icnPasscode(user))
|
||||
b[0x70] = 0x00 // rxenable (audio off — CI-V only)
|
||||
b[0x71] = 0x00 // txenable
|
||||
b[0x70] = rxEnable // rxenable: 1 opens the 50003 RX audio stream, 0 = CI-V only
|
||||
b[0x71] = 0x00 // txenable (Phase 5)
|
||||
b[0x72] = 0x10 // rxcodec
|
||||
b[0x73] = 0x04 // txcodec
|
||||
icnBE.PutUint32(b[0x74:], 16000)
|
||||
|
||||
Reference in New Issue
Block a user