Add internal rigctld

This commit is contained in:
Nonoo 2020-11-08 20:48:39 +01:00
parent 3a1b74ce2d
commit 2d1bbf76dc
7 changed files with 439 additions and 31 deletions

View file

@ -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 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). 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 a **TCP server** on port `4533` for exposing the **serial port**.
- Starts `rigctld` and connects it to kappanhang's TCP serial port server. - Starts an **internal rigctld** server. This can be used for controlling the
This can be used for controlling the server (the transceiver) with server (the transceiver) with [Hamlib](https://hamlib.github.io/) (`rigctl`)
[Hamlib](https://hamlib.github.io/) (`rigctld`). clients.
To use this with for example [WSJT-X](https://physics.princeton.edu/pulsar/K1JT/wsjtx.html), 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 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 NET rigctl`, and the *Network server* to `localhost`.
set the *poll interval* to 10 seconds.
### Virtual serial port ### Virtual serial port

View file

@ -14,8 +14,7 @@ var username string
var password string var password string
var serialTCPPort uint16 var serialTCPPort uint16
var enableSerialDevice bool var enableSerialDevice bool
var rigctldModel uint var rigctldPort uint16
var disableRigctld bool
var runCmd string var runCmd string
var runCmdOnSerialPortCreated string var runCmdOnSerialPortCreated string
var statusLogInterval time.Duration var statusLogInterval time.Duration
@ -28,8 +27,7 @@ func parseArgs() {
p := getopt.StringLong("password", 'p', "beerbeer", "Password") p := getopt.StringLong("password", 'p', "beerbeer", "Password")
t := getopt.Uint16Long("serial-tcp-port", 't', 4533, "Expose radio's serial port on this TCP port") 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") 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.Uint16Long("rigctld-port", 'r', 4532, "Use this TCP port for the internal rigctld")
r := getopt.BoolLong("disable-rigctld", 'r', "Disable starting rigctld")
e := getopt.StringLong("exec", 'e', "", "Exec cmd when connected") 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") 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") i := getopt.Uint16Long("log-interval", 'i', 100, "Status bar/log interval in milliseconds")
@ -48,8 +46,7 @@ func parseArgs() {
password = *p password = *p
serialTCPPort = *t serialTCPPort = *t
enableSerialDevice = *s enableSerialDevice = *s
rigctldModel = *m rigctldPort = *r
disableRigctld = *r
runCmd = *e runCmd = *e
runCmdOnSerialPortCreated = *o runCmdOnSerialPortCreated = *o
statusLogInterval = time.Duration(*i) * time.Millisecond statusLogInterval = time.Duration(*i) * time.Millisecond

View file

@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"math" "math"
"sync"
"time" "time"
) )
@ -109,6 +110,8 @@ type civControlStruct struct {
setVFOSent bool setVFOSent bool
setSplitSent bool setSplitSent bool
mutex sync.Mutex
freq uint freq uint
ptt bool ptt bool
tune bool tune bool
@ -141,6 +144,9 @@ func (s *civControlStruct) decode(d []byte) bool {
payload := d[5 : len(d)-1] payload := d[5 : len(d)-1]
s.state.mutex.Lock()
defer s.state.mutex.Unlock()
switch d[4] { switch d[4] {
case 0x00: case 0x00:
return s.decodeFreq(payload) 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}) return s.st.send([]byte{254, 254, civAddress, 224, 0x1c, 1, b, 253})
} }
func (s *civControlStruct) toggleDataMode() error { func (s *civControlStruct) setDataMode(enable bool) error {
s.state.setDataModeSent = true
var b byte var b byte
var f byte var f byte
if !s.state.dataMode { if enable {
b = 1 b = 1
f = 1 f = 1
} else { } else {
b = 0 b = 0
f = 0 f = 0
} }
return s.st.send([]byte{254, 254, civAddress, 224, 0x1a, 0x06, b, f, 253}) 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 { func (s *civControlStruct) incBand() error {
s.state.bandChanging = true s.state.bandChanging = true
i := s.state.bandIdx + 1 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}) 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 { func (s *civControlStruct) toggleVFO() error {
s.state.setVFOSent = true s.state.setVFOSent = true
var b byte var b byte
if !s.state.vfoBActive { if !s.state.vfoBActive {
b = 1 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 { func (s *civControlStruct) toggleSplit() error {
s.state.setSplitSent = true s.state.setSplitSent = true
var b byte var mode splitMode
switch s.state.splitMode { switch s.state.splitMode {
case splitModeOff: case splitModeOff:
b = 0x01 mode = splitModeOn
case splitModeOn: case splitModeOn:
b = 0x011 mode = splitModeDUPMinus
case splitModeDUPMinus: case splitModeDUPMinus:
b = 0x12 mode = splitModeDUPPlus
default: 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 { func (s *civControlStruct) getFreq() error {

View file

@ -17,7 +17,6 @@ type cmdRunner struct {
var runCmdRunner cmdRunner var runCmdRunner cmdRunner
var serialCmdRunner cmdRunner var serialCmdRunner cmdRunner
var rigctldRunner cmdRunner
func (c *cmdRunner) kill(cmd *exec.Cmd) { func (c *cmdRunner) kill(cmd *exec.Cmd) {
err := cmd.Process.Kill() err := cmd.Process.Kill()

View file

@ -5,8 +5,6 @@ import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"os/exec"
"time" "time"
) )
@ -273,12 +271,8 @@ func (s *controlStream) handleRead(r []byte) error {
if enableSerialDevice { if enableSerialDevice {
serialCmdRunner.startIfNeeded(runCmdOnSerialPortCreated) serialCmdRunner.startIfNeeded(runCmdOnSerialPortCreated)
} }
if !disableRigctld { if err := rigctld.initIfNeeded(); err != nil {
if _, err := exec.LookPath("rigctld"); err != nil { return err
log.Error("can't start rigctld: ", err)
} else {
rigctldRunner.startIfNeeded(fmt.Sprint("rigctld -m ", rigctldModel, " -r :", serialTCPPort))
}
} }
} }
} }

View file

@ -150,10 +150,10 @@ exit:
log.Print("restarting control stream...") log.Print("restarting control stream...")
} }
rigctld.deinit()
serialTCPSrv.deinit() serialTCPSrv.deinit()
runCmdRunner.stop() runCmdRunner.stop()
serialCmdRunner.stop() serialCmdRunner.stop()
rigctldRunner.stop()
audio.deinit() audio.deinit()
serialPort.deinit() serialPort.deinit()

387
rigctld.go Normal file
View file

@ -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
}
}