github.com/la5nta/wl2k-go@v0.11.8/transport/ax25/kenwood.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 ax25
     6  
     7  import (
     8  	"fmt"
     9  	"log"
    10  	"net"
    11  	"os"
    12  	"strings"
    13  	"syscall"
    14  	"time"
    15  
    16  	"github.com/albenik/go-serial/v2"
    17  	"github.com/la5nta/wl2k-go/fbb"
    18  )
    19  
    20  // KenwoodConn implements net.Conn using a
    21  // Kenwood (or similar) TNC in connected transparent mode.
    22  //
    23  // Tested with Kenwood TH-D72 and TM-D710 in "packet-mode".
    24  //
    25  // TODO: github.com/term/goserial does not support setting the
    26  // line flow control. Thus, KenwoodConn is not suitable for
    27  // sending messages > the TNC's internal buffer size.
    28  //
    29  // We should probably be using software flow control (XFLOW),
    30  // as hardware flow is not supported by many USB->RS232 adapters
    31  // including the adapter build into TH-D72 (at least, not using the
    32  // current linux kernel module.
    33  type KenwoodConn struct{ Conn }
    34  
    35  // Dial a packet node using a Kenwood (or similar) radio over serial
    36  func DialKenwood(dev, mycall, targetcall string, config Config, logger *log.Logger) (*KenwoodConn, error) {
    37  	if logger == nil {
    38  		logger = log.New(os.Stderr, "", log.LstdFlags)
    39  	}
    40  
    41  	localAddr, remoteAddr := tncAddrFromString(mycall), tncAddrFromString(targetcall)
    42  	conn := &KenwoodConn{Conn{
    43  		localAddr:  AX25Addr{localAddr},
    44  		remoteAddr: AX25Addr{remoteAddr},
    45  	}}
    46  
    47  	if dev == "socket" {
    48  		c, err := net.Dial("tcp", "127.0.0.1:8081")
    49  		if err != nil {
    50  			panic(err)
    51  		}
    52  		conn.Conn.ReadWriteCloser = c
    53  	} else {
    54  		s, err := serial.Open(dev, serial.WithBaudrate(config.SerialBaud))
    55  		if err != nil {
    56  			return conn, err
    57  		} else {
    58  			conn.Conn.ReadWriteCloser = s
    59  		}
    60  	}
    61  
    62  	// Initialize the TNC (with timeout)
    63  	initErr := make(chan error, 1)
    64  	go func() {
    65  		defer close(initErr)
    66  		conn.Write([]byte{3, 3, 3}) // ETX
    67  		fmt.Fprint(conn, "\r\nrestart\r\n")
    68  		// Wait for prompt, then send all the init commands
    69  		for {
    70  			line, err := fbb.ReadLine(conn)
    71  			if err != nil {
    72  				conn.Close()
    73  				initErr <- err
    74  				return
    75  			}
    76  
    77  			if strings.HasPrefix(line, "cmd:") {
    78  				fmt.Fprint(conn, "ECHO OFF\r") // Don't echo commands
    79  				fmt.Fprint(conn, "FLOW OFF\r")
    80  				fmt.Fprint(conn, "XFLOW ON\r")    // Enable software flow control
    81  				fmt.Fprint(conn, "LFIGNORE ON\r") // Ignore linefeed (\n)
    82  				fmt.Fprint(conn, "AUTOLF OFF\r")  // Don't auto-insert linefeed
    83  				fmt.Fprint(conn, "CR ON\r")
    84  				fmt.Fprint(conn, "8BITCONV ON\r") // Use 8-bit characters
    85  
    86  				// Return to command mode if station of current I/O stream disconnects.
    87  				fmt.Fprint(conn, "NEWMODE ON\r")
    88  
    89  				time.Sleep(500 * time.Millisecond)
    90  
    91  				fmt.Fprintf(conn, "MYCALL %s\r", mycall)
    92  				fmt.Fprintf(conn, "HBAUD %d\r", config.HBaud)
    93  				fmt.Fprintf(conn, "PACLEN %d\r", config.PacketLength)
    94  				fmt.Fprintf(conn, "TXDELAY %d\r", config.TXDelay/_CONFIG_TXDELAY_UNIT)
    95  				fmt.Fprintf(conn, "PERSIST %d\r", config.Persist)
    96  				time.Sleep(500 * time.Millisecond)
    97  
    98  				fmt.Fprintf(conn, "SLOTTIME %d\r", config.SlotTime/_CONFIG_SLOT_TIME_UNIT)
    99  				fmt.Fprint(conn, "FULLDUP OFF\r")
   100  				fmt.Fprintf(conn, "MAXFRAME %d\r", config.MaxFrame)
   101  				fmt.Fprintf(conn, "FRACK %d\r", config.FRACK/_CONFIG_FRACK_UNIT)
   102  				fmt.Fprintf(conn, "RESPTIME %d\r", config.ResponseTime/_CONFIG_RESPONSE_TIME_UNIT)
   103  				fmt.Fprintf(conn, "NOMODE ON\r")
   104  
   105  				break
   106  			}
   107  		}
   108  	}()
   109  	select {
   110  	case <-time.After(3 * time.Second):
   111  		conn.Close()
   112  		return nil, fmt.Errorf("initialization failed: deadline exceeded")
   113  	case err := <-initErr:
   114  		if err != nil {
   115  			conn.Close()
   116  			return nil, fmt.Errorf("initialization failed: %w", err)
   117  		}
   118  	}
   119  	time.Sleep(2 * time.Second)
   120  
   121  	// Dial the connection (with timeout)
   122  	dialErr := make(chan error, 1)
   123  	go func() {
   124  		defer close(dialErr)
   125  		fmt.Fprintf(conn, "\rc %s\r", targetcall)
   126  		// Wait for connect acknowledgement
   127  		for {
   128  			line, err := fbb.ReadLine(conn)
   129  			if err != nil {
   130  				dialErr <- err
   131  				return
   132  			}
   133  			logger.Println(line)
   134  			line = strings.TrimSpace(line)
   135  
   136  			switch {
   137  			case strings.Contains(line, "*** DISCONNECTED"):
   138  				dialErr <- fmt.Errorf("got disconnect %d", int(line[len(line)-1]))
   139  				return
   140  			case strings.Contains(line, "*** CONNECTED to"):
   141  				return
   142  			}
   143  		}
   144  	}()
   145  	select {
   146  	case <-time.After(5 * time.Minute):
   147  		conn.Close()
   148  		return nil, fmt.Errorf("connect failed: deadline exceeded")
   149  	case err := <-initErr:
   150  		if err != nil {
   151  			conn.Close()
   152  			return nil, fmt.Errorf("connect failed: %w", err)
   153  		}
   154  	}
   155  
   156  	// Success! Switch to TRANSPARENT mode and return the connection
   157  	fmt.Fprint(conn, "TRANS\r\n")
   158  	return conn, nil
   159  }
   160  
   161  func (c *KenwoodConn) Close() error {
   162  	if !c.ok() {
   163  		return syscall.EINVAL
   164  	}
   165  
   166  	// Exit TRANS mode
   167  	time.Sleep(1 * time.Second)
   168  	for i := 0; i < 3; i++ {
   169  		c.Write([]byte{3}) // ETX
   170  		time.Sleep(200 * time.Millisecond)
   171  	}
   172  
   173  	// Wait for prompt
   174  	time.Sleep(1 * time.Second)
   175  
   176  	// Disconnect
   177  	fmt.Fprint(c, "\r\nD\r\n")
   178  	for {
   179  		line, _ := fbb.ReadLine(c)
   180  		if strings.Contains(line, `DISCONN`) {
   181  			log.Println(`Disconnected`)
   182  			break
   183  		}
   184  	}
   185  	return c.Conn.Close()
   186  }