Files

243 lines
6.3 KiB
Go

// Package civ implements the Icom CI-V protocol independently of the transport
// carrying it. The exact same frames travel over a USB/serial port (local
// control) and, wrapped in Icom's UDP "serial" stream, over the network
// (remote control). Keeping the wire format in one place means the USB backend
// (icomserial) and a future network backend (icomnet) share all of it — only
// the transport differs.
//
// Frame layout: FE FE <to> <from> <cmd> [sub] [data…] FD
package civ
import (
"bytes"
"fmt"
)
// Protocol bytes.
const (
Pre = 0xFE // preamble (sent twice at the start of every frame)
End = 0xFD // end-of-message
OK = 0xFB // rig acknowledged a set command
NG = 0xFA // rig rejected a set command
// AddrController is the conventional address software uses for itself.
AddrController = 0xE0
)
// Commands (the few Phase-1 control needs; more get added with the panel).
const (
CmdTransceiveFreq = 0x00 // unsolicited freq update (dial turned)
CmdTransceiveMode = 0x01 // unsolicited mode update
CmdReadFreq = 0x03
CmdReadMode = 0x04
CmdSetFreq = 0x05
CmdSetMode = 0x06
CmdPTT = 0x1C // sub 0x00 = PTT
CmdExtra = 0x1A // sub 0x06 = data mode on modern Icoms
CmdReadID = 0x19 // sub 0x00 = rig's own CI-V address (identifies model)
CmdAtt = 0x11 // attenuator (1 BCD byte of dB; 0x00 = off)
CmdLevel = 0x14 // analogue levels (sub + 2 BCD bytes, 0000-0255)
CmdSwitch = 0x16 // on/off + multi-state DSP settings (sub + 1 byte)
SubDataMode = 0x06
SubPTT = 0x00
// CmdLevel sub-commands.
SubLevelAF = 0x01 // AF (volume)
SubLevelRF = 0x02 // RF gain
SubLevelNR = 0x06 // noise-reduction depth
SubLevelNB = 0x12 // noise-blanker depth
// CmdSwitch sub-commands.
SubSwPreamp = 0x02 // 0=off, 1=P.AMP1, 2=P.AMP2
SubSwAGC = 0x12 // 1=FAST, 2=MID, 3=SLOW
SubSwNB = 0x22 // noise blanker on/off
SubSwNR = 0x40 // noise reduction on/off
SubSwANF = 0x41 // auto-notch on/off
)
// Icom mode codes (used by CmdReadMode / CmdSetMode).
const (
ModeLSB = 0x00
ModeUSB = 0x01
ModeAM = 0x02
ModeCW = 0x03
ModeRTTY = 0x04
ModeFM = 0x05
ModeCWR = 0x07
ModeRTTYR = 0x08
)
// Frame builds a complete CI-V frame (preamble … end) for payload, which is the
// command byte followed by any sub-command/data bytes.
func Frame(to, from byte, payload ...byte) []byte {
f := make([]byte, 0, len(payload)+5)
f = append(f, Pre, Pre, to, from)
f = append(f, payload...)
f = append(f, End)
return f
}
// FreqToBCD encodes a frequency in Hz as the 5 little-endian BCD bytes Icom
// expects (10 digits, 2 per byte, least-significant byte first).
func FreqToBCD(hz int64) []byte {
if hz < 0 {
hz = 0
}
b := make([]byte, 5)
for i := 0; i < 5; i++ {
lo := hz % 10
hz /= 10
hi := hz % 10
hz /= 10
b[i] = byte(lo) | byte(hi)<<4
}
return b
}
// BCDToFreq decodes Icom little-endian BCD frequency bytes back to Hz.
func BCDToFreq(b []byte) int64 {
var hz int64
mult := int64(1)
for i := 0; i < len(b) && i < 5; i++ {
hz += int64(b[i]&0x0F) * mult
mult *= 10
hz += int64(b[i]>>4) * mult
mult *= 10
}
return hz
}
// LevelToBCD encodes a 0-255 level as the 2 big-endian BCD bytes Icom's
// CmdLevel commands use (e.g. 128 → 0x01 0x28, 255 → 0x02 0x55).
func LevelToBCD(v int) []byte {
if v < 0 {
v = 0
}
if v > 255 {
v = 255
}
return []byte{byte(v / 100), byte(((v/10)%10)<<4 | v%10)}
}
// BCDToLevel decodes the 2 BCD bytes of a CmdLevel response back to 0-255.
func BCDToLevel(b []byte) int {
if len(b) < 2 {
return 0
}
return int(b[0])*100 + int(b[1]>>4)*10 + int(b[1]&0x0F)
}
// ByteToBCD / BCDToByte handle a single packed-BCD byte (used by the
// attenuator, where the value is dB: 0x00, 0x06, 0x12, 0x18…).
func ByteToBCD(v int) byte {
if v < 0 {
v = 0
}
if v > 99 {
v = 99
}
return byte((v/10)<<4 | v%10)
}
func BCDToByte(b byte) int { return int(b>>4)*10 + int(b&0x0F) }
// ModeToADIF maps an Icom mode byte (plus the data-mode flag) to an ADIF mode
// string. Data mode on USB/LSB is surfaced as "DATA" so the app can substitute
// the user's preferred digital mode (FT8/RTTY/…), matching the OmniRig backend.
func ModeToADIF(m byte, data bool) string {
switch m {
case ModeCW, ModeCWR:
return "CW"
case ModeRTTY, ModeRTTYR:
return "RTTY"
case ModeAM:
return "AM"
case ModeFM:
return "FM"
case ModeLSB, ModeUSB:
if data {
return "DATA"
}
return "SSB"
}
return ""
}
// ModelName maps a rig's default CI-V address (from CmdReadID) to a readable
// model. Unknown addresses fall back to a hex label.
func ModelName(addr byte) string {
switch addr {
case 0x94:
return "IC-7300"
case 0x98:
return "IC-7610"
case 0xA2:
return "IC-9700"
case 0xA4:
return "IC-705"
case 0x88:
return "IC-7700"
case 0x80:
return "IC-7800"
}
return fmt.Sprintf("Icom (0x%02X)", addr)
}
// Decoded is one parsed CI-V frame. Data is everything after the command byte
// (so it still carries the sub-command for multi-byte commands like 1A 06).
type Decoded struct {
To byte
From byte
Cmd byte
Data []byte
}
// Scan extracts every complete frame from buf and reports how many leading
// bytes the caller may now discard. A trailing partial frame (or a lone
// preamble byte) is left unconsumed so it can be completed by the next read.
func Scan(buf []byte) (frames []Decoded, consumed int) {
pos := 0
for {
p := indexPreamble(buf, pos)
if p < 0 {
// No further preamble. Keep a trailing FE (possible start of the
// next preamble); otherwise everything seen is consumable.
if len(buf) > 0 && buf[len(buf)-1] == Pre {
return frames, len(buf) - 1
}
return frames, len(buf)
}
start := p + 2
for start < len(buf) && buf[start] == Pre { // tolerate padding FEs
start++
}
end := bytes.IndexByte(buf[start:], End)
if end < 0 {
return frames, p // incomplete frame — keep from its preamble
}
end += start
if body := buf[start:end]; len(body) >= 3 {
frames = append(frames, Decoded{
To: body[0],
From: body[1],
Cmd: body[2],
Data: append([]byte(nil), body[3:]...),
})
}
pos = end + 1
consumed = pos
}
}
// indexPreamble returns the index of the next FE FE pair at or after from.
func indexPreamble(buf []byte, from int) int {
for i := from; i+1 < len(buf); i++ {
if buf[i] == Pre && buf[i+1] == Pre {
return i
}
}
return -1
}