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 }