fix: solve issue with Antenna Genius for remote operations
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
// Command icomnettest is an iteration probe for the Icom IP remote protocol
|
||||
// (the LAN server built into the IC-7610 — the one RS-BA1 and wfview talk to).
|
||||
// We're reimplementing it from the public protocol description, so this tool
|
||||
// drives the CONTROL stream (default UDP 50001) and hex-dumps every packet both
|
||||
// ways, letting us confirm the framing / type codes against the real rig before
|
||||
// folding it into internal/cat/icomnet. Nothing here is copied from wfview
|
||||
// (GPLv3) — it's a clean-room implementation from the protocol structure.
|
||||
//
|
||||
// This first milestone is the CONNECTION HANDSHAKE only (no login yet):
|
||||
// areYouThere → iAmHere → areYouReady → iAmReady → periodic idle pings.
|
||||
// Watch the log: if the rig answers our areYouThere we've got the framing right;
|
||||
// its reply reveals the remote station ID we echo back. Login (token + user/
|
||||
// password) is the next step once the handshake is confirmed.
|
||||
//
|
||||
// Usage:
|
||||
// go run ./cmd/icomnettest 192.168.1.60 # control port 50001
|
||||
// go run ./cmd/icomnettest 192.168.1.60 50001 20 # port + run seconds
|
||||
//
|
||||
// SAFE: only the control stream, no CI-V commands, no TX — it just opens and
|
||||
// pings, then disconnects. Share the log and we iterate.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Control-stream packet types (best-known values from the public protocol
|
||||
// description — the very thing we're verifying with this probe).
|
||||
const (
|
||||
typeAreYouThere = 0x03
|
||||
typeIAmHere = 0x04
|
||||
typeDisconnect = 0x05
|
||||
typeAreYouReady = 0x06 // same type both directions (areYouReady / iAmReady)
|
||||
typeIdle = 0x00 // 16-byte keepalive (retransmit/ack carrier)
|
||||
typePing = 0x07 // 21-byte ping (offset 16 = 0x00 request / 0x01 reply, +4-byte payload)
|
||||
)
|
||||
|
||||
// ctrlPacket is the 16-byte common control packet, all fields little-endian:
|
||||
//
|
||||
// uint32 len (=0x10) · uint16 type · uint16 seq · uint32 sentid · uint32 rcvdid
|
||||
func ctrlPacket(typ uint16, seq uint16, sentid, rcvdid uint32) []byte {
|
||||
b := make([]byte, 16)
|
||||
binary.LittleEndian.PutUint32(b[0:], 0x10)
|
||||
binary.LittleEndian.PutUint16(b[4:], typ)
|
||||
binary.LittleEndian.PutUint16(b[6:], seq)
|
||||
binary.LittleEndian.PutUint32(b[8:], sentid)
|
||||
binary.LittleEndian.PutUint32(b[12:], rcvdid)
|
||||
return b
|
||||
}
|
||||
|
||||
func parseHeader(b []byte) (length uint32, typ, seq uint16, sentid, rcvdid uint32, ok bool) {
|
||||
if len(b) < 16 {
|
||||
return 0, 0, 0, 0, 0, false
|
||||
}
|
||||
length = binary.LittleEndian.Uint32(b[0:])
|
||||
typ = binary.LittleEndian.Uint16(b[4:])
|
||||
seq = binary.LittleEndian.Uint16(b[6:])
|
||||
sentid = binary.LittleEndian.Uint32(b[8:])
|
||||
rcvdid = binary.LittleEndian.Uint32(b[12:])
|
||||
return length, typ, seq, sentid, rcvdid, true
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("usage: icomnettest <rig-ip> [control-port] [run-seconds]")
|
||||
fmt.Println("example: icomnettest 192.168.1.60 50001 20")
|
||||
os.Exit(2)
|
||||
}
|
||||
ip := os.Args[1]
|
||||
port := 50001
|
||||
if len(os.Args) >= 3 {
|
||||
if v, err := strconv.Atoi(os.Args[2]); err == nil {
|
||||
port = v
|
||||
}
|
||||
}
|
||||
runSecs := 20
|
||||
if len(os.Args) >= 4 {
|
||||
if v, err := strconv.Atoi(os.Args[3]); err == nil && v > 0 {
|
||||
runSecs = v
|
||||
}
|
||||
}
|
||||
|
||||
target := net.JoinHostPort(ip, strconv.Itoa(port))
|
||||
conn, err := net.Dial("udp4", target)
|
||||
if err != nil {
|
||||
fmt.Printf("dial %s: %v\n", target, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Our local station ID. Real clients derive it from the local IP:port; a
|
||||
// stable non-zero value is fine for probing. We'll refine once we see how the
|
||||
// rig echoes it back.
|
||||
local := conn.LocalAddr().(*net.UDPAddr)
|
||||
myID := uint32(local.IP.To4()[0])<<24 | uint32(local.IP.To4()[1])<<16 | uint32(uint16(local.Port))
|
||||
var remoteID uint32
|
||||
var seq uint16
|
||||
|
||||
logTx := func(name string, p []byte) {
|
||||
fmt.Printf("TX %-14s (%d bytes)\n%s\n", name, len(p), hex.Dump(p))
|
||||
if _, err := conn.Write(p); err != nil {
|
||||
fmt.Printf(" write error: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Probing Icom control stream at %s (myID=0x%08X)\n\n", target, myID)
|
||||
|
||||
// 1) areYouThere — ask the rig to announce itself.
|
||||
seq++
|
||||
logTx("areYouThere", ctrlPacket(typeAreYouThere, seq, myID, 0))
|
||||
|
||||
// Read loop: dump everything, and advance the handshake when we recognise a
|
||||
// reply. Runs for runSecs then disconnects.
|
||||
deadline := time.Now().Add(time.Duration(runSecs) * time.Second)
|
||||
buf := make([]byte, 2048)
|
||||
lastIdle := time.Now()
|
||||
readyStarted := false
|
||||
for time.Now().Before(deadline) {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
// Periodic idle keepalive once connected.
|
||||
if remoteID != 0 && time.Since(lastIdle) > 100*time.Millisecond {
|
||||
seq++
|
||||
logTx("idle", ctrlPacket(typeIdle, seq, myID, remoteID))
|
||||
lastIdle = time.Now()
|
||||
}
|
||||
continue
|
||||
}
|
||||
fmt.Printf("read error: %v\n", err)
|
||||
break
|
||||
}
|
||||
pkt := append([]byte(nil), buf[:n]...)
|
||||
length, typ, rseq, sentid, rcvdid, ok := parseHeader(pkt)
|
||||
if !ok {
|
||||
fmt.Printf("RX (%d bytes, too short to parse)\n%s\n", n, hex.Dump(pkt))
|
||||
continue
|
||||
}
|
||||
fmt.Printf("RX len=%d type=0x%02X seq=%d sentid=0x%08X rcvdid=0x%08X (%d bytes)\n%s\n",
|
||||
length, typ, rseq, sentid, rcvdid, n, hex.Dump(pkt))
|
||||
|
||||
switch typ {
|
||||
case typeIAmHere:
|
||||
remoteID = sentid // the rig's ID — echo it back as rcvdid from now on
|
||||
fmt.Printf(">> iAmHere: remoteID=0x%08X — sending areYouReady\n\n", remoteID)
|
||||
seq++
|
||||
logTx("areYouReady", ctrlPacket(typeAreYouReady, seq, myID, remoteID))
|
||||
readyStarted = true
|
||||
case typeAreYouReady:
|
||||
if readyStarted {
|
||||
fmt.Printf(">> iAmReady — control link is up. (Login is the next milestone.)\n\n")
|
||||
}
|
||||
case typePing:
|
||||
// Reply to the rig's ping: mirror the packet, swap sender/receiver IDs,
|
||||
// set the reply flag at offset 16. Keeps the link healthy so we can
|
||||
// observe the connection long enough to work on login.
|
||||
reply := append([]byte(nil), pkt...)
|
||||
if len(reply) >= 17 {
|
||||
binary.LittleEndian.PutUint32(reply[8:], myID)
|
||||
binary.LittleEndian.PutUint32(reply[12:], remoteID)
|
||||
reply[16] = 0x01 // request → reply
|
||||
logTx("pingReply", reply)
|
||||
}
|
||||
case typeDisconnect:
|
||||
fmt.Printf(">> rig sent disconnect\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Clean disconnect.
|
||||
if remoteID != 0 {
|
||||
seq++
|
||||
logTx("disconnect", ctrlPacket(typeDisconnect, seq, myID, remoteID))
|
||||
}
|
||||
fmt.Println("Done. Paste the log — especially the rig's replies to areYouThere.")
|
||||
}
|
||||
Reference in New Issue
Block a user