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  }