blob: 069900a0bcfda56f3d8c44d055069c04ca24534c [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
8import sys
9import os
10import threading
11import time
12import socket
13import serial
14import serial.rfc2217
cliechti5cc3eb12009-08-11 23:04:30 +000015import logging
cliechtief39b8b2009-08-07 18:22:49 +000016
17class Redirector:
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))
62 self._write_lock.acquire()
63 try:
64 self.socket.sendall(data) # send it over TCP
65 finally:
66 self._write_lock.release()
67 except socket.error, msg:
cliechti5cc3eb12009-08-11 23:04:30 +000068 self.log.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +000069 # probably got disconnected
70 break
71 self.alive = False
cliechti5cc3eb12009-08-11 23:04:30 +000072 self.log.debug('reader thread terminated')
cliechtief39b8b2009-08-07 18:22:49 +000073
74 def write(self, data):
75 """thread safe socket write with no data escaping. used to send telnet stuff"""
76 self._write_lock.acquire()
77 try:
78 self.socket.sendall(data)
79 finally:
80 self._write_lock.release()
81
82 def writer(self):
83 """loop forever and copy socket->serial"""
84 while self.alive:
85 try:
86 data = self.socket.recv(1024)
87 if not data:
88 break
89 self.serial.write(serial.to_bytes(self.rfc2217.filter(data)))
90 except socket.error, msg:
cliechti5cc3eb12009-08-11 23:04:30 +000091 self.log.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +000092 # probably got disconnected
93 break
cliechtid9a06ce2009-08-10 01:30:53 +000094 self.stop()
cliechtief39b8b2009-08-07 18:22:49 +000095
96 def stop(self):
97 """Stop copying"""
cliechti5cc3eb12009-08-11 23:04:30 +000098 self.log.debug('stopping')
cliechtief39b8b2009-08-07 18:22:49 +000099 if self.alive:
100 self.alive = False
101 self.thread_read.join()
cliechtid9a06ce2009-08-10 01:30:53 +0000102 self.thread_poll.join()
cliechtief39b8b2009-08-07 18:22:49 +0000103
104
105if __name__ == '__main__':
106 import optparse
107
108 parser = optparse.OptionParser(
cliechti3453f122009-08-10 22:56:34 +0000109 usage = "%prog [options] port",
cliechtief39b8b2009-08-07 18:22:49 +0000110 description = "RFC 2217 Serial to Network (TCP/IP) redirector.",
111 epilog = """\
112NOTE: no security measures are implemented. Anyone can remotely connect
113to this service over the network.
114
115Only one connection at once is supported. When the connection is terminated
116it waits for the next connect.
117""")
118
119 parser.add_option("-p", "--localport",
120 dest = "local_port",
121 action = "store",
122 type = 'int',
123 help = "local TCP port",
124 default = 2217
125 )
126
cliechti5cc3eb12009-08-11 23:04:30 +0000127 parser.add_option("-v", "--verbose",
128 dest = "verbosity",
129 action = "count",
130 help = "print more diagnostic messages (option can be given multiple times)",
131 default = 0
132 )
133
cliechtief39b8b2009-08-07 18:22:49 +0000134 (options, args) = parser.parse_args()
135
136 if len(args) != 1:
137 parser.error('serial port name required as argument')
138
cliechti5cc3eb12009-08-11 23:04:30 +0000139 if options.verbosity > 3:
140 options.verbosity = 3
141 level = (
142 logging.WARNING,
143 logging.INFO,
144 logging.DEBUG,
145 logging.NOTSET,
146 )[options.verbosity]
147 logging.basicConfig(level=logging.INFO)
cliechtic64ba692009-08-12 00:32:47 +0000148 logging.getLogger('root').setLevel(logging.INFO)
cliechti5cc3eb12009-08-11 23:04:30 +0000149 logging.getLogger('rfc2217').setLevel(level)
150
cliechtief39b8b2009-08-07 18:22:49 +0000151 # connect to serial port
152 ser = serial.Serial()
153 ser.port = args[0]
cliechtid9a06ce2009-08-10 01:30:53 +0000154 ser.timeout = 3 # required so that the reader thread can exit
cliechtief39b8b2009-08-07 18:22:49 +0000155
cliechti5cc3eb12009-08-11 23:04:30 +0000156 logging.info("RFC 2217 TCP/IP to Serial redirector - type Ctrl-C / BREAK to quit")
cliechtief39b8b2009-08-07 18:22:49 +0000157
158 try:
159 ser.open()
160 except serial.SerialException, e:
cliechti5cc3eb12009-08-11 23:04:30 +0000161 logging.error("Could not open serial port %s: %s" % (ser.portstr, e))
cliechtief39b8b2009-08-07 18:22:49 +0000162 sys.exit(1)
163
cliechti5cc3eb12009-08-11 23:04:30 +0000164 logging.info("Serving serial port: %s" % (ser.portstr,))
cliechtid9a06ce2009-08-10 01:30:53 +0000165 settings = ser.getSettingsDict()
cliechtie542b362011-03-18 00:49:16 +0000166 # reset control line as no _remote_ "terminal" has been connected yet
cliechtid9a06ce2009-08-10 01:30:53 +0000167 ser.setDTR(False)
168 ser.setRTS(False)
cliechtief39b8b2009-08-07 18:22:49 +0000169
170 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
171 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
172 srv.bind( ('', options.local_port) )
173 srv.listen(1)
cliechti5cc3eb12009-08-11 23:04:30 +0000174 logging.info("TCP/IP port: %s" % (options.local_port,))
cliechtief39b8b2009-08-07 18:22:49 +0000175 while True:
176 try:
177 connection, addr = srv.accept()
cliechti5cc3eb12009-08-11 23:04:30 +0000178 logging.info('Connected by %s:%s' % (addr[0], addr[1]))
cliechtid9a06ce2009-08-10 01:30:53 +0000179 connection.setsockopt( socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
180 ser.setRTS(True)
181 ser.setDTR(True)
182 # enter network <-> serial loop
cliechtief39b8b2009-08-07 18:22:49 +0000183 r = Redirector(
184 ser,
185 connection,
cliechti5cc3eb12009-08-11 23:04:30 +0000186 options.verbosity > 0
cliechtief39b8b2009-08-07 18:22:49 +0000187 )
cliechtid9a06ce2009-08-10 01:30:53 +0000188 try:
189 r.shortcut()
190 finally:
cliechti5cc3eb12009-08-11 23:04:30 +0000191 logging.info('Disconnected')
cliechtid9a06ce2009-08-10 01:30:53 +0000192 r.stop()
193 connection.close()
194 ser.setDTR(False)
195 ser.setRTS(False)
196 # Restore port settings (may have been changed by RFC 2217 capable
197 # client)
198 ser.applySettingsDict(settings)
cliechtief39b8b2009-08-07 18:22:49 +0000199 except KeyboardInterrupt:
200 break
201 except socket.error, msg:
cliechti5cc3eb12009-08-11 23:04:30 +0000202 logging.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +0000203
cliechti5cc3eb12009-08-11 23:04:30 +0000204 logging.info('--- exit ---')