blob: fb8962798fb41b1284eec07a21bc915856715587 [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#
6# (C) 2009 Chris Liechti <cliechti@gmx.net>
7#
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):
cliechti5cc3eb12009-08-11 23:04:30 +000020 def __init__(self, serial_instance, socket, debug=None):
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(
25 self.serial,
26 self,
cliechti6a300772009-08-12 02:28:56 +000027 logger = (debug and logging.getLogger('rfc2217.server'))
cliechti5cc3eb12009-08-11 23:04:30 +000028 )
29 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
38 def shortcut(self):
39 """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__':
102 import optparse
103
104 parser = optparse.OptionParser(
cliechti3453f122009-08-10 22:56:34 +0000105 usage = "%prog [options] port",
cliechtief39b8b2009-08-07 18:22:49 +0000106 description = "RFC 2217 Serial to Network (TCP/IP) redirector.",
107 epilog = """\
108NOTE: no security measures are implemented. Anyone can remotely connect
109to this service over the network.
110
111Only one connection at once is supported. When the connection is terminated
112it waits for the next connect.
113""")
114
115 parser.add_option("-p", "--localport",
116 dest = "local_port",
117 action = "store",
118 type = 'int',
119 help = "local TCP port",
120 default = 2217
121 )
122
cliechti5cc3eb12009-08-11 23:04:30 +0000123 parser.add_option("-v", "--verbose",
124 dest = "verbosity",
125 action = "count",
126 help = "print more diagnostic messages (option can be given multiple times)",
127 default = 0
128 )
129
cliechtief39b8b2009-08-07 18:22:49 +0000130 (options, args) = parser.parse_args()
131
132 if len(args) != 1:
133 parser.error('serial port name required as argument')
134
cliechti5cc3eb12009-08-11 23:04:30 +0000135 if options.verbosity > 3:
136 options.verbosity = 3
137 level = (
138 logging.WARNING,
139 logging.INFO,
140 logging.DEBUG,
141 logging.NOTSET,
142 )[options.verbosity]
143 logging.basicConfig(level=logging.INFO)
cliechtic64ba692009-08-12 00:32:47 +0000144 logging.getLogger('root').setLevel(logging.INFO)
cliechti5cc3eb12009-08-11 23:04:30 +0000145 logging.getLogger('rfc2217').setLevel(level)
146
cliechtief39b8b2009-08-07 18:22:49 +0000147 # connect to serial port
148 ser = serial.Serial()
149 ser.port = args[0]
cliechtid9a06ce2009-08-10 01:30:53 +0000150 ser.timeout = 3 # required so that the reader thread can exit
cliechtief39b8b2009-08-07 18:22:49 +0000151
cliechti5cc3eb12009-08-11 23:04:30 +0000152 logging.info("RFC 2217 TCP/IP to Serial redirector - type Ctrl-C / BREAK to quit")
cliechtief39b8b2009-08-07 18:22:49 +0000153
154 try:
155 ser.open()
156 except serial.SerialException, e:
cliechti5cc3eb12009-08-11 23:04:30 +0000157 logging.error("Could not open serial port %s: %s" % (ser.portstr, e))
cliechtief39b8b2009-08-07 18:22:49 +0000158 sys.exit(1)
159
cliechti5cc3eb12009-08-11 23:04:30 +0000160 logging.info("Serving serial port: %s" % (ser.portstr,))
cliechtid9a06ce2009-08-10 01:30:53 +0000161 settings = ser.getSettingsDict()
cliechtie542b362011-03-18 00:49:16 +0000162 # reset control line as no _remote_ "terminal" has been connected yet
cliechtid9a06ce2009-08-10 01:30:53 +0000163 ser.setDTR(False)
164 ser.setRTS(False)
cliechtief39b8b2009-08-07 18:22:49 +0000165
166 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
167 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
168 srv.bind( ('', options.local_port) )
169 srv.listen(1)
cliechti5cc3eb12009-08-11 23:04:30 +0000170 logging.info("TCP/IP port: %s" % (options.local_port,))
cliechtief39b8b2009-08-07 18:22:49 +0000171 while True:
172 try:
173 connection, addr = srv.accept()
cliechti5cc3eb12009-08-11 23:04:30 +0000174 logging.info('Connected by %s:%s' % (addr[0], addr[1]))
cliechtid9a06ce2009-08-10 01:30:53 +0000175 connection.setsockopt( socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
176 ser.setRTS(True)
177 ser.setDTR(True)
178 # enter network <-> serial loop
cliechtief39b8b2009-08-07 18:22:49 +0000179 r = Redirector(
180 ser,
181 connection,
cliechti5cc3eb12009-08-11 23:04:30 +0000182 options.verbosity > 0
cliechtief39b8b2009-08-07 18:22:49 +0000183 )
cliechtid9a06ce2009-08-10 01:30:53 +0000184 try:
185 r.shortcut()
186 finally:
cliechti5cc3eb12009-08-11 23:04:30 +0000187 logging.info('Disconnected')
cliechtid9a06ce2009-08-10 01:30:53 +0000188 r.stop()
189 connection.close()
190 ser.setDTR(False)
191 ser.setRTS(False)
192 # Restore port settings (may have been changed by RFC 2217 capable
193 # client)
194 ser.applySettingsDict(settings)
cliechtief39b8b2009-08-07 18:22:49 +0000195 except KeyboardInterrupt:
196 break
Chris Liechti4caf6a52015-08-04 01:07:45 +0200197 except socket.error as msg:
cliechti5cc3eb12009-08-11 23:04:30 +0000198 logging.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +0000199
cliechti5cc3eb12009-08-11 23:04:30 +0000200 logging.info('--- exit ---')