blob: 886925af20ef303624d0b40a39f3e578300043b0 [file] [log] [blame]
cliechtief39b8b2009-08-07 18:22:49 +00001#!/usr/bin/env python
2
3# (C) 2009 Chris Liechti <cliechti@gmx.net>
4# redirect data from a TCP/IP connection to a serial port and vice versa
5# using RFC 2217
6
7
Chris Liechti4caf6a52015-08-04 01:07:45 +02008import logging
cliechtief39b8b2009-08-07 18:22:49 +00009import os
cliechtief39b8b2009-08-07 18:22:49 +000010import socket
Chris Liechti4caf6a52015-08-04 01:07:45 +020011import sys
12import time
13import threading
cliechtief39b8b2009-08-07 18:22:49 +000014import serial
15import serial.rfc2217
16
Chris Liechti4caf6a52015-08-04 01:07:45 +020017class Redirector(object):
cliechti5cc3eb12009-08-11 23:04:30 +000018 def __init__(self, serial_instance, socket, debug=None):
cliechtief39b8b2009-08-07 18:22:49 +000019 self.serial = serial_instance
20 self.socket = socket
21 self._write_lock = threading.Lock()
cliechti5cc3eb12009-08-11 23:04:30 +000022 self.rfc2217 = serial.rfc2217.PortManager(
23 self.serial,
24 self,
cliechti6a300772009-08-12 02:28:56 +000025 logger = (debug and logging.getLogger('rfc2217.server'))
cliechti5cc3eb12009-08-11 23:04:30 +000026 )
27 self.log = logging.getLogger('redirector')
cliechtief39b8b2009-08-07 18:22:49 +000028
29 def statusline_poller(self):
cliechti5cc3eb12009-08-11 23:04:30 +000030 self.log.debug('status line poll thread started')
cliechtief39b8b2009-08-07 18:22:49 +000031 while self.alive:
32 time.sleep(1)
33 self.rfc2217.check_modem_lines()
cliechti5cc3eb12009-08-11 23:04:30 +000034 self.log.debug('status line poll thread terminated')
cliechtief39b8b2009-08-07 18:22:49 +000035
36 def shortcut(self):
37 """connect the serial port to the TCP port by copying everything
38 from one side to the other"""
39 self.alive = True
40 self.thread_read = threading.Thread(target=self.reader)
41 self.thread_read.setDaemon(True)
42 self.thread_read.setName('serial->socket')
43 self.thread_read.start()
44 self.thread_poll = threading.Thread(target=self.statusline_poller)
45 self.thread_poll.setDaemon(True)
46 self.thread_poll.setName('status line poll')
47 self.thread_poll.start()
48 self.writer()
49
50 def reader(self):
51 """loop forever and copy serial->socket"""
cliechti5cc3eb12009-08-11 23:04:30 +000052 self.log.debug('reader thread started')
cliechtief39b8b2009-08-07 18:22:49 +000053 while self.alive:
54 try:
55 data = self.serial.read(1) # read one, blocking
56 n = self.serial.inWaiting() # look if there is more
57 if n:
58 data = data + self.serial.read(n) # and get as much as possible
59 if data:
60 # escape outgoing data when needed (Telnet IAC (0xff) character)
61 data = serial.to_bytes(self.rfc2217.escape(data))
Chris Liechti4caf6a52015-08-04 01:07:45 +020062 with self._write_lock:
cliechtief39b8b2009-08-07 18:22:49 +000063 self.socket.sendall(data) # send it over TCP
Chris Liechti4caf6a52015-08-04 01:07:45 +020064 except socket.error as msg:
cliechti5cc3eb12009-08-11 23:04:30 +000065 self.log.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +000066 # probably got disconnected
67 break
68 self.alive = False
cliechti5cc3eb12009-08-11 23:04:30 +000069 self.log.debug('reader thread terminated')
cliechtief39b8b2009-08-07 18:22:49 +000070
71 def write(self, data):
72 """thread safe socket write with no data escaping. used to send telnet stuff"""
Chris Liechti4caf6a52015-08-04 01:07:45 +020073 with self._write_lock:
cliechtief39b8b2009-08-07 18:22:49 +000074 self.socket.sendall(data)
cliechtief39b8b2009-08-07 18:22:49 +000075
76 def writer(self):
77 """loop forever and copy socket->serial"""
78 while self.alive:
79 try:
80 data = self.socket.recv(1024)
81 if not data:
82 break
83 self.serial.write(serial.to_bytes(self.rfc2217.filter(data)))
Chris Liechti4caf6a52015-08-04 01:07:45 +020084 except socket.error as msg:
cliechti5cc3eb12009-08-11 23:04:30 +000085 self.log.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +000086 # probably got disconnected
87 break
cliechtid9a06ce2009-08-10 01:30:53 +000088 self.stop()
cliechtief39b8b2009-08-07 18:22:49 +000089
90 def stop(self):
91 """Stop copying"""
cliechti5cc3eb12009-08-11 23:04:30 +000092 self.log.debug('stopping')
cliechtief39b8b2009-08-07 18:22:49 +000093 if self.alive:
94 self.alive = False
95 self.thread_read.join()
cliechtid9a06ce2009-08-10 01:30:53 +000096 self.thread_poll.join()
cliechtief39b8b2009-08-07 18:22:49 +000097
98
99if __name__ == '__main__':
100 import optparse
101
102 parser = optparse.OptionParser(
cliechti3453f122009-08-10 22:56:34 +0000103 usage = "%prog [options] port",
cliechtief39b8b2009-08-07 18:22:49 +0000104 description = "RFC 2217 Serial to Network (TCP/IP) redirector.",
105 epilog = """\
106NOTE: no security measures are implemented. Anyone can remotely connect
107to this service over the network.
108
109Only one connection at once is supported. When the connection is terminated
110it waits for the next connect.
111""")
112
113 parser.add_option("-p", "--localport",
114 dest = "local_port",
115 action = "store",
116 type = 'int',
117 help = "local TCP port",
118 default = 2217
119 )
120
cliechti5cc3eb12009-08-11 23:04:30 +0000121 parser.add_option("-v", "--verbose",
122 dest = "verbosity",
123 action = "count",
124 help = "print more diagnostic messages (option can be given multiple times)",
125 default = 0
126 )
127
cliechtief39b8b2009-08-07 18:22:49 +0000128 (options, args) = parser.parse_args()
129
130 if len(args) != 1:
131 parser.error('serial port name required as argument')
132
cliechti5cc3eb12009-08-11 23:04:30 +0000133 if options.verbosity > 3:
134 options.verbosity = 3
135 level = (
136 logging.WARNING,
137 logging.INFO,
138 logging.DEBUG,
139 logging.NOTSET,
140 )[options.verbosity]
141 logging.basicConfig(level=logging.INFO)
cliechtic64ba692009-08-12 00:32:47 +0000142 logging.getLogger('root').setLevel(logging.INFO)
cliechti5cc3eb12009-08-11 23:04:30 +0000143 logging.getLogger('rfc2217').setLevel(level)
144
cliechtief39b8b2009-08-07 18:22:49 +0000145 # connect to serial port
146 ser = serial.Serial()
147 ser.port = args[0]
cliechtid9a06ce2009-08-10 01:30:53 +0000148 ser.timeout = 3 # required so that the reader thread can exit
cliechtief39b8b2009-08-07 18:22:49 +0000149
cliechti5cc3eb12009-08-11 23:04:30 +0000150 logging.info("RFC 2217 TCP/IP to Serial redirector - type Ctrl-C / BREAK to quit")
cliechtief39b8b2009-08-07 18:22:49 +0000151
152 try:
153 ser.open()
154 except serial.SerialException, e:
cliechti5cc3eb12009-08-11 23:04:30 +0000155 logging.error("Could not open serial port %s: %s" % (ser.portstr, e))
cliechtief39b8b2009-08-07 18:22:49 +0000156 sys.exit(1)
157
cliechti5cc3eb12009-08-11 23:04:30 +0000158 logging.info("Serving serial port: %s" % (ser.portstr,))
cliechtid9a06ce2009-08-10 01:30:53 +0000159 settings = ser.getSettingsDict()
cliechtie542b362011-03-18 00:49:16 +0000160 # reset control line as no _remote_ "terminal" has been connected yet
cliechtid9a06ce2009-08-10 01:30:53 +0000161 ser.setDTR(False)
162 ser.setRTS(False)
cliechtief39b8b2009-08-07 18:22:49 +0000163
164 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
165 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
166 srv.bind( ('', options.local_port) )
167 srv.listen(1)
cliechti5cc3eb12009-08-11 23:04:30 +0000168 logging.info("TCP/IP port: %s" % (options.local_port,))
cliechtief39b8b2009-08-07 18:22:49 +0000169 while True:
170 try:
171 connection, addr = srv.accept()
cliechti5cc3eb12009-08-11 23:04:30 +0000172 logging.info('Connected by %s:%s' % (addr[0], addr[1]))
cliechtid9a06ce2009-08-10 01:30:53 +0000173 connection.setsockopt( socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
174 ser.setRTS(True)
175 ser.setDTR(True)
176 # enter network <-> serial loop
cliechtief39b8b2009-08-07 18:22:49 +0000177 r = Redirector(
178 ser,
179 connection,
cliechti5cc3eb12009-08-11 23:04:30 +0000180 options.verbosity > 0
cliechtief39b8b2009-08-07 18:22:49 +0000181 )
cliechtid9a06ce2009-08-10 01:30:53 +0000182 try:
183 r.shortcut()
184 finally:
cliechti5cc3eb12009-08-11 23:04:30 +0000185 logging.info('Disconnected')
cliechtid9a06ce2009-08-10 01:30:53 +0000186 r.stop()
187 connection.close()
188 ser.setDTR(False)
189 ser.setRTS(False)
190 # Restore port settings (may have been changed by RFC 2217 capable
191 # client)
192 ser.applySettingsDict(settings)
cliechtief39b8b2009-08-07 18:22:49 +0000193 except KeyboardInterrupt:
194 break
Chris Liechti4caf6a52015-08-04 01:07:45 +0200195 except socket.error as msg:
cliechti5cc3eb12009-08-11 23:04:30 +0000196 logging.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +0000197
cliechti5cc3eb12009-08-11 23:04:30 +0000198 logging.info('--- exit ---')