package main import ( "bytes" "fmt" "io" "net" "strconv" "strings" ) const ( rigctldNoError = iota rigctldInvalidParam = -1 rigctldUnsupportedCmd = -11 ) type rigctldStruct struct { listener net.Listener client net.Conn clientLoopDeinitNeededChan chan bool clientLoopDeinitFinishedChan chan bool deinitNeededChan chan bool deinitFinishedChan chan bool } var rigctld rigctldStruct func (s *rigctldStruct) disconnectClient() { if s.client != nil { s.client.Close() } } func (s *rigctldStruct) deinitClient() { if s.clientLoopDeinitNeededChan != nil { s.clientLoopDeinitNeededChan <- true <-s.clientLoopDeinitFinishedChan s.clientLoopDeinitNeededChan = nil s.clientLoopDeinitFinishedChan = nil } } func (s *rigctldStruct) send(a ...interface{}) error { str := fmt.Sprint(a...) _, err := s.client.Write([]byte(str)) return err } func (s *rigctldStruct) sendReplyCode(code int) error { str := fmt.Sprint("RPRT ", code, "\n") _, err := s.client.Write([]byte(str)) return err } func (s *rigctldStruct) processCmd(cmd string) (close bool, err error) { cmdSplit := strings.Fields(cmd) switch { case cmd == "\\chk_vfo": err = s.send("0\n") case cmd == "\\dump_state": err = s.send("1\n" + "3085\n" + "0\n" + "30000.000000 199999999.000000 0x1401dbf -1 -1 0x10000003 0x1\n" + "400000000.000000 470000000.000000 0x1401dbf -1 -1 0x10000003 0x1\n" + "0 0 0 0 0 0 0\n" + "1800000.000000 1999999.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "3500000.000000 3999999.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "5255000.000000 5405000.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "7000000.000000 7300000.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "10100000.000000 10150000.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "14000000.000000 14350000.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "18068000.000000 18168000.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "21000000.000000 21450000.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "24890000.000000 24990000.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "28000000.000000 29700000.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "50000000.000000 54000000.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "144000000.000000 148000000.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "430000000.000000 450000000.000000 0x10001bf 100 10000 0x10000003 0x1\n" + "0 0 0 0 0 0 0\n" + "0x401dbf 100\n" + "0x401dbf 500\n" + "0x401dbf 1000\n" + "0x401dbf 5000\n" + "0x401dbf 6250\n" + "0x401dbf 8330\n" + "0x401dbf 9000\n" + "0x401dbf 10000\n" + "0x401dbf 12500\n" + "0x401dbf 20000\n" + "0x401dbf 25000\n" + "0x401dbf 50000\n" + "0x401dbf 100000\n" + "0 0\n" + "0xc0c 3600\n" + "0xc0c 2400\n" + "0xc0c 1800\n" + "0x192 500\n" + "0x192 250\n" + "0x82 1200\n" + "0x110 2400\n" + "0x400001 6000\n" + "0x400001 3000\n" + "0x400001 9000\n" + "0x1020 10000\n" + "0x1020 7000\n" + "0x1020 15000\n" + "0 0\n" + "9999\n" + "9999\n" + "0\n" + "0\n" + "1 2\n" + "20\n" + "0xc90133fe\n" + "0xc90133fe\n" + "0x7f74677f3f\n" + "0x7000677f3f\n" + "0x35\n" + "0x35\n" + "vfo_ops=0x81f\n" + "ptt_type=0x1\n" + "targetable_vfo=0x0\n" + "done\n") case cmd == "q": err = s.sendReplyCode(rigctldNoError) close = true case cmd == "f", cmd == "\\get_freq": civControl.state.mutex.Lock() defer civControl.state.mutex.Unlock() err = s.send(civControl.state.freq, "\n") case cmdSplit[0] == "F", cmdSplit[0] == "\\set_freq": var f float64 f, err = strconv.ParseFloat(cmdSplit[1], 0) if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) return } err = civControl.setMainVFOFreq(uint(f)) if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) return } err = s.sendReplyCode(rigctldNoError) case cmd == "m", cmd == "\\get_mode": civControl.state.mutex.Lock() defer civControl.state.mutex.Unlock() var mode string if civControl.state.dataMode { mode = "PKT" } mode += civOperatingModes[civControl.state.operatingModeIdx].name // This can be queried with a CIV command for accurate values by the way. width := "3000" switch civControl.state.filterIdx { case 1: width = "2400" case 2: width = "1800" } err = s.send(mode, "\n", width, "\n") case cmdSplit[0] == "M", cmdSplit[0] == "\\set_mode": mode := cmdSplit[1] var dataMode bool if mode[:3] == "PKT" { dataMode = true mode = mode[3:] } var modeCode byte var modeFound bool for _, m := range civOperatingModes { if m.name == mode { modeCode = m.code modeFound = true break } } if !modeFound { err = fmt.Errorf("unknown mode %s", mode) _ = s.sendReplyCode(rigctldInvalidParam) return } var width int width, err = strconv.Atoi(cmdSplit[2]) if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) return } var filterCode byte if width <= 1800 { filterCode = 2 } else if width <= 2400 { filterCode = 1 } err = civControl.setOperatingModeAndFilter(modeCode, filterCode) if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) } else { err = civControl.setDataMode(dataMode) if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) return } _ = s.sendReplyCode(rigctldNoError) } case cmd == "t", cmd == "\\get_ptt": civControl.state.mutex.Lock() defer civControl.state.mutex.Unlock() res := "0" if civControl.state.ptt { res = "1" } err = s.send(res, "\n") case cmdSplit[0] == "T", cmdSplit[0] == "\\set_ptt": if cmdSplit[1] != "0" { if setDataModeOnTx { if err := civControl.setDataMode(true); err != nil { log.Error("can't enable data mode: ", err) } } err = civControl.setPTT(true) } else { err = civControl.setPTT(false) } if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) } else { _ = s.sendReplyCode(rigctldNoError) } case cmdSplit[0] == "V", cmdSplit[0] == "\\set_vfo": if cmdSplit[1] == "VFOB" { err = civControl.setVFO(1) } else { err = civControl.setVFO(0) } if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) } else { _ = s.sendReplyCode(rigctldNoError) } case cmd == "s", cmd == "\\get_split_vfo": civControl.state.mutex.Lock() defer civControl.state.mutex.Unlock() res := "0" if civControl.state.splitMode == splitModeOn { res = "1" } err = s.send(res, "\n") if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) return } if civControl.state.vfoBActive { res = "VFOA" } else { res = "VFOB" } err = s.send(res, "\n") case cmdSplit[0] == "S", cmdSplit[0] == "\\set_split_vfo": if cmdSplit[1] == "1" { err = civControl.setSplit(splitModeOn) } else { err = civControl.setSplit(splitModeOff) } if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) } else { _ = s.sendReplyCode(rigctldNoError) } case cmd == "i", cmd == "\\get_split_freq": civControl.state.mutex.Lock() defer civControl.state.mutex.Unlock() err = s.send(civControl.state.subFreq, "\n") case cmdSplit[0] == "I", cmdSplit[0] == "\\set_split_freq": var f float64 f, err = strconv.ParseFloat(cmdSplit[1], 0) if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) return } err = civControl.setSubVFOFreq(uint(f)) if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) return } err = s.sendReplyCode(rigctldNoError) case cmd == "x", cmd == "\\get_split_mode": civControl.state.mutex.Lock() defer civControl.state.mutex.Unlock() var mode string if civControl.state.subDataMode { mode = "PKT" } mode += civOperatingModes[civControl.state.subOperatingModeIdx].name // This can be queried with a CIV command for accurate values by the way. width := "3000" switch civControl.state.subFilterIdx { case 1: width = "2400" case 2: width = "1800" } err = s.send(mode, "\n", width, "\n") case cmdSplit[0] == "X", cmdSplit[0] == "\\set_split_mode": mode := cmdSplit[1] var dataMode byte if mode[:3] == "PKT" { dataMode = 1 mode = mode[3:] } var modeCode byte var modeFound bool for _, m := range civOperatingModes { if m.name == mode { modeCode = m.code modeFound = true break } } if !modeFound { err = fmt.Errorf("unknown mode %s", mode) _ = s.sendReplyCode(rigctldInvalidParam) return } var width int width, err = strconv.Atoi(cmdSplit[2]) if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) return } var filterCode byte if width <= 1800 { filterCode = 2 } else if width <= 2400 { filterCode = 1 } err = civControl.setSubVFOMode(modeCode, dataMode, filterCode) if err != nil { _ = s.sendReplyCode(rigctldInvalidParam) } else { _ = s.sendReplyCode(rigctldNoError) } case cmd == "v": // Ignore this command. _ = s.sendReplyCode(rigctldUnsupportedCmd) return default: _ = s.sendReplyCode(rigctldUnsupportedCmd) return false, fmt.Errorf("got unknown cmd %s", cmd) } return } func (s *rigctldStruct) clientLoop() { defer func() { s.client.Close() log.Print("client ", s.client.RemoteAddr().String(), " disconnected") <-s.clientLoopDeinitNeededChan s.clientLoopDeinitFinishedChan <- true }() log.Print("client ", s.client.RemoteAddr().String(), " connected") var b [128]byte var lineBuf bytes.Buffer for { n, err := s.client.Read(b[:]) if err != nil { break } select { case <-s.clientLoopDeinitNeededChan: s.clientLoopDeinitFinishedChan <- true return default: } lineBuf.Write(b[:n]) endIndex := bytes.Index(lineBuf.Bytes(), []byte{'\n'}) if endIndex >= 0 { lineB := make([]byte, endIndex+1) n, err := lineBuf.Read(lineB) if err != nil { log.Error(err) return } if n < endIndex+1 { log.Error("short read") return } if n > 1 { close, err := s.processCmd(strings.TrimSpace(string(lineB[:len(lineB)-1]))) if err != nil { log.Error(err) } if close { return } } } } } func (s *rigctldStruct) loop() { for { newClient, err := s.listener.Accept() s.disconnectClient() s.deinitClient() s.clientLoopDeinitNeededChan = make(chan bool) s.clientLoopDeinitFinishedChan = make(chan bool) if err != nil { if err != io.EOF { reportError(err) } <-s.deinitNeededChan s.deinitFinishedChan <- true return } s.client = newClient go s.clientLoop() } } // We only init the serial port TCP server once, with the first device name we acquire, so apps using the // serial port TCP server won't have issues with the interface going down while the app is running. func (s *rigctldStruct) initIfNeeded() (err error) { if s.listener != nil { return } s.listener, err = net.Listen("tcp", fmt.Sprint(":", rigctldPort)) if err != nil { fmt.Println(err) return } log.Print("starting internal rigctld on tcp port ", rigctldPort) s.deinitNeededChan = make(chan bool) s.deinitFinishedChan = make(chan bool) go s.loop() return } func (s *rigctldStruct) deinit() { if s.listener != nil { s.listener.Close() } if s.deinitNeededChan != nil { s.deinitNeededChan <- true <-s.deinitFinishedChan } }