467 lines
16 KiB
Go
467 lines
16 KiB
Go
// Command icomnettest is an iteration probe for the Icom IP remote protocol —
|
|
// the LAN server built into the IC-7610 that the Icom "Remote Utility" (and
|
|
// wfview) talk to. OpsLog reimplements this directly so it can BE both the
|
|
// Remote Utility (Ethernet ↔ radio) and the logger/CAT client, dropping the
|
|
// virtual-COM + RS-BA1 chain entirely.
|
|
//
|
|
// This probe drives TWO streams and hex-dumps everything:
|
|
// Control (UDP 50001): handshake → login → token [VERIFIED on the real rig]
|
|
// CI-V (UDP 50002): handshake → openClose(open) → send CI-V read-freq
|
|
// (FE FE 98 E0 03 FD) → print the rig's reply.
|
|
// Framing (passcode table, packet offsets, CI-V data_packet, openclose) is
|
|
// reimplemented from the public wfview protocol and verified byte-for-byte
|
|
// against real Remote-Utility captures (build/bin/civ*.pcapng). No GPLv3 code.
|
|
//
|
|
// Usage:
|
|
// go run ./cmd/icomnettest <rig-ip> <user> <pass> [compname]
|
|
//
|
|
// SAFE: read-only CI-V (operating frequency). No TX, no writes.
|
|
package main
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
var le = binary.LittleEndian
|
|
var be = binary.BigEndian
|
|
|
|
// passcodeSeq — Icom's obfuscation table (values live at index 0x20..0x7e).
|
|
// VERIFIED: user "f6bgc" → 3F 65 50 25 55 (matches the capture).
|
|
var passcodeSeq = [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 passcode(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, passcodeSeq[p])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// --- packet builders (offsets verified vs wfview structs + real captures) ---
|
|
|
|
func ctrlPacket(typ, seq uint16, sentid, rcvdid uint32) []byte {
|
|
b := make([]byte, 16)
|
|
le.PutUint32(b[0:], 0x10)
|
|
le.PutUint16(b[4:], typ)
|
|
le.PutUint16(b[6:], seq)
|
|
le.PutUint32(b[8:], sentid)
|
|
le.PutUint32(b[12:], rcvdid)
|
|
return b
|
|
}
|
|
|
|
func buildLogin(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, user, pass, name string) []byte {
|
|
b := make([]byte, 0x80)
|
|
le.PutUint32(b[0:], 0x80)
|
|
le.PutUint16(b[6:], seq)
|
|
le.PutUint32(b[8:], sentid)
|
|
le.PutUint32(b[12:], rcvdid)
|
|
be.PutUint32(b[0x10:], 0x80-0x10)
|
|
b[0x14] = 0x01 // requestreply
|
|
b[0x15] = 0x00 // requesttype = login
|
|
be.PutUint16(b[0x16:], innerSeq)
|
|
le.PutUint16(b[0x1a:], tokReq)
|
|
le.PutUint32(b[0x1c:], token)
|
|
copy(b[0x40:0x50], passcode(user))
|
|
copy(b[0x50:0x60], passcode(pass))
|
|
nm := name
|
|
if len(nm) > 16 {
|
|
nm = nm[:16]
|
|
}
|
|
copy(b[0x60:0x70], []byte(nm))
|
|
return b
|
|
}
|
|
|
|
func buildToken(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32) []byte {
|
|
b := make([]byte, 0x40)
|
|
le.PutUint32(b[0:], 0x40)
|
|
le.PutUint16(b[6:], seq)
|
|
le.PutUint32(b[8:], sentid)
|
|
le.PutUint32(b[12:], rcvdid)
|
|
be.PutUint32(b[0x10:], 0x40-0x10)
|
|
b[0x14] = 0x01 // requestreply
|
|
b[0x15] = 0x02 // requesttype = token
|
|
be.PutUint16(b[0x16:], innerSeq)
|
|
le.PutUint16(b[0x1a:], tokReq)
|
|
le.PutUint32(b[0x1c:], token)
|
|
return b
|
|
}
|
|
|
|
// buildConnInfo — 144-byte sendRequestStream on the CONTROL stream. Tells the
|
|
// rig to route the CI-V/audio streams to the authenticated session and which
|
|
// local ports we use. Values verified byte-for-byte vs a real Remote-Utility
|
|
// capture (civ4): requesttype=0x03, commoncap=0x8010, the rig's MAC echoed,
|
|
// name "IC-7610", scrambled username, rxenable=0 (audio off — CI-V only),
|
|
// rxcodec 0x10 / txcodec 0x04, rxsample 16000 / txsample 8000 (BE), civport /
|
|
// audioport (BE), txbuffer 100.
|
|
func buildConnInfo(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, user string, rigMAC []byte, civPort, audioPort uint16) []byte {
|
|
b := make([]byte, 0x90)
|
|
le.PutUint32(b[0:], 0x90)
|
|
le.PutUint16(b[6:], seq)
|
|
le.PutUint32(b[8:], sentid)
|
|
le.PutUint32(b[12:], rcvdid)
|
|
be.PutUint32(b[0x10:], 0x90-0x10)
|
|
b[0x14] = 0x01 // requestreply
|
|
b[0x15] = 0x03 // requesttype = conninfo / open streams
|
|
be.PutUint16(b[0x16:], innerSeq)
|
|
le.PutUint16(b[0x1a:], tokReq)
|
|
le.PutUint32(b[0x1c:], token)
|
|
le.PutUint16(b[0x27:], 0x8010) // commoncap
|
|
copy(b[0x2a:0x30], rigMAC) // macaddress (the rig's, echoed back)
|
|
copy(b[0x40:0x60], []byte("IC-7610"))
|
|
copy(b[0x60:0x70], passcode(user))
|
|
b[0x70] = 0x00 // rxenable (0 = audio off)
|
|
b[0x71] = 0x00 // txenable
|
|
b[0x72] = 0x10 // rxcodec
|
|
b[0x73] = 0x04 // txcodec
|
|
be.PutUint32(b[0x74:], 16000) // rxsample
|
|
be.PutUint32(b[0x78:], 8000) // txsample
|
|
be.PutUint32(b[0x7c:], uint32(civPort))
|
|
be.PutUint32(b[0x80:], uint32(audioPort))
|
|
be.PutUint32(b[0x84:], 100) // txbuffer
|
|
b[0x88] = 0x00 // convert
|
|
return b
|
|
}
|
|
|
|
// buildOpenClose — 22-byte start/stop for the CI-V stream. magic 0x04=open,
|
|
// 0x00=close. data=0x01c0 (@0x10), civSeq (BE @0x13), magic (@0x15).
|
|
func buildOpenClose(seq uint16, sentid, rcvdid uint32, civSeq uint16, magic byte) []byte {
|
|
b := make([]byte, 0x16)
|
|
le.PutUint32(b[0:], 0x16)
|
|
le.PutUint16(b[6:], seq)
|
|
le.PutUint32(b[8:], sentid)
|
|
le.PutUint32(b[12:], rcvdid)
|
|
le.PutUint16(b[0x10:], 0x01c0)
|
|
be.PutUint16(b[0x13:], civSeq)
|
|
b[0x15] = magic
|
|
return b
|
|
}
|
|
|
|
// buildCivData — wraps raw CI-V bytes: 21-byte header (reply 0xc1 @0x10,
|
|
// datalen LE @0x11, civSeq BE @0x13) + CI-V frame @0x15.
|
|
func buildCivData(seq uint16, sentid, rcvdid uint32, civSeq uint16, civ []byte) []byte {
|
|
n := 0x15 + len(civ)
|
|
b := make([]byte, n)
|
|
le.PutUint32(b[0:], uint32(n))
|
|
le.PutUint16(b[6:], seq)
|
|
le.PutUint32(b[8:], sentid)
|
|
le.PutUint32(b[12:], rcvdid)
|
|
b[0x10] = 0xc1
|
|
le.PutUint16(b[0x11:], uint16(len(civ)))
|
|
be.PutUint16(b[0x13:], civSeq)
|
|
copy(b[0x15:], civ)
|
|
return b
|
|
}
|
|
|
|
func header(b []byte) (length uint32, typ, seq uint16, sentid, rcvdid uint32, ok bool) {
|
|
if len(b) < 16 {
|
|
return 0, 0, 0, 0, 0, false
|
|
}
|
|
return le.Uint32(b[0:]), le.Uint16(b[4:]), le.Uint16(b[6:]), le.Uint32(b[8:]), le.Uint32(b[12:]), true
|
|
}
|
|
|
|
func localID(conn net.Conn) uint32 {
|
|
a := conn.LocalAddr().(*net.UDPAddr)
|
|
return uint32(a.IP.To4()[0])<<24 | uint32(a.IP.To4()[1])<<16 | uint32(uint16(a.Port))
|
|
}
|
|
|
|
func recv(conn net.Conn, ms int, buf []byte) ([]byte, bool) {
|
|
_ = conn.SetReadDeadline(time.Now().Add(time.Duration(ms) * time.Millisecond))
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
return append([]byte(nil), buf[:n]...), true
|
|
}
|
|
|
|
func dump(tag string, p []byte) { fmt.Printf("%s (%d)\n%s\n", tag, len(p), hex.Dump(p)) }
|
|
|
|
// pingReply mirrors a ping, swaps ids, sets the reply flag at offset 16.
|
|
func pingReply(pkt []byte, myID, remoteID uint32) []byte {
|
|
r := append([]byte(nil), pkt...)
|
|
if len(r) >= 17 {
|
|
le.PutUint32(r[8:], myID)
|
|
le.PutUint32(r[12:], remoteID)
|
|
r[16] = 0x01
|
|
}
|
|
return r
|
|
}
|
|
|
|
// handshake: areYouThere(seq0) → iAmHere → areYouReady(seq1) → iAmReady.
|
|
// Returns the rig's remote id. Replies to any pings meanwhile.
|
|
func handshake(conn net.Conn, myID uint32, label string) (uint32, bool) {
|
|
buf := make([]byte, 2048)
|
|
conn.Write(ctrlPacket(0x03, 0, myID, 0)) // areYouThere
|
|
fmt.Printf("[%s] TX areYouThere\n", label)
|
|
var remoteID uint32
|
|
deadline := time.Now().Add(4 * time.Second)
|
|
lastTry := time.Now()
|
|
for time.Now().Before(deadline) {
|
|
p, ok := recv(conn, 200, buf)
|
|
if !ok {
|
|
if remoteID == 0 && time.Since(lastTry) > 500*time.Millisecond {
|
|
conn.Write(ctrlPacket(0x03, 0, myID, 0))
|
|
lastTry = time.Now()
|
|
}
|
|
continue
|
|
}
|
|
_, typ, _, sentid, _, ok := header(p)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch typ {
|
|
case 0x04: // iAmHere
|
|
remoteID = sentid
|
|
fmt.Printf("[%s] iAmHere remoteID=0x%08X → TX areYouReady\n", label, remoteID)
|
|
conn.Write(ctrlPacket(0x06, 1, myID, remoteID))
|
|
case 0x06: // iAmReady
|
|
if remoteID != 0 {
|
|
fmt.Printf("[%s] iAmReady — link up ✓\n", label)
|
|
return remoteID, true
|
|
}
|
|
case 0x07: // ping
|
|
conn.Write(pingReply(p, myID, remoteID))
|
|
}
|
|
}
|
|
return remoteID, false
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) < 4 {
|
|
fmt.Println("usage: icomnettest <rig-ip> <user> <pass> [compname]")
|
|
os.Exit(2)
|
|
}
|
|
ip, user, pass := os.Args[1], os.Args[2], os.Args[3]
|
|
compName := "OpsLog"
|
|
if len(os.Args) >= 5 {
|
|
compName = os.Args[4]
|
|
}
|
|
|
|
// ===================== CONTROL STREAM (50001) =====================
|
|
ctrl, err := net.Dial("udp4", net.JoinHostPort(ip, "50001"))
|
|
if err != nil {
|
|
fmt.Printf("dial control: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer ctrl.Close()
|
|
cID := localID(ctrl)
|
|
fmt.Printf("=== CONTROL 50001 (myID=0x%08X) ===\n", cID)
|
|
fmt.Printf("scrambled user=% X pass=% X\n\n", passcode(user), passcode(pass))
|
|
|
|
cRemote, ok := handshake(ctrl, cID, "ctrl")
|
|
if !ok {
|
|
fmt.Println("control handshake failed")
|
|
return
|
|
}
|
|
|
|
// login → token
|
|
var cTracked uint16 = 1
|
|
var cInner uint16 = 1
|
|
tokReq := uint16(0x0c77)
|
|
dump("[ctrl] TX login", buildLogin(cTracked, cInner, tokReq, cID, cRemote, 0, user, pass, compName))
|
|
ctrl.Write(buildLogin(cTracked, cInner, tokReq, cID, cRemote, 0, user, pass, compName))
|
|
cTracked++
|
|
cInner++
|
|
|
|
var token uint32
|
|
buf := make([]byte, 2048)
|
|
deadline := time.Now().Add(4 * time.Second)
|
|
for token == 0 && time.Now().Before(deadline) {
|
|
p, ok := recv(ctrl, 200, buf)
|
|
if !ok {
|
|
continue
|
|
}
|
|
length, typ, _, _, _, _ := header(p)
|
|
if typ == 0x00 && length == 0x60 && len(p) >= 0x34 { // login response
|
|
token = le.Uint32(p[0x1c:])
|
|
errCode := le.Uint32(p[0x30:])
|
|
if errCode != 0 || token == 0 {
|
|
fmt.Printf(">> LOGIN REJECTED err=0x%08X token=0x%08X\n", errCode, token)
|
|
return
|
|
}
|
|
fmt.Printf(">> LOGIN OK ✓ token=0x%08X\n", token)
|
|
ctrl.Write(buildToken(cTracked, cInner, tokReq, cID, cRemote, token))
|
|
cTracked++
|
|
cInner++
|
|
} else if typ == 0x07 {
|
|
ctrl.Write(pingReply(p, cID, cRemote))
|
|
}
|
|
}
|
|
if token == 0 {
|
|
fmt.Println("no token — login not accepted")
|
|
return
|
|
}
|
|
|
|
// Send conninfo on the control stream — routes the CI-V stream to this
|
|
// authenticated session and announces our civ/audio local ports (50002/3).
|
|
rigMAC := []byte{0x00, 0x90, 0xc7, 0x09, 0xba, 0x3f} // F6BGC's IC-7610 (from the caps packet)
|
|
dump("[ctrl] TX conninfo", buildConnInfo(cTracked, cInner, tokReq, cID, cRemote, token, user, rigMAC, 50002, 50003))
|
|
ctrl.Write(buildConnInfo(cTracked, cInner, tokReq, cID, cRemote, token, user, rigMAC, 50002, 50003))
|
|
cTracked++
|
|
cInner++
|
|
// Let the rig's caps/conninfo replies flow for ~600ms (reply to pings).
|
|
drainEnd := time.Now().Add(600 * time.Millisecond)
|
|
for time.Now().Before(drainEnd) {
|
|
if p, ok := recv(ctrl, 100, buf); ok {
|
|
if _, typ, _, _, _, _ := header(p); typ == 0x07 {
|
|
ctrl.Write(pingReply(p, cID, cRemote))
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===================== CI-V STREAM (50002) =====================
|
|
// Bind our civ socket to LOCAL port 50002 (= the civport announced above),
|
|
// as the Remote Utility does. Requires the Remote Utility to be CLOSED.
|
|
civ, err := net.DialUDP("udp4", &net.UDPAddr{Port: 50002}, &net.UDPAddr{IP: net.ParseIP(ip), Port: 50002})
|
|
if err != nil {
|
|
fmt.Printf("dial civ (local :50002 — is the Remote Utility still running?): %v\n", err)
|
|
return
|
|
}
|
|
defer civ.Close()
|
|
vID := localID(civ)
|
|
fmt.Printf("\n=== CI-V 50002 (myID=0x%08X) ===\n", vID)
|
|
vRemote, ok := handshake(civ, vID, "civ")
|
|
if !ok {
|
|
fmt.Println("CI-V handshake failed (may need the conninfo packet on control first)")
|
|
return
|
|
}
|
|
|
|
var vTracked uint16 = 1 // outer tracked seq @0x06
|
|
var vCivSeq uint16 = 1 // inner CI-V seq @0x13 (BE)
|
|
// openClose(open) starts CI-V data flow.
|
|
dump("[civ] TX openClose(open)", buildOpenClose(vTracked, vID, vRemote, vCivSeq, 0x04))
|
|
civ.Write(buildOpenClose(vTracked, vID, vRemote, vCivSeq, 0x04))
|
|
vTracked++
|
|
vCivSeq++
|
|
|
|
// Try several read commands, spaced out. Some rigs NG the basic 0x03 read
|
|
// over the network tunnel; 0x25 / 0x04 and unsolicited transceive frames
|
|
// (sent when you turn the VFO) still work. The tunnel itself is proven, so
|
|
// this figures out which read the rig actually answers.
|
|
sendCiv := func(name string, f []byte) {
|
|
fmt.Printf("[civ] TX %s\n", name)
|
|
civ.Write(buildCivData(vTracked, vID, vRemote, vCivSeq, f))
|
|
vTracked++
|
|
vCivSeq++
|
|
}
|
|
// The rig is in STANDBY (network up, radio off) — it NG's every command
|
|
// until powered on via CI-V. Send power-on (0x18 0x01, with an FE wake
|
|
// preamble, as the Remote Utility does), then poll read-freq while it boots.
|
|
powerOn := make([]byte, 0, 32)
|
|
for i := 0; i < 25; i++ {
|
|
powerOn = append(powerOn, 0xFE)
|
|
}
|
|
powerOn = append(powerOn, 0xFE, 0xFE, 0x98, 0xE0, 0x18, 0x01, 0xFD)
|
|
time.Sleep(300 * time.Millisecond)
|
|
sendCiv("POWER ON (0x18 01)", powerOn)
|
|
fmt.Print("\n>>> rig booting (~10-15 s) — polling read-freq until it answers <<<\n\n")
|
|
readFreq := []byte{0xFE, 0xFE, 0x98, 0xE0, 0x03, 0xFD}
|
|
|
|
cbuf := make([]byte, 4096)
|
|
vbuf := make([]byte, 4096)
|
|
end := time.Now().Add(30 * time.Second)
|
|
lastIdleC, lastIdleV, lastCmd := time.Now(), time.Now(), time.Now()
|
|
for time.Now().Before(end) {
|
|
if p, ok := recv(ctrl, 40, cbuf); ok {
|
|
if _, typ, _, _, _, _ := header(p); typ == 0x07 {
|
|
ctrl.Write(pingReply(p, cID, cRemote))
|
|
}
|
|
} else if time.Since(lastIdleC) > 200*time.Millisecond {
|
|
ctrl.Write(ctrlPacket(0x00, 0, cID, cRemote))
|
|
lastIdleC = time.Now()
|
|
}
|
|
if p, ok := recv(civ, 40, vbuf); ok {
|
|
_, typ, _, _, _, _ := header(p)
|
|
if typ == 0x07 {
|
|
civ.Write(pingReply(p, vID, vRemote))
|
|
} else if typ == 0x00 && len(p) > 0x15 && p[0x10] == 0xc1 {
|
|
f := p[0x15:]
|
|
if d := decodeCiv(f); d != "" {
|
|
fmt.Printf(">> CI-V RX: % X %s\n", f, d)
|
|
}
|
|
}
|
|
} else if time.Since(lastIdleV) > 200*time.Millisecond {
|
|
civ.Write(ctrlPacket(0x00, 0, vID, vRemote))
|
|
lastIdleV = time.Now()
|
|
}
|
|
if time.Since(lastCmd) > 1000*time.Millisecond {
|
|
sendCiv("read-freq 0x03", readFreq)
|
|
lastCmd = time.Now()
|
|
}
|
|
}
|
|
|
|
// Clean close.
|
|
civ.Write(buildOpenClose(vTracked, vID, vRemote, vCivSeq, 0x00)) // openClose(close)
|
|
civ.Write(ctrlPacket(0x05, 0, vID, vRemote)) // disconnect
|
|
ctrl.Write(ctrlPacket(0x05, 0, cID, cRemote))
|
|
fmt.Println("\nDone. Look for '>> CI-V RX:' and 'FREQUENCY reply'.")
|
|
}
|
|
|
|
// decodeCiv describes a received CI-V frame (FE FE <to> <from> <cmd> … FD).
|
|
// Only frames FROM the rig (from=0x98) are interesting; our own echoed commands
|
|
// (from=0xE0) return "" so they're not printed.
|
|
func decodeCiv(f []byte) string {
|
|
if len(f) < 6 || f[0] != 0xFE || f[1] != 0xFE {
|
|
return ""
|
|
}
|
|
if f[3] != 0x98 { // not from the rig (our echoed command) — skip
|
|
return ""
|
|
}
|
|
cmd := f[4]
|
|
body := f[5 : len(f)-1] // between cmd and the trailing FD
|
|
switch cmd {
|
|
case 0xFA:
|
|
return "NG (command rejected)"
|
|
case 0xFB:
|
|
return "OK (ack)"
|
|
case 0x00, 0x03, 0x05: // (transceive) freq / read-freq
|
|
if len(body) >= 5 {
|
|
return "FREQ " + decodeFreq(body[:5])
|
|
}
|
|
case 0x25: // read/set VFO freq (body = subcmd + 5 BCD)
|
|
if len(body) >= 6 {
|
|
return fmt.Sprintf("VFO%d FREQ %s", body[0], decodeFreq(body[1:6]))
|
|
}
|
|
case 0x01, 0x04: // (transceive) mode / read-mode
|
|
if len(body) >= 1 {
|
|
return fmt.Sprintf("MODE 0x%02X filt 0x%02X", body[0], lastOr(body, 1))
|
|
}
|
|
}
|
|
return fmt.Sprintf("cmd 0x%02X", cmd)
|
|
}
|
|
|
|
func lastOr(b []byte, i int) byte {
|
|
if i < len(b) {
|
|
return b[i]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// decodeFreq turns Icom little-endian BCD (5 bytes) into a MHz string.
|
|
func decodeFreq(bcd []byte) string {
|
|
var hz uint64
|
|
mul := uint64(1)
|
|
for _, b := range bcd {
|
|
hz += uint64(b&0x0f) * mul
|
|
mul *= 10
|
|
hz += uint64(b>>4) * mul
|
|
mul *= 10
|
|
}
|
|
return fmt.Sprintf("%.6f MHz", float64(hz)/1e6)
|
|
}
|