feat: Implemented Icom Ethernet CAT control
This commit is contained in:
@@ -0,0 +1,643 @@
|
||||
package cat
|
||||
|
||||
// icomnet.go — the NETWORK transport for the Icom backend. It talks the Icom IP
|
||||
// remote protocol (the LAN server built into the IC-7610, the one the Icom
|
||||
// Remote Utility speaks) directly, and presents the tunnelled CI-V byte stream
|
||||
// as a plain civTransport (Read/Write). So the entire IcomController surface —
|
||||
// freq/mode, receive-DSP, TX, scope, RIT, CW — runs unchanged over the network;
|
||||
// only the transport differs. OpsLog thus replaces BOTH the Remote Utility and
|
||||
// RS-BA1.
|
||||
//
|
||||
// The protocol (framing, passcode table, packet offsets, power-on) was
|
||||
// reimplemented from the public wfview protocol and verified byte-for-byte
|
||||
// against real Remote-Utility captures. No GPLv3 code is copied.
|
||||
//
|
||||
// Three UDP streams exist on the rig (control 50001 / CI-V 50002 / audio 50003);
|
||||
// this transport uses control + CI-V (audio is not needed for CAT). Connect:
|
||||
// control: areYouThere→iAmHere→areYouReady→iAmReady → login → token → conninfo
|
||||
// civ: areYouThere→…→iAmReady → openClose(open) → power-on
|
||||
// then CI-V flows in data packets. A pump goroutine keeps both streams alive
|
||||
// (ping replies + idle keepalives) and feeds received CI-V bytes to Read.
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var icnLE = binary.LittleEndian
|
||||
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 {
|
||||
if civAddr <= 0 || civAddr > 0xFF {
|
||||
civAddr = 0x98 // IC-7610
|
||||
}
|
||||
if digitalDefault == "" {
|
||||
digitalDefault = "FT8"
|
||||
}
|
||||
b := &IcomSerial{
|
||||
portName: host,
|
||||
rigAddr: byte(civAddr),
|
||||
digital: strings.ToUpper(digitalDefault),
|
||||
model: "Icom",
|
||||
scopeFixed: true,
|
||||
}
|
||||
b.open = func() (civTransport, error) {
|
||||
if strings.TrimSpace(host) == "" {
|
||||
return nil, fmt.Errorf("no rig host configured")
|
||||
}
|
||||
return dialIcomNet(host, user, pass, "OpsLog", b.rigAddr)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// icomNet is the connected network transport. It satisfies civTransport.
|
||||
type icomNet struct {
|
||||
ctrl *net.UDPConn // control stream (50001)
|
||||
civ *net.UDPConn // CI-V stream (50002)
|
||||
|
||||
cID, cRemote uint32 // control stream ids
|
||||
vID, vRemote uint32 // civ stream ids
|
||||
|
||||
// Tracked-packet sequence + CI-V data sequence. Written only by Write (on the
|
||||
// CAT goroutine) and during dial — never by the pump — so no lock is needed.
|
||||
vTracked uint16
|
||||
vCivSeq uint16
|
||||
|
||||
rx chan []byte // CI-V byte chunks from civPump → Read
|
||||
leftover []byte // partial chunk not yet returned by Read (Read-only)
|
||||
readTO time.Duration // Read timeout (SetReadTimeout)
|
||||
|
||||
// sentBuf keeps recently-sent tracked civ packets (by outer seq) so we can
|
||||
// answer the rig's UDP retransmit requests. Written by Write (CAT goroutine),
|
||||
// read by the pumps → guarded by sentMu.
|
||||
sentMu sync.Mutex
|
||||
sentBuf map[uint16][]byte
|
||||
|
||||
done chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
// 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) {
|
||||
if len(n.leftover) > 0 {
|
||||
k := copy(p, n.leftover)
|
||||
n.leftover = n.leftover[k:]
|
||||
return k, nil
|
||||
}
|
||||
to := n.readTO
|
||||
if to <= 0 {
|
||||
to = 60 * time.Millisecond
|
||||
}
|
||||
select {
|
||||
case f, ok := <-n.rx:
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
}
|
||||
k := copy(p, f)
|
||||
if k < len(f) {
|
||||
n.leftover = append(n.leftover[:0], f[k:]...)
|
||||
}
|
||||
return k, nil
|
||||
case <-time.After(to):
|
||||
return 0, nil // timeout, no data
|
||||
case <-n.done:
|
||||
return 0, io.EOF
|
||||
}
|
||||
}
|
||||
|
||||
// Write wraps raw CI-V bytes (FE FE … FD) in a data packet and sends them.
|
||||
func (n *icomNet) Write(p []byte) (int, error) {
|
||||
if icnTrace {
|
||||
debugLog.Printf("icom net TX: % X", p)
|
||||
}
|
||||
seq := n.vTracked
|
||||
pkt := icnCivData(seq, n.vID, n.vRemote, n.vCivSeq, p)
|
||||
n.vTracked++
|
||||
n.vCivSeq++
|
||||
n.sentMu.Lock()
|
||||
n.sentBuf[seq] = pkt
|
||||
delete(n.sentBuf, seq-256) // keep the buffer bounded (~last 256 packets)
|
||||
n.sentMu.Unlock()
|
||||
if _, err := n.civ.Write(pkt); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// icnTrace toggles verbose CI-V request/reply logging for diagnosing the
|
||||
// network transport (temporary).
|
||||
var icnTrace = true
|
||||
|
||||
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))
|
||||
_ = 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.
|
||||
func (n *icomNet) ctrlPump() {
|
||||
buf := make([]byte, 4096)
|
||||
lastIdle := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-n.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
_ = n.ctrl.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
if k, err := n.ctrl.Read(buf); err == nil && k >= 16 {
|
||||
switch icnLE.Uint16(buf[4:]) {
|
||||
case 0x07: // ping
|
||||
_, _ = n.ctrl.Write(icnPingReply(buf[:k], n.cID, n.cRemote))
|
||||
case 0x01: // retransmit request
|
||||
if k >= 8 {
|
||||
n.resend(icnLE.Uint16(buf[6:]))
|
||||
}
|
||||
}
|
||||
}
|
||||
if time.Since(lastIdle) > 150*time.Millisecond {
|
||||
_, _ = n.ctrl.Write(icnCtrl(0x00, 0, n.cID, n.cRemote))
|
||||
lastIdle = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// civPump owns the CI-V stream (50002): drains it as fast as packets arrive
|
||||
// (its own goroutine — not throttled by the control reads), replies to pings,
|
||||
// answers retransmit requests, skips scope frames, and feeds control CI-V bytes
|
||||
// to Read via n.rx.
|
||||
func (n *icomNet) civPump() {
|
||||
buf := make([]byte, 8192)
|
||||
lastIdle := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-n.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
_ = n.civ.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
if k, err := n.civ.Read(buf); err == nil && k >= 16 {
|
||||
switch typ := icnLE.Uint16(buf[4:]); {
|
||||
case typ == 0x07: // ping
|
||||
_, _ = n.civ.Write(icnPingReply(buf[:k], n.vID, n.vRemote))
|
||||
case typ == 0x01: // retransmit request — resend that seq
|
||||
if k >= 8 {
|
||||
n.resend(icnLE.Uint16(buf[6:]))
|
||||
}
|
||||
case typ == 0x00 && k > 0x15 && buf[0x10] == 0xc1: // CI-V data
|
||||
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...)
|
||||
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:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if time.Since(lastIdle) > 150*time.Millisecond {
|
||||
_, _ = n.civ.Write(icnCtrl(0x00, 0, n.vID, n.vRemote))
|
||||
lastIdle = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resend re-transmits a previously-sent tracked CI-V packet the rig asks for
|
||||
// (its UDP retransmit mechanism). Without this the rig drops the whole session
|
||||
// after a few seconds when a packet is lost under load.
|
||||
func (n *icomNet) resend(seq uint16) {
|
||||
n.sentMu.Lock()
|
||||
pkt := n.sentBuf[seq]
|
||||
n.sentMu.Unlock()
|
||||
if pkt != nil {
|
||||
_, _ = n.civ.Write(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------- connect -------------------------
|
||||
|
||||
func dialIcomNet(host, user, pass, compName string, rigAddr byte) (*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"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctrl, err := net.DialUDP("udp4", nil, craddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial control: %w", err)
|
||||
}
|
||||
cID := icnLocalID(ctrl)
|
||||
cRemote, err := icnHandshake(ctrl, cID)
|
||||
if err != nil {
|
||||
_ = ctrl.Close()
|
||||
debugLog.Printf("icom net: control handshake FAILED (rig unreachable at %s:50001?): %v", host, err)
|
||||
return nil, fmt.Errorf("control handshake: %w", err)
|
||||
}
|
||||
debugLog.Printf("icom net: control link up (rig id 0x%08X) — logging in", cRemote)
|
||||
|
||||
var cTracked, cInner uint16 = 1, 1
|
||||
tokReq := uint16(0x0c77)
|
||||
_, _ = ctrl.Write(icnLogin(cTracked, cInner, tokReq, cID, cRemote, 0, user, pass, compName))
|
||||
cTracked++
|
||||
cInner++
|
||||
|
||||
var token uint32
|
||||
buf := make([]byte, 2048)
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for token == 0 && time.Now().Before(deadline) {
|
||||
p, ok := icnRecv(ctrl, 200, buf)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
length := icnLE.Uint32(p[0:])
|
||||
typ := icnLE.Uint16(p[4:])
|
||||
if typ == 0x00 && length == 0x60 && len(p) >= 0x34 { // login response
|
||||
token = icnLE.Uint32(p[0x1c:])
|
||||
if e := icnLE.Uint32(p[0x30:]); e != 0 || token == 0 {
|
||||
_ = ctrl.Close()
|
||||
debugLog.Printf("icom net: LOGIN REJECTED (err=0x%08X) — wrong Network User1 ID/Password", e)
|
||||
return nil, fmt.Errorf("login rejected — check the rig's Network User1 ID/Password")
|
||||
}
|
||||
_, _ = ctrl.Write(icnToken(cTracked, cInner, tokReq, cID, cRemote, token))
|
||||
cTracked++
|
||||
cInner++
|
||||
debugLog.Printf("icom net: LOGIN OK, token 0x%08X", token)
|
||||
} else if typ == 0x07 {
|
||||
_, _ = ctrl.Write(icnPingReply(p, cID, cRemote))
|
||||
}
|
||||
}
|
||||
if token == 0 {
|
||||
_ = ctrl.Close()
|
||||
debugLog.Printf("icom net: login TIMED OUT (no token in 5s) — check host/credentials")
|
||||
return nil, fmt.Errorf("login timed out (no token) — check host/credentials")
|
||||
}
|
||||
|
||||
// Learn the rig's MAC from its conninfo push (144B) to echo in our conninfo.
|
||||
var rigMAC []byte
|
||||
macEnd := time.Now().Add(1200 * time.Millisecond)
|
||||
for time.Now().Before(macEnd) {
|
||||
p, ok := icnRecv(ctrl, 150, buf)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(p) >= 0x30 && icnLE.Uint32(p[0:]) == 0x90 { // 144-byte conninfo push
|
||||
rigMAC = append([]byte(nil), p[0x2a:0x30]...)
|
||||
}
|
||||
if icnLE.Uint16(p[4:]) == 0x07 {
|
||||
_, _ = ctrl.Write(icnPingReply(p, cID, cRemote))
|
||||
}
|
||||
if rigMAC != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if rigMAC == nil {
|
||||
rigMAC = make([]byte, 6)
|
||||
}
|
||||
|
||||
_, _ = ctrl.Write(icnConnInfo(cTracked, cInner, tokReq, cID, cRemote, token, user, rigMAC, 50002, 50003))
|
||||
cTracked++
|
||||
cInner++
|
||||
drainEnd := time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(drainEnd) {
|
||||
if p, ok := icnRecv(ctrl, 100, buf); ok && icnLE.Uint16(p[4:]) == 0x07 {
|
||||
_, _ = ctrl.Write(icnPingReply(p, cID, cRemote))
|
||||
}
|
||||
}
|
||||
|
||||
// ---- CI-V stream (50002): bind LOCAL :50002 (the announced civport) ----
|
||||
vraddr, err := net.ResolveUDPAddr("udp4", net.JoinHostPort(host, "50002"))
|
||||
if err != nil {
|
||||
_ = ctrl.Close()
|
||||
return nil, err
|
||||
}
|
||||
debugLog.Printf("icom net: conninfo sent (rig mac % X) — opening CI-V stream", rigMAC)
|
||||
civ, err := net.DialUDP("udp4", &net.UDPAddr{Port: 50002}, vraddr)
|
||||
if err != nil {
|
||||
_ = ctrl.Close()
|
||||
debugLog.Printf("icom net: cannot bind local :50002 — the Icom Remote Utility is probably still running: %v", 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)
|
||||
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)")
|
||||
|
||||
// Bigger receive buffers so a burst of scope/CI-V packets doesn't overflow
|
||||
// (dropped packets → the rig's retransmit requests → session drop).
|
||||
_ = ctrl.SetReadBuffer(1 << 20)
|
||||
_ = civ.SetReadBuffer(1 << 20)
|
||||
|
||||
n := &icomNet{
|
||||
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{}),
|
||||
}
|
||||
// openClose(open) starts the CI-V data flow.
|
||||
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) {
|
||||
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) {
|
||||
p, ok := icnRecv(c, 200, buf)
|
||||
if !ok {
|
||||
if remoteID == 0 && time.Since(lastTry) > 500*time.Millisecond {
|
||||
_, _ = c.Write(icnCtrl(0x03, 0, myID, 0))
|
||||
lastTry = time.Now()
|
||||
}
|
||||
continue
|
||||
}
|
||||
typ := icnLE.Uint16(p[4:])
|
||||
sentid := icnLE.Uint32(p[8:])
|
||||
switch typ {
|
||||
case 0x04: // iAmHere
|
||||
remoteID = sentid
|
||||
_, _ = c.Write(icnCtrl(0x06, 1, myID, remoteID))
|
||||
case 0x06: // iAmReady
|
||||
if remoteID != 0 {
|
||||
return remoteID, nil
|
||||
}
|
||||
case 0x07: // ping
|
||||
_, _ = c.Write(icnPingReply(p, myID, remoteID))
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("handshake timeout")
|
||||
}
|
||||
|
||||
func icnRecv(c *net.UDPConn, ms int, buf []byte) ([]byte, bool) {
|
||||
_ = c.SetReadDeadline(time.Now().Add(time.Duration(ms) * time.Millisecond))
|
||||
k, err := c.Read(buf)
|
||||
if err != nil || k < 16 {
|
||||
return nil, false
|
||||
}
|
||||
return buf[:k], true
|
||||
}
|
||||
|
||||
func icnLocalID(c *net.UDPConn) uint32 {
|
||||
a := c.LocalAddr().(*net.UDPAddr)
|
||||
ip := a.IP.To4()
|
||||
if ip == nil {
|
||||
ip = []byte{192, 168, 0, 1}
|
||||
}
|
||||
return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(uint16(a.Port))
|
||||
}
|
||||
|
||||
// ------------------------- packet builders -------------------------
|
||||
// (offsets verified vs wfview structs + real captures)
|
||||
|
||||
func icnCtrl(typ, seq uint16, sentid, rcvdid uint32) []byte {
|
||||
b := make([]byte, 16)
|
||||
icnLE.PutUint32(b[0:], 0x10)
|
||||
icnLE.PutUint16(b[4:], typ)
|
||||
icnLE.PutUint16(b[6:], seq)
|
||||
icnLE.PutUint32(b[8:], sentid)
|
||||
icnLE.PutUint32(b[12:], rcvdid)
|
||||
return b
|
||||
}
|
||||
|
||||
func icnPingReply(pkt []byte, myID, remoteID uint32) []byte {
|
||||
r := append([]byte(nil), pkt...)
|
||||
if len(r) >= 17 {
|
||||
icnLE.PutUint32(r[8:], myID)
|
||||
icnLE.PutUint32(r[12:], remoteID)
|
||||
r[16] = 0x01
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func icnLogin(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, user, pass, name string) []byte {
|
||||
b := make([]byte, 0x80)
|
||||
icnLE.PutUint32(b[0:], 0x80)
|
||||
icnLE.PutUint16(b[6:], seq)
|
||||
icnLE.PutUint32(b[8:], sentid)
|
||||
icnLE.PutUint32(b[12:], rcvdid)
|
||||
icnBE.PutUint32(b[0x10:], 0x80-0x10)
|
||||
b[0x14] = 0x01
|
||||
b[0x15] = 0x00
|
||||
icnBE.PutUint16(b[0x16:], innerSeq)
|
||||
icnLE.PutUint16(b[0x1a:], tokReq)
|
||||
icnLE.PutUint32(b[0x1c:], token)
|
||||
copy(b[0x40:0x50], icnPasscode(user))
|
||||
copy(b[0x50:0x60], icnPasscode(pass))
|
||||
nm := name
|
||||
if len(nm) > 16 {
|
||||
nm = nm[:16]
|
||||
}
|
||||
copy(b[0x60:0x70], []byte(nm))
|
||||
return b
|
||||
}
|
||||
|
||||
func icnToken(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
|
||||
b[0x15] = 0x02
|
||||
icnBE.PutUint16(b[0x16:], innerSeq)
|
||||
icnLE.PutUint16(b[0x1a:], tokReq)
|
||||
icnLE.PutUint32(b[0x1c:], token)
|
||||
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)
|
||||
icnLE.PutUint16(b[6:], seq)
|
||||
icnLE.PutUint32(b[8:], sentid)
|
||||
icnLE.PutUint32(b[12:], rcvdid)
|
||||
icnBE.PutUint32(b[0x10:], 0x90-0x10)
|
||||
b[0x14] = 0x01
|
||||
b[0x15] = 0x03 // requesttype = conninfo / open streams
|
||||
icnBE.PutUint16(b[0x16:], innerSeq)
|
||||
icnLE.PutUint16(b[0x1a:], tokReq)
|
||||
icnLE.PutUint32(b[0x1c:], token)
|
||||
icnLE.PutUint16(b[0x27:], 0x8010) // commoncap
|
||||
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[0x72] = 0x10 // rxcodec
|
||||
b[0x73] = 0x04 // txcodec
|
||||
icnBE.PutUint32(b[0x74:], 16000)
|
||||
icnBE.PutUint32(b[0x78:], 8000)
|
||||
icnBE.PutUint32(b[0x7c:], uint32(civPort))
|
||||
icnBE.PutUint32(b[0x80:], uint32(audioPort))
|
||||
icnBE.PutUint32(b[0x84:], 100)
|
||||
b[0x88] = 0x00
|
||||
return b
|
||||
}
|
||||
|
||||
func icnOpenClose(seq uint16, sentid, rcvdid uint32, civSeq uint16, magic byte) []byte {
|
||||
b := make([]byte, 0x16)
|
||||
icnLE.PutUint32(b[0:], 0x16)
|
||||
icnLE.PutUint16(b[6:], seq)
|
||||
icnLE.PutUint32(b[8:], sentid)
|
||||
icnLE.PutUint32(b[12:], rcvdid)
|
||||
icnLE.PutUint16(b[0x10:], 0x01c0)
|
||||
icnBE.PutUint16(b[0x13:], civSeq)
|
||||
b[0x15] = magic
|
||||
return b
|
||||
}
|
||||
|
||||
func icnCivData(seq uint16, sentid, rcvdid uint32, civSeq uint16, civ []byte) []byte {
|
||||
nn := 0x15 + len(civ)
|
||||
b := make([]byte, nn)
|
||||
icnLE.PutUint32(b[0:], uint32(nn))
|
||||
icnLE.PutUint16(b[6:], seq)
|
||||
icnLE.PutUint32(b[8:], sentid)
|
||||
icnLE.PutUint32(b[12:], rcvdid)
|
||||
b[0x10] = 0xc1
|
||||
icnLE.PutUint16(b[0x11:], uint16(len(civ)))
|
||||
icnBE.PutUint16(b[0x13:], civSeq)
|
||||
copy(b[0x15:], civ)
|
||||
return b
|
||||
}
|
||||
|
||||
// icnPasscodeSeq — Icom's obfuscation table (values at index 0x20..0x7e).
|
||||
// VERIFIED: user "f6bgc" → 3F 65 50 25 55 (matches the capture).
|
||||
var icnPasscodeSeq = [256]byte{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0x47, 0x5d, 0x4c, 0x42, 0x66, 0x20, 0x23, 0x46, 0x4e, 0x57, 0x45, 0x3d, 0x67, 0x76, 0x60, 0x41, 0x62, 0x39, 0x59, 0x2d, 0x68, 0x7e,
|
||||
0x7c, 0x65, 0x7d, 0x49, 0x29, 0x72, 0x73, 0x78, 0x21, 0x6e, 0x5a, 0x5e, 0x4a, 0x3e, 0x71, 0x2c, 0x2a, 0x54, 0x3c, 0x3a, 0x63, 0x4f,
|
||||
0x43, 0x75, 0x27, 0x79, 0x5b, 0x35, 0x70, 0x48, 0x6b, 0x56, 0x6f, 0x34, 0x32, 0x6c, 0x30, 0x61, 0x6d, 0x7b, 0x2f, 0x4b, 0x64, 0x38,
|
||||
0x2b, 0x2e, 0x50, 0x40, 0x3f, 0x55, 0x33, 0x37, 0x25, 0x77, 0x24, 0x26, 0x74, 0x6a, 0x28, 0x53, 0x4d, 0x69, 0x22, 0x5c, 0x44, 0x31,
|
||||
0x36, 0x58, 0x3b, 0x7a, 0x51, 0x5f, 0x52,
|
||||
}
|
||||
|
||||
func icnPasscode(s string) []byte {
|
||||
out := make([]byte, 0, len(s))
|
||||
for i := 0; i < len(s) && i < 16; i++ {
|
||||
p := int(s[i]) + i
|
||||
if p > 126 {
|
||||
p = 32 + p%127
|
||||
}
|
||||
out = append(out, icnPasscodeSeq[p])
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user