1#!/usr/bin/env python 2# 3# Redirect data from a TCP/IP connection to a serial port and vice versa. 4# 5# (C) 2002-2020 Chris Liechti <cliechti@gmx.net> 6# 7# SPDX-License-Identifier: BSD-3-Clause 8 9import sys 10import socket 11import serial 12import serial.threaded 13import time 14 15 16class SerialToNet(serial.threaded.Protocol): 17 """serial->socket""" 18 19 def __init__(self): 20 self.socket = None 21 22 def __call__(self): 23 return self 24 25 def data_received(self, data): 26 if self.socket is not None: 27 self.socket.sendall(data) 28 29 30if __name__ == '__main__': # noqa 31 import argparse 32 33 parser = argparse.ArgumentParser( 34 description='Simple Serial to Network (TCP/IP) redirector.', 35 epilog="""\ 36NOTE: no security measures are implemented. Anyone can remotely connect 37to this service over the network. 38 39Only one connection at once is supported. When the connection is terminated 40it waits for the next connect. 41""") 42 43 parser.add_argument( 44 'SERIALPORT', 45 help="serial port name") 46 47 parser.add_argument( 48 'BAUDRATE', 49 type=int, 50 nargs='?', 51 help='set baud rate, default: %(default)s', 52 default=9600) 53 54 parser.add_argument( 55 '-q', '--quiet', 56 action='store_true', 57 help='suppress non error messages', 58 default=False) 59 60 parser.add_argument( 61 '--develop', 62 action='store_true', 63 help='Development mode, prints Python internals on errors', 64 default=False) 65 66 group = parser.add_argument_group('serial port') 67 68 group.add_argument( 69 "--bytesize", 70 choices=[5, 6, 7, 8], 71 type=int, 72 help="set bytesize, one of {5 6 7 8}, default: 8", 73 default=8) 74 75 group.add_argument( 76 "--parity", 77 choices=['N', 'E', 'O', 'S', 'M'], 78 type=lambda c: c.upper(), 79 help="set parity, one of {N E O S M}, default: N", 80 default='N') 81 82 group.add_argument( 83 "--stopbits", 84 choices=[1, 1.5, 2], 85 type=float, 86 help="set stopbits, one of {1 1.5 2}, default: 1", 87 default=1) 88 89 group.add_argument( 90 '--rtscts', 91 action='store_true', 92 help='enable RTS/CTS flow control (default off)', 93 default=False) 94 95 group.add_argument( 96 '--xonxoff', 97 action='store_true', 98 help='enable software flow control (default off)', 99 default=False) 100 101 group.add_argument( 102 '--rts', 103 type=int, 104 help='set initial RTS line state (possible values: 0, 1)', 105 default=None) 106 107 group.add_argument( 108 '--dtr', 109 type=int, 110 help='set initial DTR line state (possible values: 0, 1)', 111 default=None) 112 113 group = parser.add_argument_group('network settings') 114 115 exclusive_group = group.add_mutually_exclusive_group() 116 117 exclusive_group.add_argument( 118 '-P', '--localport', 119 type=int, 120 help='local TCP port', 121 default=7777) 122 123 exclusive_group.add_argument( 124 '-c', '--client', 125 metavar='HOST:PORT', 126 help='make the connection as a client, instead of running a server', 127 default=False) 128 129 args = parser.parse_args() 130 131 # connect to serial port 132 ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True) 133 ser.baudrate = args.BAUDRATE 134 ser.bytesize = args.bytesize 135 ser.parity = args.parity 136 ser.stopbits = args.stopbits 137 ser.rtscts = args.rtscts 138 ser.xonxoff = args.xonxoff 139 140 if args.rts is not None: 141 ser.rts = args.rts 142 143 if args.dtr is not None: 144 ser.dtr = args.dtr 145 146 if not args.quiet: 147 sys.stderr.write( 148 '--- TCP/IP to Serial redirect on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n' 149 '--- type Ctrl-C / BREAK to quit\n'.format(p=ser)) 150 151 try: 152 ser.open() 153 except serial.SerialException as e: 154 sys.stderr.write('Could not open serial port {}: {}\n'.format(ser.name, e)) 155 sys.exit(1) 156 157 ser_to_net = SerialToNet() 158 serial_worker = serial.threaded.ReaderThread(ser, ser_to_net) 159 serial_worker.start() 160 161 if not args.client: 162 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 163 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 164 srv.bind(('', args.localport)) 165 srv.listen(1) 166 try: 167 intentional_exit = False 168 while True: 169 if args.client: 170 host, port = args.client.split(':') 171 sys.stderr.write("Opening connection to {}:{}...\n".format(host, port)) 172 client_socket = socket.socket() 173 try: 174 client_socket.connect((host, int(port))) 175 except socket.error as msg: 176 sys.stderr.write('WARNING: {}\n'.format(msg)) 177 time.sleep(5) # intentional delay on reconnection as client 178 continue 179 sys.stderr.write('Connected\n') 180 client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 181 #~ client_socket.settimeout(5) 182 else: 183 sys.stderr.write('Waiting for connection on {}...\n'.format(args.localport)) 184 client_socket, addr = srv.accept() 185 sys.stderr.write('Connected by {}\n'.format(addr)) 186 # More quickly detect bad clients who quit without closing the 187 # connection: After 1 second of idle, start sending TCP keep-alive 188 # packets every 1 second. If 3 consecutive keep-alive packets 189 # fail, assume the client is gone and close the connection. 190 try: 191 client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1) 192 client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 1) 193 client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) 194 client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) 195 except AttributeError: 196 pass # XXX not available on windows 197 client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 198 try: 199 ser_to_net.socket = client_socket 200 # enter network <-> serial loop 201 while True: 202 try: 203 data = client_socket.recv(1024) 204 if not data: 205 break 206 ser.write(data) # get a bunch of bytes and send them 207 except socket.error as msg: 208 if args.develop: 209 raise 210 sys.stderr.write('ERROR: {}\n'.format(msg)) 211 # probably got disconnected 212 break 213 except KeyboardInterrupt: 214 intentional_exit = True 215 raise 216 except socket.error as msg: 217 if args.develop: 218 raise 219 sys.stderr.write('ERROR: {}\n'.format(msg)) 220 finally: 221 ser_to_net.socket = None 222 sys.stderr.write('Disconnected\n') 223 client_socket.close() 224 if args.client and not intentional_exit: 225 time.sleep(5) # intentional delay on reconnection as client 226 except KeyboardInterrupt: 227 pass 228 229 sys.stderr.write('\n--- exit ---\n') 230 serial_worker.stop() 231