github.com/la5nta/wl2k-go@v0.11.8/rigcontrol/hamlib/rigctld.go (about) 1 // Copyright 2015 Martin Hebnes Pedersen (LA5NTA). All rights reserved. 2 // Use of this source code is governed by the MIT-license that can be 3 // found in the LICENSE file. 4 5 package hamlib 6 7 import ( 8 "errors" 9 "fmt" 10 "io" 11 "net" 12 "net/textproto" 13 "strconv" 14 "strings" 15 "sync" 16 "time" 17 ) 18 19 const DefaultTCPAddr = "localhost:4532" 20 21 var ErrNotVFOMode = errors.New("rigctl is not running in VFO mode") 22 23 var ErrUnexpectedValue = fmt.Errorf("Unexpected value in response") 24 25 // TCPTimeout defines the timeout duration of dial, read and write operations. 26 var TCPTimeout = time.Second 27 28 // Rig represents a receiver or tranceiver. 29 // 30 // It holds the tcp connection to the service (rigctld). 31 type TCPRig struct { 32 mu sync.Mutex 33 conn *textproto.Conn 34 tcpConn net.Conn 35 addr string 36 } 37 38 // VFO (Variable Frequency Oscillator) represents a tunable channel, 39 // from the radio operator's view. 40 // 41 // Also referred to as "BAND" (A-band/B-band) by some radio manufacturers. 42 type tcpVFO struct { 43 r *TCPRig 44 prefix string 45 } 46 47 // OpenTCP opens a new TCPRig and returns a ready to use Rig. 48 // 49 // The connection to rigctld is not initiated until the connection is requred. 50 // To check for a valid connection, call Ping. 51 // 52 // Caller must remember to Close the Rig after use. 53 func OpenTCP(addr string) (*TCPRig, error) { 54 r := &TCPRig{addr: addr} 55 return r, nil 56 } 57 58 // Ping checks that a connection to rigctld is open and valid. 59 // 60 // If no connection is active, it will try to establish one. 61 func (r *TCPRig) Ping() error { _, err := r.cmd(`dump_caps`); return err } 62 63 func (r *TCPRig) dial() (err error) { 64 r.mu.Lock() 65 defer r.mu.Unlock() 66 67 if r.conn != nil { 68 r.conn.Close() 69 } 70 71 // Dial with 3 second timeout 72 r.tcpConn, err = net.DialTimeout("tcp", r.addr, TCPTimeout) 73 if err != nil { 74 return err 75 } 76 77 r.conn = textproto.NewConn(r.tcpConn) 78 79 return err 80 } 81 82 // Closes the connection to the Rig. 83 func (r *TCPRig) Close() error { 84 if r.conn == nil { 85 return nil 86 } 87 return r.conn.Close() 88 } 89 90 // Returns the Rig's active VFO (for control). 91 func (r *TCPRig) CurrentVFO() VFO { return &tcpVFO{r, ""} } 92 93 // Returns the Rig's VFO A (for control). 94 // 95 // ErrNotVFOMode is returned if rigctld is not in VFO mode. 96 func (r *TCPRig) VFOA() (VFO, error) { 97 if ok, err := r.VFOMode(); err != nil { 98 return nil, err 99 } else if !ok { 100 return nil, ErrNotVFOMode 101 } 102 103 return &tcpVFO{r, "VFOA"}, nil 104 } 105 106 // Returns the Rig's VFO B (for control). 107 // 108 // ErrNotVFOMode is returned if rigctld is not in VFO mode. 109 func (r *TCPRig) VFOB() (VFO, error) { 110 if ok, err := r.VFOMode(); err != nil { 111 return nil, err 112 } else if !ok { 113 return nil, ErrNotVFOMode 114 } 115 116 return &tcpVFO{r, "VFOB"}, nil 117 } 118 119 func (r *TCPRig) VFOMode() (bool, error) { 120 resp, err := r.cmd(`\chk_vfo`) 121 if err != nil { 122 return false, err 123 } 124 return strings.TrimPrefix(resp, "CHKVFO ") == "1", nil 125 } 126 127 // Gets the dial frequency for this VFO. 128 func (v *tcpVFO) GetFreq() (int, error) { 129 resp, err := v.cmd(`\get_freq`) 130 if err != nil { 131 return -1, err 132 } 133 134 freq, err := strconv.Atoi(resp) 135 if err != nil { 136 return -1, err 137 } 138 139 return freq, nil 140 } 141 142 // Sets the dial frequency for this VFO. 143 func (v *tcpVFO) SetFreq(freq int) error { 144 _, err := v.cmd(`\set_freq %d`, freq) 145 return err 146 } 147 148 // GetPTT returns the PTT state for this VFO. 149 func (v *tcpVFO) GetPTT() (bool, error) { 150 resp, err := v.cmd("t") 151 if err != nil { 152 return false, err 153 } 154 155 switch resp { 156 case "0": 157 return false, nil 158 case "1", "2", "3": 159 return true, nil 160 default: 161 return false, ErrUnexpectedValue 162 } 163 } 164 165 // Enable (or disable) PTT on this VFO. 166 func (v *tcpVFO) SetPTT(on bool) error { 167 bInt := 0 168 if on == true { 169 bInt = 1 170 } 171 172 // Experimental PTT STATE 3 (https://github.com/la5nta/pat/issues/184) 173 if experimentalPTT3Enabled() { 174 bInt = 3 175 } 176 177 _, err := v.cmd(`\set_ptt %d`, bInt) 178 return err 179 } 180 181 func (v *tcpVFO) cmd(format string, args ...interface{}) (string, error) { 182 // Add VFO argument (if set) 183 if v.prefix != "" { 184 parts := strings.Split(format, " ") 185 parts = append([]string{parts[0], v.prefix}, parts[1:]...) 186 format = strings.Join(parts, " ") 187 } 188 return v.r.cmd(format, args...) 189 } 190 191 func (r *TCPRig) cmd(format string, args ...interface{}) (resp string, err error) { 192 // Retry 193 for i := 0; i < 3; i++ { 194 if r.conn == nil { 195 // Try re-dialing 196 if err = r.dial(); err != nil { 197 break 198 } 199 } 200 201 resp, err = r.doCmd(format, args...) 202 if err == nil { 203 break 204 } 205 206 _, isNetError := err.(net.Error) 207 if err == io.EOF || isNetError { 208 r.conn = nil 209 } 210 } 211 return resp, err 212 } 213 214 func (r *TCPRig) doCmd(format string, args ...interface{}) (string, error) { 215 r.tcpConn.SetDeadline(time.Now().Add(TCPTimeout)) 216 id, err := r.conn.Cmd(format, args...) 217 r.tcpConn.SetDeadline(time.Time{}) 218 219 if err != nil { 220 return "", err 221 } 222 223 r.conn.StartResponse(id) 224 defer r.conn.EndResponse(id) 225 226 r.tcpConn.SetDeadline(time.Now().Add(TCPTimeout)) 227 resp, err := r.conn.ReadLine() 228 r.tcpConn.SetDeadline(time.Time{}) 229 230 if err != nil { 231 return "", err 232 } else if err := toError(resp); err != nil { 233 return resp, err 234 } 235 236 return resp, nil 237 } 238 239 func toError(str string) error { 240 if !strings.HasPrefix(str, "RPRT ") { 241 return nil 242 } 243 244 parts := strings.SplitN(str, " ", 2) 245 246 code, err := strconv.Atoi(parts[1]) 247 if err != nil { 248 return err 249 } 250 251 switch code { 252 case 0: 253 return nil 254 default: 255 return fmt.Errorf("code %d", code) 256 } 257 }