blob: 10c47c54f1cb8f0c1cf2d70ff55ac796236dafe2 [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 Liechti34290f42015-08-17 03:08:24 +020025 self.serial,
26 self,
Chris Liechti6948bd82015-09-07 23:38:37 +020027 logger=logging.getLogger('rfc2217.server') if debug else None
Chris Liechti34290f42015-08-17 03:08:24 +020028 )
cliechti5cc3eb12009-08-11 23:04:30 +000029 self.log = logging.getLogger('redirector')
cliechtief39b8b2009-08-07 18:22:49 +000030
31 def statusline_poller(self):
cliechti5cc3eb12009-08-11 23:04:30 +000032 self.log.debug('status line poll thread started')
cliechtief39b8b2009-08-07 18:22:49 +000033 while self.alive:
34 time.sleep(1)
35 self.rfc2217.check_modem_lines()
cliechti5cc3eb12009-08-11 23:04:30 +000036 self.log.debug('status line poll thread terminated')
cliechtief39b8b2009-08-07 18:22:49 +000037
Chris Liechti34290f42015-08-17 03:08:24 +020038 def shortcircuit(self):
cliechtief39b8b2009-08-07 18:22:49 +000039 """connect the serial port to the TCP port by copying everything
40 from one side to the other"""
41 self.alive = True
42 self.thread_read = threading.Thread(target=self.reader)
Chris Liechti6948bd82015-09-07 23:38:37 +020043 self.thread_read.daemon = True
44 self.thread_read.name = 'serial->socket'
cliechtief39b8b2009-08-07 18:22:49 +000045 self.thread_read.start()
46 self.thread_poll = threading.Thread(target=self.statusline_poller)
Chris Liechti6948bd82015-09-07 23:38:37 +020047 self.thread_poll.daemon = True
48 self.thread_poll.name = 'status line poll'
cliechtief39b8b2009-08-07 18:22:49 +000049 self.thread_poll.start()
50 self.writer()
51
52 def reader(self):
53 """loop forever and copy serial->socket"""
cliechti5cc3eb12009-08-11 23:04:30 +000054 self.log.debug('reader thread started')
cliechtief39b8b2009-08-07 18:22:49 +000055 while self.alive:
56 try:
Chris Liechti6948bd82015-09-07 23:38:37 +020057 data = self.serial.read(self.serial.in_waiting or 1)
cliechtief39b8b2009-08-07 18:22:49 +000058 if data:
59 # escape outgoing data when needed (Telnet IAC (0xff) character)
Chris Liechti6948bd82015-09-07 23:38:37 +020060 self.write(serial.to_bytes(self.rfc2217.escape(data)))
Chris Liechti4caf6a52015-08-04 01:07:45 +020061 except socket.error as msg:
cliechti5cc3eb12009-08-11 23:04:30 +000062 self.log.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +000063 # probably got disconnected
64 break
65 self.alive = False
cliechti5cc3eb12009-08-11 23:04:30 +000066 self.log.debug('reader thread terminated')
cliechtief39b8b2009-08-07 18:22:49 +000067
68 def write(self, data):
69 """thread safe socket write with no data escaping. used to send telnet stuff"""
Chris Liechti4caf6a52015-08-04 01:07:45 +020070 with self._write_lock:
cliechtief39b8b2009-08-07 18:22:49 +000071 self.socket.sendall(data)
cliechtief39b8b2009-08-07 18:22:49 +000072
73 def writer(self):
74 """loop forever and copy socket->serial"""
75 while self.alive:
76 try:
77 data = self.socket.recv(1024)
78 if not data:
79 break
80 self.serial.write(serial.to_bytes(self.rfc2217.filter(data)))
Chris Liechti4caf6a52015-08-04 01:07:45 +020081 except socket.error as msg:
cliechti5cc3eb12009-08-11 23:04:30 +000082 self.log.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +000083 # probably got disconnected
84 break
cliechtid9a06ce2009-08-10 01:30:53 +000085 self.stop()
cliechtief39b8b2009-08-07 18:22:49 +000086
87 def stop(self):
88 """Stop copying"""
cliechti5cc3eb12009-08-11 23:04:30 +000089 self.log.debug('stopping')
cliechtief39b8b2009-08-07 18:22:49 +000090 if self.alive:
91 self.alive = False
92 self.thread_read.join()
cliechtid9a06ce2009-08-10 01:30:53 +000093 self.thread_poll.join()
cliechtief39b8b2009-08-07 18:22:49 +000094
95
96if __name__ == '__main__':
Chris Liechti34290f42015-08-17 03:08:24 +020097 import argparse
cliechtief39b8b2009-08-07 18:22:49 +000098
Chris Liechti34290f42015-08-17 03:08:24 +020099 parser = argparse.ArgumentParser(
Chris Liechti6948bd82015-09-07 23:38:37 +0200100 description="RFC 2217 Serial to Network (TCP/IP) redirector.",
101 epilog="""\
cliechtief39b8b2009-08-07 18:22:49 +0000102NOTE: no security measures are implemented. Anyone can remotely connect
103to this service over the network.
104
105Only one connection at once is supported. When the connection is terminated
106it waits for the next connect.
107""")
108
Chris Liechti34290f42015-08-17 03:08:24 +0200109 parser.add_argument('SERIALPORT')
cliechtief39b8b2009-08-07 18:22:49 +0000110
Chris Liechti6948bd82015-09-07 23:38:37 +0200111 parser.add_argument(
112 '-p', '--localport',
Chris Liechti34290f42015-08-17 03:08:24 +0200113 type=int,
114 help='local TCP port, default: %(default)s',
115 metavar='TCPPORT',
116 default=2217
117 )
cliechti5cc3eb12009-08-11 23:04:30 +0000118
Chris Liechti6948bd82015-09-07 23:38:37 +0200119 parser.add_argument(
120 '-v', '--verbose',
Chris Liechti34290f42015-08-17 03:08:24 +0200121 dest='verbosity',
122 action='count',
123 help='print more diagnostic messages (option can be given multiple times)',
124 default=0
125 )
cliechtief39b8b2009-08-07 18:22:49 +0000126
Chris Liechti34290f42015-08-17 03:08:24 +0200127 args = parser.parse_args()
cliechtief39b8b2009-08-07 18:22:49 +0000128
Chris Liechti34290f42015-08-17 03:08:24 +0200129 if args.verbosity > 3:
130 args.verbosity = 3
cliechti5cc3eb12009-08-11 23:04:30 +0000131 level = (
Chris Liechti34290f42015-08-17 03:08:24 +0200132 logging.WARNING,
133 logging.INFO,
134 logging.DEBUG,
135 logging.NOTSET,
136 )[args.verbosity]
cliechti5cc3eb12009-08-11 23:04:30 +0000137 logging.basicConfig(level=logging.INFO)
Chris Liechti6948bd82015-09-07 23:38:37 +0200138 #~ logging.getLogger('root').setLevel(logging.INFO)
cliechti5cc3eb12009-08-11 23:04:30 +0000139 logging.getLogger('rfc2217').setLevel(level)
140
cliechtief39b8b2009-08-07 18:22:49 +0000141 # connect to serial port
Chris Liechticd42db92015-08-17 03:24:34 +0200142 ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True)
Chris Liechti6948bd82015-09-07 23:38:37 +0200143 ser.timeout = 3 # required so that the reader thread can exit
144 # reset control line as no _remote_ "terminal" has been connected yet
145 ser.dtr = False
146 ser.rts = False
cliechtief39b8b2009-08-07 18:22:49 +0000147
cliechti5cc3eb12009-08-11 23:04:30 +0000148 logging.info("RFC 2217 TCP/IP to Serial redirector - type Ctrl-C / BREAK to quit")
cliechtief39b8b2009-08-07 18:22:49 +0000149
150 try:
151 ser.open()
Chris Liechti34290f42015-08-17 03:08:24 +0200152 except serial.SerialException as e:
Chris Liechti6948bd82015-09-07 23:38:37 +0200153 logging.error("Could not open serial port {}: {}".format(ser.name, e))
cliechtief39b8b2009-08-07 18:22:49 +0000154 sys.exit(1)
155
Chris Liechti6948bd82015-09-07 23:38:37 +0200156 logging.info("Serving serial port: {}".format(ser.name))
157 settings = ser.get_settings()
cliechtief39b8b2009-08-07 18:22:49 +0000158
159 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
160 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Chris Liechti34290f42015-08-17 03:08:24 +0200161 srv.bind(('', args.localport))
cliechtief39b8b2009-08-07 18:22:49 +0000162 srv.listen(1)
Chris Liechti6948bd82015-09-07 23:38:37 +0200163 logging.info("TCP/IP port: {}".format(args.localport))
cliechtief39b8b2009-08-07 18:22:49 +0000164 while True:
165 try:
Chris Liechti6948bd82015-09-07 23:38:37 +0200166 client_socket, addr = srv.accept()
167 logging.info('Connected by {}:{}'.format(addr[0], addr[1]))
168 client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
169 ser.rts = True
170 ser.dtr = True
cliechtid9a06ce2009-08-10 01:30:53 +0000171 # enter network <-> serial loop
cliechtief39b8b2009-08-07 18:22:49 +0000172 r = Redirector(
Chris Liechti34290f42015-08-17 03:08:24 +0200173 ser,
Chris Liechti6948bd82015-09-07 23:38:37 +0200174 client_socket,
Chris Liechti34290f42015-08-17 03:08:24 +0200175 args.verbosity > 0
176 )
cliechtid9a06ce2009-08-10 01:30:53 +0000177 try:
Chris Liechti34290f42015-08-17 03:08:24 +0200178 r.shortcircuit()
cliechtid9a06ce2009-08-10 01:30:53 +0000179 finally:
cliechti5cc3eb12009-08-11 23:04:30 +0000180 logging.info('Disconnected')
cliechtid9a06ce2009-08-10 01:30:53 +0000181 r.stop()
Chris Liechti6948bd82015-09-07 23:38:37 +0200182 client_socket.close()
183 ser.dtr = False
184 ser.rts = False
Chris Liechti34290f42015-08-17 03:08:24 +0200185 # Restore port settings (may have been changed by RFC 2217
186 # capable client)
Chris Liechti6948bd82015-09-07 23:38:37 +0200187 ser.apply_settings(settings)
cliechtief39b8b2009-08-07 18:22:49 +0000188 except KeyboardInterrupt:
Chris Liechti6948bd82015-09-07 23:38:37 +0200189 sys.stdout.write('\n')
cliechtief39b8b2009-08-07 18:22:49 +0000190 break
Chris Liechti4caf6a52015-08-04 01:07:45 +0200191 except socket.error as msg:
Chris Liechti6948bd82015-09-07 23:38:37 +0200192 logging.error(str(msg))
cliechtief39b8b2009-08-07 18:22:49 +0000193
cliechti5cc3eb12009-08-11 23:04:30 +0000194 logging.info('--- exit ---')