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 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 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 }