1#!/usr/bin/env python 2# 3# redirect data from a TCP/IP connection to a serial port and vice versa 4# using RFC 2217 5# 6# (C) 2009-2015 Chris Liechti <[email protected]> 7# 8# SPDX-License-Identifier: BSD-3-Clause 9 10import logging 11import socket 12import sys 13import time 14import threading 15import serial 16import serial.rfc2217 17 18 19class Redirector(object): 20 def __init__(self, serial_instance, socket, debug=False): 21 self.serial = serial_instance 22 self.socket = socket 23 self._write_lock = threading.Lock() 24 self.rfc2217 = serial.rfc2217.PortManager( 25 self.serial, 26 self, 27 logger=logging.getLogger('rfc2217.server') if debug else None) 28 self.log = logging.getLogger('redirector') 29 30 def statusline_poller(self): 31 self.log.debug('status line poll thread started') 32 while self.alive: 33 time.sleep(1) 34 self.rfc2217.check_modem_lines() 35 self.log.debug('status line poll thread terminated') 36 37 def shortcircuit(self): 38 """connect the serial port to the TCP port by copying everything 39 from one side to the other""" 40 self.alive = True 41 self.thread_read = threading.Thread(target=self.reader) 42 self.thread_read.daemon = True 43 self.thread_read.name = 'serial->socket' 44 self.thread_read.start() 45 self.thread_poll = threading.Thread(target=self.statusline_poller) 46 self.thread_poll.daemon = True 47 self.thread_poll.name = 'status line poll' 48 self.thread_poll.start() 49 self.writer() 50 51 def reader(self): 52 """loop forever and copy serial->socket""" 53 self.log.debug('reader thread started') 54 while self.alive: 55 try: 56 data = self.serial.read(self.serial.in_waiting or 1) 57 if data: 58 # escape outgoing data when needed (Telnet IAC (0xff) character) 59 self.write(b''.join(self.rfc2217.escape(data))) 60 except socket.error as msg: 61 self.log.error('{}'.format(msg)) 62 # probably got disconnected 63 break 64 self.alive = False 65 self.log.debug('reader thread terminated') 66 67 def write(self, data): 68 """thread safe socket write with no data escaping. used to send telnet stuff""" 69 with self._write_lock: 70 self.socket.sendall(data) 71 72 def writer(self): 73 """loop forever and copy socket->serial""" 74 while self.alive: 75 try: 76 data = self.socket.recv(1024) 77 if not data: 78 break 79 self.serial.write(b''.join(self.rfc2217.filter(data))) 80 except socket.error as msg: 81 self.log.error('{}'.format(msg)) 82 # probably got disconnected 83 break 84 self.stop() 85 86 def stop(self): 87 """Stop copying""" 88 self.log.debug('stopping') 89 if self.alive: 90 self.alive = False 91 self.thread_read.join() 92 self.thread_poll.join() 93 94 95if __name__ == '__main__': 96 import argparse 97 98 parser = argparse.ArgumentParser( 99 description="RFC 2217 Serial to Network (TCP/IP) redirector.", 100 epilog="""\ 101NOTE: no security measures are implemented. Anyone can remotely connect 102to this service over the network. 103 104Only one connection at once is supported. When the connection is terminated 105it waits for the next connect. 106""") 107 108 parser.add_argument('SERIALPORT') 109 110 parser.add_argument( 111 '-p', '--localport', 112 type=int, 113 help='local TCP port, default: %(default)s', 114 metavar='TCPPORT', 115 default=2217) 116 117 parser.add_argument( 118 '-v', '--verbose', 119 dest='verbosity', 120 action='count', 121 help='print more diagnostic messages (option can be given multiple times)', 122 default=0) 123 124 args = parser.parse_args() 125 126 if args.verbosity > 3: 127 args.verbosity = 3 128 level = (logging.WARNING, 129 logging.INFO, 130 logging.DEBUG, 131 logging.NOTSET)[args.verbosity] 132 logging.basicConfig(level=logging.INFO) 133 #~ logging.getLogger('root').setLevel(logging.INFO) 134 logging.getLogger('rfc2217').setLevel(level) 135 136 # connect to serial port 137 ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True) 138 ser.timeout = 3 # required so that the reader thread can exit 139 # reset control line as no _remote_ "terminal" has been connected yet 140 ser.dtr = False 141 ser.rts = False 142 143 logging.info("RFC 2217 TCP/IP to Serial redirector - type Ctrl-C / BREAK to quit") 144 145 try: 146 ser.open() 147 except serial.SerialException as e: 148 logging.error("Could not open serial port {}: {}".format(ser.name, e)) 149 sys.exit(1) 150 151 logging.info("Serving serial port: {}".format(ser.name)) 152 settings = ser.get_settings() 153 154 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 155 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 156 srv.bind(('', args.localport)) 157 srv.listen(1) 158 logging.info("TCP/IP port: {}".format(args.localport)) 159 while True: 160 try: 161 client_socket, addr = srv.accept() 162 logging.info('Connected by {}:{}'.format(addr[0], addr[1])) 163 client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 164 ser.rts = True 165 ser.dtr = True 166 # enter network <-> serial loop 167 r = Redirector( 168 ser, 169 client_socket, 170 args.verbosity > 0) 171 try: 172 r.shortcircuit() 173 finally: 174 logging.info('Disconnected') 175 r.stop() 176 client_socket.close() 177 ser.dtr = False 178 ser.rts = False 179 # Restore port settings (may have been changed by RFC 2217 180 # capable client) 181 ser.apply_settings(settings) 182 except KeyboardInterrupt: 183 sys.stdout.write('\n') 184 break 185 except socket.error as msg: 186 logging.error(str(msg)) 187 188 logging.info('--- exit ---') 189