// 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 [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.") }