package kpa // Decoding the amplifier's answers. // // Every format here is quoted from the KPA1500 Programming Reference, with the // document's own example kept in the test next door. That is the whole // discipline: a meter decoded from a guess reports a good match on a bad // antenna, and nobody finds out until something is damaged. import ( "fmt" "strconv" "strings" ) // payload strips the leading "^", the command letters and the trailing ";", // leaving the value. Returns false when the answer is not for this command — // which happens on a shared serial line and on the first read after a // reconnect, where a stale reply is still in flight. func payload(reply, cmd string) (string, bool) { r := strings.TrimSpace(reply) r = strings.TrimSuffix(r, ";") r = strings.TrimPrefix(r, "^") cmd = strings.TrimSuffix(strings.TrimPrefix(cmd, "^"), ";") if !strings.HasPrefix(strings.ToUpper(r), strings.ToUpper(cmd)) { return "", false } return strings.TrimSpace(r[len(cmd):]), true } // parseWS reads forward power and SWR from one answer. // // ^WS1204 014; → 1204 W, SWR 1.4 // // The watts field is FOUR digits on a KPA1500 and THREE on a KPA500 — the // reference says so where it explains that ^WS exists for KPA500 compatibility // — so the split is on the space and not on a width. The SWR is in tenths, the // same units as everywhere else in this protocol. func parseWS(reply string) (watts int, swr float64, err error) { v, ok := payload(reply, "^WS") if !ok { return 0, 0, fmt.Errorf("not a ^WS answer: %q", reply) } f := strings.Fields(v) if len(f) != 2 { return 0, 0, fmt.Errorf("^WS wants two fields, got %q", v) } w, err1 := strconv.Atoi(f[0]) s, err2 := strconv.Atoi(f[1]) if err1 != nil || err2 != nil { return 0, 0, fmt.Errorf("^WS not numeric: %q", v) } return w, float64(s) / 10, nil } // parseVI reads the PA supply voltage and current. // // ^VI513 061; → 51.3 V, 61 A // // Volts in TENTHS, amps whole. Two different scales in one answer, which is // exactly the kind of detail that is wrong when it is assumed. func parseVI(reply string) (volts float64, amps int, err error) { v, ok := payload(reply, "^VI") if !ok { return 0, 0, fmt.Errorf("not a ^VI answer: %q", reply) } f := strings.Fields(v) if len(f) != 2 { return 0, 0, fmt.Errorf("^VI wants two fields, got %q", v) } dv, err1 := strconv.Atoi(f[0]) a, err2 := strconv.Atoi(f[1]) if err1 != nil || err2 != nil { return 0, 0, fmt.Errorf("^VI not numeric: %q", v) } return float64(dv) / 10, a, nil } // parseInt reads the plain numeric answers: ^TMxxx (°C), ^PCnnn (A), // ^BNbb (band number), ^OSx, ^ONx, ^TPx. func parseInt(reply, cmd string) (int, error) { v, ok := payload(reply, cmd) if !ok { return 0, fmt.Errorf("not a %s answer: %q", cmd, reply) } n, err := strconv.Atoi(strings.TrimSpace(v)) if err != nil { return 0, fmt.Errorf("%s not numeric: %q", cmd, v) } return n, nil } // parseFault reads ^FLhh — TWO HEX DIGITS, not decimal. Fault 90 is reflected // power and fault 91 is "antenna not connected"; read as decimal they would be // 144 and 145 and match nothing in the table. func parseFault(reply string) (int, error) { v, ok := payload(reply, "^FL") if !ok { return 0, fmt.Errorf("not a ^FL answer: %q", reply) } n, err := strconv.ParseInt(strings.TrimSpace(v), 16, 32) if err != nil { return 0, fmt.Errorf("^FL not hex: %q", v) } return int(n), nil } // faultNames is the table from the reference, keyed by the hex code. // // Said in the operator's terms rather than the amplifier's: "the antenna is not // connected" is a thing to go and fix, "fault 91" is a thing to go and look up. var faultNames = map[int]string{ 0x00: "no fault", 0x10: "watchdog timer reset", 0x20: "PA current too high", 0x40: "too hot — clears as it cools", 0x60: "drive power too high", 0x61: "gain too low for the drive", 0x70: "frequency outside a ham band", 0x80: "50 V supply out of range", 0x81: "5 V supply out of range", 0x82: "10 V supply out of range", 0x83: "12 V supply out of range", 0x84: "-12 V supply out of range", 0x85: "no LPF board supply detected", 0x90: "reflected power too high", 0x91: "SWR very high — antenna not connected?", 0x92: "the ATU found no match", 0xB0: "dissipated power too high", 0xC0: "forward power too high", 0xC1: "forward power too high for this ATU setting", 0xF0: "gain too high for the drive", } // FaultName describes a fault code, or says the code itself when the firmware // reports one this table does not know — a newer amplifier must not be able to // produce a blank explanation. func FaultName(code int) string { if code == 0 { return "" } if s, ok := faultNames[code]; ok { return s } return fmt.Sprintf("fault %02X", code) } // bandNames maps ^BN to the ADIF band. The numbering is the K3/K4 one, which is // why it is worth writing down: it is not frequency order beyond 6 m and there // is no arithmetic that produces it. var bandNames = map[int]string{ 0: "160m", 1: "80m", 2: "60m", 3: "40m", 4: "30m", 5: "20m", 6: "17m", 7: "15m", 8: "12m", 9: "10m", 10: "6m", } // BandName is the ADIF band for a ^BN number, or "" when unknown. func BandName(n int) string { return bandNames[n] }