blob: 42660dd3f052cd8c182c8997c174acf8b0d1ed2d [file] [log] [blame]
cliechtief39b8b2009-08-07 18:22:49 +00001#!/usr/bin/env python
Chris Liechtifbdd8a02015-08-09 02:37:45 +02002#
cliechtief39b8b2009-08-07 18:22:49 +00003# redirect data from a TCP/IP connection to a serial port and vice versa
4# using RFC 2217
Chris Liechtifbdd8a02015-08-09 02:37:45 +02005#
Chris Liechti34290f42015-08-17 03:08:24 +02006# (C) 2009-2015 Chris Liechti <cliechti@gmx.net>
Chris Liechtifbdd8a02015-08-09 02:37:45 +02007#
8# SPDX-License-Identifier: BSD-3-Clause
cliechtief39b8b2009-08-07 18:22:49 +00009
Chris Liechti4caf6a52015-08-04 01:07:45 +020010import logging
cliechtief39b8b2009-08-07 18:22:49 +000011import socket
Chris Liechti4caf6a52015-08-04 01:07:45 +020012import sys
13import time
14import threading
cliechtief39b8b2009-08-07 18:22:49 +000015import serial
16import serial.rfc2217
17
Chris Liechti6948bd82015-09-07 23:38:37 +020018
Chris Liechti4caf6a52015-08-04 01:07:45 +020019class Redirector(object):
Chris Liechti34290f42015-08-17 03:08:24 +020020 def __init__(self, serial_instance, socket, debug=False):
cliechtief39b8b2009-08-07 18:22:49 +000021 self.serial = serial_instance
22 self.socket = socket
23 self._write_lock = threading.Lock()
cliechti5cc3eb12009-08-11 23:04:30 +000024 self.rfc2217 = serial.rfc2217.PortManager(
Chris Liechti0269b2c2016-02-14 23:45:23 +010025 self.serial,
26 self,
27 logger=logging.getLogger('rfc2217.server') if debug else None)
cliechti5cc3eb12009-08-11 23:04:30 +000028 self.log = logging.getLogger('redirector')
cliechtief39b8b2009-08-07 18:22:49 +000029
30 def statusline_poller(self):
cliechti5cc3eb12009-08-11 23:04:30 +000031 self.log.debug('status line poll thread started')
cliechtief39b8b2009-08-07 18:22:49 +000032 while self.alive:
33 time.sleep(1)
34 self.rfc2217.check_modem_lines()
cliechti5cc3eb12009-08-11 23:04:30 +000035 self.log.debug('status line poll thread terminated')
cliechtief39b8b2009-08-07 18:22:49 +000036
Chris Liechti34290f42015-08-17 03:08:24 +020037 def shortcircuit(self):
cliechtief39b8b2009-08-07 18:22:49 +000038 """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)
Chris Liechti6948bd82015-09-07 23:38:37 +020042 self.thread_read.daemon = True
43 self.thread_read.name = 'serial->socket'
cliechtief39b8b2009-08-07 18:22:49 +000044 self.thread_read.start()
45 self.thread_poll = threading.Thread(target=self.statusline_poller)
Chris Liechti6948bd82015-09-07 23:38:37 +020046 self.thread_poll.daemon = True
47 self.thread_poll.name = 'status line poll'
cliechtief39b8b2009-08-07 18:22:49 +000048 self.thread_poll.start()
49 self.writer()
50
51 def reader(self):
52 """loop forever and copy serial->socket"""
cliechti5cc3eb12009-08-11 23:04:30 +000053 self.log.debug('reader thread started')
cliechtief39b8b2009-08-07 18:22:49 +000054 while self.alive:
55 try:
Chris Liechti6948bd82015-09-07 23:38:37 +020056 data = self.serial.read(self.serial.in_waiting or 1)
cliechtief39b8b2009-08-07 18:22:49 +000057 if data:
58 # escape outgoing data when needed (Telnet IAC (0xff) character)
Greg Bowserd0058422016-11-22 13:38:24 -050059 self.write(b''.join(self.rfc2217.escape(data)))
Chris Liechti4caf6a52015-08-04 01:07:45 +020060 except socket.error as msg:
Chris Liechti9a166662016-06-21 23:22:53 +020061 self.log.error('{}'.format(msg))
cliechtief39b8b2009-08-07 18:22:49 +000062 # probably got disconnected
63 break
64 self.alive = False
cliechti5cc3eb12009-08-11 23:04:30 +000065 self.log.debug('reader thread terminated')
cliechtief39b8b2009-08-07 18:22:49 +000066
67 def write(self, data):
68 """thread safe socket write with no data escaping. used to send telnet stuff"""
Chris Liechti4caf6a52015-08-04 01:07:45 +020069 with self._write_lock:
cliechtief39b8b2009-08-07 18:22:49 +000070 self.socket.sendall(data)
cliechtief39b8b2009-08-07 18:22:49 +000071
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
Greg Bowserd0058422016-11-22 13:38:24 -050079 self.serial.write(b''.join(self.rfc2217.filter(data)))
Chris Liechti4caf6a52015-08-04 01:07:45 +020080 except socket.error as msg:
Chris Liechti9a166662016-06-21 23:22:53 +020081 self.log.error('{}'.format(msg))
cliechtief39b8b2009-08-07 18:22:49 +000082 # probably got disconnected
83 break
cliechtid9a06ce2009-08-10 01:30:53 +000084 self.stop()
cliechtief39b8b2009-08-07 18:22:49 +000085
86 def stop(self):
87 """Stop copying"""
cliechti5cc3eb12009-08-11 23:04:30 +000088 self.log.debug('stopping')
cliechtief39b8b2009-08-07 18:22:49 +000089 if self.alive:
90 self.alive = False
91 self.thread_read.join()
cliechtid9a06ce2009-08-10 01:30:53 +000092 self.thread_poll.join()
cliechtief39b8b2009-08-07 18:22:49 +000093
94
95if __name__ == '__main__':
Chris Liechti34290f42015-08-17 03:08:24 +020096 import argparse
cliechtief39b8b2009-08-07 18:22:49 +000097
Chris Liechti34290f42015-08-17 03:08:24 +020098 parser = argparse.ArgumentParser(
Chris Liechti6948bd82015-09-07 23:38:37 +020099 description="RFC 2217 Serial to Network (TCP/IP) redirector.",
100 epilog="""\
cliechtief39b8b2009-08-07 18:22:49 +0000101NOTE: 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
Chris Liechti34290f42015-08-17 03:08:24 +0200108 parser.add_argument('SERIALPORT')
cliechtief39b8b2009-08-07 18:22:49 +0000109
Chris Liechti6948bd82015-09-07 23:38:37 +0200110 parser.add_argument(
Chris Liechti0269b2c2016-02-14 23:45:23 +0100111 '-p', '--localport',
112 type=int,
113 help='local TCP port, default: %(default)s',
114 metavar='TCPPORT',
115 default=2217)
cliechti5cc3eb12009-08-11 23:04:30 +0000116
Chris Liechti6948bd82015-09-07 23:38:37 +0200117 parser.add_argument(
Chris Liechti0269b2c2016-02-14 23:45:23 +0100118 '-v', '--verbose',
119 dest='verbosity',
120 action='count',
121 help='print more diagnostic messages (option can be given multiple times)',
122 default=0)
cliechtief39b8b2009-08-07 18:22:49 +0000123
Chris Liechti34290f42015-08-17 03:08:24 +0200124 args = parser.parse_args()
cliechtief39b8b2009-08-07 18:22:49 +0000125
Chris Liechti34290f42015-08-17 03:08:24 +0200126 if args.verbosity > 3:
127 args.verbosity = 3
Chris Liechti0269b2c2016-02-14 23:45:23 +0100128 level = (logging.WARNING,
129 logging.INFO,
130 logging.DEBUG,
131 logging.NOTSET)[args.verbosity]
cliechti5cc3eb12009-08-11 23:04:30 +0000132 logging.basicConfig(level=logging.INFO)
Chris Liechti6948bd82015-09-07 23:38:37 +0200133 #~ logging.getLogger('root').setLevel(logging.INFO)
cliechti5cc3eb12009-08-11 23:04:30 +0000134 logging.getLogger('rfc2217').setLevel(level)
135
cliechtief39b8b2009-08-07 18:22:49 +0000136 # connect to serial port
Chris Liechticd42db92015-08-17 03:24:34 +0200137 ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True)
Chris Liechti6948bd82015-09-07 23:38:37 +0200138 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
cliechtief39b8b2009-08-07 18:22:49 +0000142
cliechti5cc3eb12009-08-11 23:04:30 +0000143 logging.info("RFC 2217 TCP/IP to Serial redirector - type Ctrl-C / BREAK to quit")
cliechtief39b8b2009-08-07 18:22:49 +0000144
145 try:
146 ser.open()
Chris Liechti34290f42015-08-17 03:08:24 +0200147 except serial.SerialException as e:
Chris Liechti6948bd82015-09-07 23:38:37 +0200148 logging.error("Could not open serial port {}: {}".format(ser.name, e))
cliechtief39b8b2009-08-07 18:22:49 +0000149 sys.exit(1)
150
Chris Liechti6948bd82015-09-07 23:38:37 +0200151 logging.info("Serving serial port: {}".format(ser.name))
152 settings = ser.get_settings()
cliechtief39b8b2009-08-07 18:22:49 +0000153
154 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
155 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Chris Liechti34290f42015-08-17 03:08:24 +0200156 srv.bind(('', args.localport))
cliechtief39b8b2009-08-07 18:22:49 +0000157 srv.listen(1)
Chris Liechti6948bd82015-09-07 23:38:37 +0200158 logging.info("TCP/IP port: {}".format(args.localport))
cliechtief39b8b2009-08-07 18:22:49 +0000159 while True:
160 try:
Chris Liechti6948bd82015-09-07 23:38:37 +0200161 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
cliechtid9a06ce2009-08-10 01:30:53 +0000166 # enter network <-> serial loop
cliechtief39b8b2009-08-07 18:22:49 +0000167 r = Redirector(
Chris Liechti0269b2c2016-02-14 23:45:23 +0100168 ser,
169 client_socket,
170 args.verbosity > 0)
cliechtid9a06ce2009-08-10 01:30:53 +0000171 try:
Chris Liechti34290f42015-08-17 03:08:24 +0200172 r.shortcircuit()
cliechtid9a06ce2009-08-10 01:30:53 +0000173 finally:
cliechti5cc3eb12009-08-11 23:04:30 +0000174 logging.info('Disconnected')
cliechtid9a06ce2009-08-10 01:30:53 +0000175 r.stop()
Chris Liechti6948bd82015-09-07 23:38:37 +0200176 client_socket.close()
177 ser.dtr = False
178 ser.rts = False
Chris Liechti34290f42015-08-17 03:08:24 +0200179 # Restore port settings (may have been changed by RFC 2217
180 # capable client)
Chris Liechti6948bd82015-09-07 23:38:37 +0200181 ser.apply_settings(settings)
cliechtief39b8b2009-08-07 18:22:49 +0000182 except KeyboardInterrupt:
Chris Liechti6948bd82015-09-07 23:38:37 +0200183 sys.stdout.write('\n')
cliechtief39b8b2009-08-07 18:22:49 +0000184 break
Chris Liechti4caf6a52015-08-04 01:07:45 +0200185 except socket.error as msg:
Chris Liechti6948bd82015-09-07 23:38:37 +0200186 logging.error(str(msg))
cliechtief39b8b2009-08-07 18:22:49 +0000187
cliechti5cc3eb12009-08-11 23:04:30 +0000188 logging.info('--- exit ---')