blob: a8685f480508a1039083ac64c2e39175da85ba91 [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
145 ser = serial.Serial()
Chris Liechti34290f42015-08-17 03:08:24 +0200146 ser.port = args.SERIALPORT
cliechtid9a06ce2009-08-10 01:30:53 +0000147 ser.timeout = 3 # required so that the reader thread can exit
cliechtief39b8b2009-08-07 18:22:49 +0000148
cliechti5cc3eb12009-08-11 23:04:30 +0000149 logging.info("RFC 2217 TCP/IP to Serial redirector - type Ctrl-C / BREAK to quit")
cliechtief39b8b2009-08-07 18:22:49 +0000150
151 try:
152 ser.open()
Chris Liechti34290f42015-08-17 03:08:24 +0200153 except serial.SerialException as e:
154 logging.error("Could not open serial port %s: %s" % (ser.name, e))
cliechtief39b8b2009-08-07 18:22:49 +0000155 sys.exit(1)
156
Chris Liechti34290f42015-08-17 03:08:24 +0200157 logging.info("Serving serial port: %s" % (ser.name,))
cliechtid9a06ce2009-08-10 01:30:53 +0000158 settings = ser.getSettingsDict()
cliechtie542b362011-03-18 00:49:16 +0000159 # reset control line as no _remote_ "terminal" has been connected yet
cliechtid9a06ce2009-08-10 01:30:53 +0000160 ser.setDTR(False)
161 ser.setRTS(False)
cliechtief39b8b2009-08-07 18:22:49 +0000162
163 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
164 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Chris Liechti34290f42015-08-17 03:08:24 +0200165 srv.bind(('', args.localport))
cliechtief39b8b2009-08-07 18:22:49 +0000166 srv.listen(1)
Chris Liechti34290f42015-08-17 03:08:24 +0200167 logging.info("TCP/IP port: %s" % (args.localport,))
cliechtief39b8b2009-08-07 18:22:49 +0000168 while True:
169 try:
170 connection, addr = srv.accept()
cliechti5cc3eb12009-08-11 23:04:30 +0000171 logging.info('Connected by %s:%s' % (addr[0], addr[1]))
Chris Liechti34290f42015-08-17 03:08:24 +0200172 connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
cliechtid9a06ce2009-08-10 01:30:53 +0000173 ser.setRTS(True)
174 ser.setDTR(True)
175 # enter network <-> serial loop
cliechtief39b8b2009-08-07 18:22:49 +0000176 r = Redirector(
Chris Liechti34290f42015-08-17 03:08:24 +0200177 ser,
178 connection,
179 args.verbosity > 0
180 )
cliechtid9a06ce2009-08-10 01:30:53 +0000181 try:
Chris Liechti34290f42015-08-17 03:08:24 +0200182 r.shortcircuit()
cliechtid9a06ce2009-08-10 01:30:53 +0000183 finally:
cliechti5cc3eb12009-08-11 23:04:30 +0000184 logging.info('Disconnected')
cliechtid9a06ce2009-08-10 01:30:53 +0000185 r.stop()
186 connection.close()
187 ser.setDTR(False)
188 ser.setRTS(False)
Chris Liechti34290f42015-08-17 03:08:24 +0200189 # Restore port settings (may have been changed by RFC 2217
190 # capable client)
191 ser.applySettingsDict(settings)
cliechtief39b8b2009-08-07 18:22:49 +0000192 except KeyboardInterrupt:
193 break
Chris Liechti4caf6a52015-08-04 01:07:45 +0200194 except socket.error as msg:
cliechti5cc3eb12009-08-11 23:04:30 +0000195 logging.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +0000196
cliechti5cc3eb12009-08-11 23:04:30 +0000197 logging.info('--- exit ---')