From 2d1bbf76dcac5468ed6e49d03c04a181ae026ceb Mon Sep 17 00:00:00 2001 From: Nonoo Date: Sun, 8 Nov 2020 20:48:39 +0100 Subject: [PATCH] Add internal rigctld --- README.md | 9 +- args.go | 9 +- civcontrol.go | 52 +++++-- cmdrunner.go | 1 - controlstream.go | 10 +- main.go | 2 +- rigctld.go | 387 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 439 insertions(+), 31 deletions(-) create mode 100644 rigctld.go diff --git a/README.md b/README.md index 4799eb6..0dd170e 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,13 @@ After it is connected and logged in: used to record/play audio from/to the server (the transceiver). You can also set this sound card in [WSJT-X](https://physics.princeton.edu/pulsar/K1JT/wsjtx.html). - Starts a **TCP server** on port `4533` for exposing the **serial port**. -- Starts `rigctld` and connects it to kappanhang's TCP serial port server. - This can be used for controlling the server (the transceiver) with - [Hamlib](https://hamlib.github.io/) (`rigctld`). +- Starts an **internal rigctld** server. This can be used for controlling the + server (the transceiver) with [Hamlib](https://hamlib.github.io/) (`rigctl`) + clients. To use this with for example [WSJT-X](https://physics.princeton.edu/pulsar/K1JT/wsjtx.html), open WSJT-X settings, go to the *Radio* tab, set the *rig type* to `Hamlib - NET rigctl`, and the *Network server* to `localhost`. It is recommended to - set the *poll interval* to 10 seconds. + NET rigctl`, and the *Network server* to `localhost`. ### Virtual serial port diff --git a/args.go b/args.go index 4846c66..4e31989 100644 --- a/args.go +++ b/args.go @@ -14,8 +14,7 @@ var username string var password string var serialTCPPort uint16 var enableSerialDevice bool -var rigctldModel uint -var disableRigctld bool +var rigctldPort uint16 var runCmd string var runCmdOnSerialPortCreated string var statusLogInterval time.Duration @@ -28,8 +27,7 @@ func parseArgs() { p := getopt.StringLong("password", 'p', "beerbeer", "Password") t := getopt.Uint16Long("serial-tcp-port", 't', 4533, "Expose radio's serial port on this TCP port") s := getopt.BoolLong("enable-serial-device", 's', "Expose radio's serial port as a virtual serial port") - m := getopt.UintLong("rigctld-model", 'm', 3085, "rigctld model number") - r := getopt.BoolLong("disable-rigctld", 'r', "Disable starting rigctld") + r := getopt.Uint16Long("rigctld-port", 'r', 4532, "Use this TCP port for the internal rigctld") e := getopt.StringLong("exec", 'e', "", "Exec cmd when connected") o := getopt.StringLong("exec-serial", 'o', "socat /tmp/kappanhang-IC-705.pty /tmp/vmware.pty", "Exec cmd when virtual serial port is created, set to - to disable") i := getopt.Uint16Long("log-interval", 'i', 100, "Status bar/log interval in milliseconds") @@ -48,8 +46,7 @@ func parseArgs() { password = *p serialTCPPort = *t enableSerialDevice = *s - rigctldModel = *m - disableRigctld = *r + rigctldPort = *r runCmd = *e runCmdOnSerialPortCreated = *o statusLogInterval = time.Duration(*i) * time.Millisecond diff --git a/civcontrol.go b/civcontrol.go index 5eb3261..c386e1e 100644 --- a/civcontrol.go +++ b/civcontrol.go @@ -3,6 +3,7 @@ package main import ( "fmt" "math" + "sync" "time" ) @@ -109,6 +110,8 @@ type civControlStruct struct { setVFOSent bool setSplitSent bool + mutex sync.Mutex + freq uint ptt bool tune bool @@ -141,6 +144,9 @@ func (s *civControlStruct) decode(d []byte) bool { payload := d[5 : len(d)-1] + s.state.mutex.Lock() + defer s.state.mutex.Unlock() + switch d[4] { case 0x00: return s.decodeFreq(payload) @@ -799,20 +805,25 @@ func (s *civControlStruct) toggleTune() error { return s.st.send([]byte{254, 254, civAddress, 224, 0x1c, 1, b, 253}) } -func (s *civControlStruct) toggleDataMode() error { - s.state.setDataModeSent = true +func (s *civControlStruct) setDataMode(enable bool) error { var b byte var f byte - if !s.state.dataMode { + if enable { b = 1 f = 1 } else { b = 0 f = 0 } + return s.st.send([]byte{254, 254, civAddress, 224, 0x1a, 0x06, b, f, 253}) } +func (s *civControlStruct) toggleDataMode() error { + s.state.setDataModeSent = true + return s.setDataMode(!s.state.dataMode) +} + func (s *civControlStruct) incBand() error { s.state.bandChanging = true i := s.state.bandIdx + 1 @@ -888,29 +899,50 @@ func (s *civControlStruct) decTS() error { return s.st.send([]byte{254, 254, civAddress, 224, 0x10, b, 253}) } +func (s *civControlStruct) setVFO(nr byte) error { + s.state.setVFOSent = true + return s.st.send([]byte{254, 254, civAddress, 224, 0x07, nr, 253}) +} + func (s *civControlStruct) toggleVFO() error { s.state.setVFOSent = true var b byte if !s.state.vfoBActive { b = 1 } - return s.st.send([]byte{254, 254, civAddress, 224, 0x07, b, 253}) + return s.setVFO(b) +} + +func (s *civControlStruct) setSplit(mode splitMode) error { + s.state.setSplitSent = true + var b byte + switch mode { + default: + b = 0x00 + case splitModeOn: + b = 0x01 + case splitModeDUPMinus: + b = 0x11 + case splitModeDUPPlus: + b = 0x12 + } + return s.st.send([]byte{254, 254, civAddress, 224, 0x0f, b, 253}) } func (s *civControlStruct) toggleSplit() error { s.state.setSplitSent = true - var b byte + var mode splitMode switch s.state.splitMode { case splitModeOff: - b = 0x01 + mode = splitModeOn case splitModeOn: - b = 0x011 + mode = splitModeDUPMinus case splitModeDUPMinus: - b = 0x12 + mode = splitModeDUPPlus default: - b = 0x10 + mode = splitModeOff } - return s.st.send([]byte{254, 254, civAddress, 224, 0x0f, b, 253}) + return s.setSplit(mode) } func (s *civControlStruct) getFreq() error { diff --git a/cmdrunner.go b/cmdrunner.go index 76db522..e7164bc 100644 --- a/cmdrunner.go +++ b/cmdrunner.go @@ -17,7 +17,6 @@ type cmdRunner struct { var runCmdRunner cmdRunner var serialCmdRunner cmdRunner -var rigctldRunner cmdRunner func (c *cmdRunner) kill(cmd *exec.Cmd) { err := cmd.Process.Kill() diff --git a/controlstream.go b/controlstream.go index 8a6fbf5..1cc32f0 100644 --- a/controlstream.go +++ b/controlstream.go @@ -5,8 +5,6 @@ import ( "crypto/rand" "encoding/binary" "errors" - "fmt" - "os/exec" "time" ) @@ -273,12 +271,8 @@ func (s *controlStream) handleRead(r []byte) error { if enableSerialDevice { serialCmdRunner.startIfNeeded(runCmdOnSerialPortCreated) } - if !disableRigctld { - if _, err := exec.LookPath("rigctld"); err != nil { - log.Error("can't start rigctld: ", err) - } else { - rigctldRunner.startIfNeeded(fmt.Sprint("rigctld -m ", rigctldModel, " -r :", serialTCPPort)) - } + if err := rigctld.initIfNeeded(); err != nil { + return err } } } diff --git a/main.go b/main.go index 1f97dca..bff8073 100644 --- a/main.go +++ b/main.go @@ -150,10 +150,10 @@ exit: log.Print("restarting control stream...") } + rigctld.deinit() serialTCPSrv.deinit() runCmdRunner.stop() serialCmdRunner.stop() - rigctldRunner.stop() audio.deinit() serialPort.deinit() diff --git a/rigctld.go b/rigctld.go new file mode 100644 index 0000000..05ddf25 --- /dev/null +++ b/rigctld.go @@ -0,0 +1,387 @@ +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.Split(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": + civControl.state.mutex.Lock() + defer civControl.state.mutex.Unlock() + + err = s.send(civControl.state.freq, "\n") + case cmdSplit[0] == "F": + var f float64 + f, err = strconv.ParseFloat(cmdSplit[1], 0) + if err != nil { + _ = s.sendReplyCode(rigctldInvalidParam) + return + } + err = civControl.setFreq(uint(f)) + if err != nil { + _ = s.sendReplyCode(rigctldInvalidParam) + return + } + err = s.sendReplyCode(rigctldNoError) + case cmd == "m": + 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": + mode := cmdSplit[1] + if mode[:3] == "PKT" { + err = civControl.setDataMode(true) + if err != nil { + _ = s.sendReplyCode(rigctldInvalidParam) + return + } + 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 { + _ = s.sendReplyCode(rigctldNoError) + } + case cmd == "t": + 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": + if cmdSplit[1] != "0" { + err = civControl.setPTT(true) + } else { + err = civControl.setPTT(false) + } + if err != nil { + _ = s.sendReplyCode(rigctldInvalidParam) + } else { + _ = s.sendReplyCode(rigctldNoError) + } + case cmdSplit[0] == "V": + 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": + 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": + if cmdSplit[1] == "1" { + err = civControl.setSplit(splitModeOn) + } else { + err = civControl.setSplit(splitModeOff) + } + if err != nil { + _ = s.sendReplyCode(rigctldInvalidParam) + } else { + _ = s.sendReplyCode(rigctldNoError) + } + 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 + } +}