blob: 53f11b47d0e90f48e72e6cc817f18fdb6f8aee0d [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 os
cliechtief39b8b2009-08-07 18:22:49 +000012import socket
Chris Liechti4caf6a52015-08-04 01:07:45 +020013import sys
14import time
15import threading
cliechtief39b8b2009-08-07 18:22:49 +000016import serial
17import serial.rfc2217
18
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,
27 logger = logging.getLogger('rfc2217.server') if debug else None
28 )
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)
43 self.thread_read.setDaemon(True)
44 self.thread_read.setName('serial->socket')
45 self.thread_read.start()
46 self.thread_poll = threading.Thread(target=self.statusline_poller)
47 self.thread_poll.setDaemon(True)
48 self.thread_poll.setName('status line poll')
49 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:
57 data = self.serial.read(1) # read one, blocking
58 n = self.serial.inWaiting() # look if there is more
59 if n:
60 data = data + self.serial.read(n) # and get as much as possible
61 if data:
62 # escape outgoing data when needed (Telnet IAC (0xff) character)
63 data = serial.to_bytes(self.rfc2217.escape(data))
Chris Liechti4caf6a52015-08-04 01:07:45 +020064 with self._write_lock:
cliechtief39b8b2009-08-07 18:22:49 +000065 self.socket.sendall(data) # send it over TCP
Chris Liechti4caf6a52015-08-04 01:07:45 +020066 except socket.error as msg:
cliechti5cc3eb12009-08-11 23:04:30 +000067 self.log.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +000068 # probably got disconnected
69 break
70 self.alive = False
cliechti5cc3eb12009-08-11 23:04:30 +000071 self.log.debug('reader thread terminated')
cliechtief39b8b2009-08-07 18:22:49 +000072
73 def write(self, data):
74 """thread safe socket write with no data escaping. used to send telnet stuff"""
Chris Liechti4caf6a52015-08-04 01:07:45 +020075 with self._write_lock:
cliechtief39b8b2009-08-07 18:22:49 +000076 self.socket.sendall(data)
cliechtief39b8b2009-08-07 18:22:49 +000077
78 def writer(self):
79 """loop forever and copy socket->serial"""
80 while self.alive:
81 try:
82 data = self.socket.recv(1024)
83 if not data:
84 break
85 self.serial.write(serial.to_bytes(self.rfc2217.filter(data)))
Chris Liechti4caf6a52015-08-04 01:07:45 +020086 except socket.error as msg:
cliechti5cc3eb12009-08-11 23:04:30 +000087 self.log.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +000088 # probably got disconnected
89 break
cliechtid9a06ce2009-08-10 01:30:53 +000090 self.stop()
cliechtief39b8b2009-08-07 18:22:49 +000091
92 def stop(self):
93 """Stop copying"""
cliechti5cc3eb12009-08-11 23:04:30 +000094 self.log.debug('stopping')
cliechtief39b8b2009-08-07 18:22:49 +000095 if self.alive:
96 self.alive = False
97 self.thread_read.join()
cliechtid9a06ce2009-08-10 01:30:53 +000098 self.thread_poll.join()
cliechtief39b8b2009-08-07 18:22:49 +000099
100
101if __name__ == '__main__':
Chris Liechti34290f42015-08-17 03:08:24 +0200102 import argparse
cliechtief39b8b2009-08-07 18:22:49 +0000103
Chris Liechti34290f42015-08-17 03:08:24 +0200104 parser = argparse.ArgumentParser(
cliechtief39b8b2009-08-07 18:22:49 +0000105 description = "RFC 2217 Serial to Network (TCP/IP) redirector.",
106 epilog = """\
107NOTE: no security measures are implemented. Anyone can remotely connect
108to this service over the network.
109
110Only one connection at once is supported. When the connection is terminated
111it waits for the next connect.
112""")
113
Chris Liechti34290f42015-08-17 03:08:24 +0200114 parser.add_argument('SERIALPORT')
cliechtief39b8b2009-08-07 18:22:49 +0000115
Chris Liechti34290f42015-08-17 03:08:24 +0200116 parser.add_argument('-p', '--localport',
117 type=int,
118 help='local TCP port, default: %(default)s',
119 metavar='TCPPORT',
120 default=2217
121 )
cliechti5cc3eb12009-08-11 23:04:30 +0000122
Chris Liechti34290f42015-08-17 03:08:24 +0200123 parser.add_argument('-v', '--verbose',
124 dest='verbosity',
125 action='count',
126 help='print more diagnostic messages (option can be given multiple times)',
127 default=0
128 )
cliechtief39b8b2009-08-07 18:22:49 +0000129
Chris Liechti34290f42015-08-17 03:08:24 +0200130 args = parser.parse_args()
cliechtief39b8b2009-08-07 18:22:49 +0000131
Chris Liechti34290f42015-08-17 03:08:24 +0200132 if args.verbosity > 3:
133 args.verbosity = 3
cliechti5cc3eb12009-08-11 23:04:30 +0000134 level = (
Chris Liechti34290f42015-08-17 03:08:24 +0200135 logging.WARNING,
136 logging.INFO,
137 logging.DEBUG,
138 logging.NOTSET,
139 )[args.verbosity]
cliechti5cc3eb12009-08-11 23:04:30 +0000140 logging.basicConfig(level=logging.INFO)
cliechtic64ba692009-08-12 00:32:47 +0000141 logging.getLogger('root').setLevel(logging.INFO)
cliechti5cc3eb12009-08-11 23:04:30 +0000142 logging.getLogger('rfc2217').setLevel(level)
143
cliechtief39b8b2009-08-07 18:22:49 +0000144 # connect to serial port
Chris Liechticd42db92015-08-17 03:24:34 +0200145 ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True)
cliechtid9a06ce2009-08-10 01:30:53 +0000146 ser.timeout = 3 # required so that the reader thread can exit
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:
153 logging.error("Could not open serial port %s: %s" % (ser.name, e))
cliechtief39b8b2009-08-07 18:22:49 +0000154 sys.exit(1)
155
Chris Liechti34290f42015-08-17 03:08:24 +0200156 logging.info("Serving serial port: %s" % (ser.name,))
cliechtid9a06ce2009-08-10 01:30:53 +0000157 settings = ser.getSettingsDict()
cliechtie542b362011-03-18 00:49:16 +0000158 # reset control line as no _remote_ "terminal" has been connected yet
cliechtid9a06ce2009-08-10 01:30:53 +0000159 ser.setDTR(False)
160 ser.setRTS(False)
cliechtief39b8b2009-08-07 18:22:49 +0000161
162 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
163 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Chris Liechti34290f42015-08-17 03:08:24 +0200164 srv.bind(('', args.localport))
cliechtief39b8b2009-08-07 18:22:49 +0000165 srv.listen(1)
Chris Liechti34290f42015-08-17 03:08:24 +0200166 logging.info("TCP/IP port: %s" % (args.localport,))
cliechtief39b8b2009-08-07 18:22:49 +0000167 while True:
168 try:
169 connection, addr = srv.accept()
cliechti5cc3eb12009-08-11 23:04:30 +0000170 logging.info('Connected by %s:%s' % (addr[0], addr[1]))
Chris Liechti34290f42015-08-17 03:08:24 +0200171 connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
cliechtid9a06ce2009-08-10 01:30:53 +0000172 ser.setRTS(True)
173 ser.setDTR(True)
174 # enter network <-> serial loop
cliechtief39b8b2009-08-07 18:22:49 +0000175 r = Redirector(
Chris Liechti34290f42015-08-17 03:08:24 +0200176 ser,
177 connection,
178 args.verbosity > 0
179 )
cliechtid9a06ce2009-08-10 01:30:53 +0000180 try:
Chris Liechti34290f42015-08-17 03:08:24 +0200181 r.shortcircuit()
cliechtid9a06ce2009-08-10 01:30:53 +0000182 finally:
cliechti5cc3eb12009-08-11 23:04:30 +0000183 logging.info('Disconnected')
cliechtid9a06ce2009-08-10 01:30:53 +0000184 r.stop()
185 connection.close()
186 ser.setDTR(False)
187 ser.setRTS(False)
Chris Liechti34290f42015-08-17 03:08:24 +0200188 # Restore port settings (may have been changed by RFC 2217
189 # capable client)
190 ser.applySettingsDict(settings)
cliechtief39b8b2009-08-07 18:22:49 +0000191 except KeyboardInterrupt:
192 break
Chris Liechti4caf6a52015-08-04 01:07:45 +0200193 except socket.error as msg:
cliechti5cc3eb12009-08-11 23:04:30 +0000194 logging.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +0000195
cliechti5cc3eb12009-08-11 23:04:30 +0000196 logging.info('--- exit ---')