blob: d5ea771396690bc25c4a45aced0a7531f310de80 [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,
25 debug_output = (debug and logging.getLogger('rfc2217.server'))
26 )
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)
148 logging.getLogger('rfc2217').setLevel(level)
149
cliechtief39b8b2009-08-07 18:22:49 +0000150 # connect to serial port
151 ser = serial.Serial()
152 ser.port = args[0]
cliechtid9a06ce2009-08-10 01:30:53 +0000153 ser.timeout = 3 # required so that the reader thread can exit
cliechtief39b8b2009-08-07 18:22:49 +0000154
cliechti5cc3eb12009-08-11 23:04:30 +0000155 logging.info("RFC 2217 TCP/IP to Serial redirector - type Ctrl-C / BREAK to quit")
cliechtief39b8b2009-08-07 18:22:49 +0000156
157 try:
158 ser.open()
159 except serial.SerialException, e:
cliechti5cc3eb12009-08-11 23:04:30 +0000160 logging.error("Could not open serial port %s: %s" % (ser.portstr, e))
cliechtief39b8b2009-08-07 18:22:49 +0000161 sys.exit(1)
162
cliechti5cc3eb12009-08-11 23:04:30 +0000163 logging.info("Serving serial port: %s" % (ser.portstr,))
cliechtid9a06ce2009-08-10 01:30:53 +0000164 settings = ser.getSettingsDict()
165 # reset contol line as no _remote_ "terminal" has been connected yet
166 ser.setDTR(False)
167 ser.setRTS(False)
cliechtief39b8b2009-08-07 18:22:49 +0000168
169 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
170 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
171 srv.bind( ('', options.local_port) )
172 srv.listen(1)
cliechti5cc3eb12009-08-11 23:04:30 +0000173 logging.info("TCP/IP port: %s" % (options.local_port,))
cliechtief39b8b2009-08-07 18:22:49 +0000174 while True:
175 try:
176 connection, addr = srv.accept()
cliechti5cc3eb12009-08-11 23:04:30 +0000177 logging.info('Connected by %s:%s' % (addr[0], addr[1]))
cliechtid9a06ce2009-08-10 01:30:53 +0000178 connection.setsockopt( socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
179 ser.setRTS(True)
180 ser.setDTR(True)
181 # enter network <-> serial loop
cliechtief39b8b2009-08-07 18:22:49 +0000182 r = Redirector(
183 ser,
184 connection,
cliechti5cc3eb12009-08-11 23:04:30 +0000185 options.verbosity > 0
cliechtief39b8b2009-08-07 18:22:49 +0000186 )
cliechtid9a06ce2009-08-10 01:30:53 +0000187 try:
188 r.shortcut()
189 finally:
cliechti5cc3eb12009-08-11 23:04:30 +0000190 logging.info('Disconnected')
cliechtid9a06ce2009-08-10 01:30:53 +0000191 r.stop()
192 connection.close()
193 ser.setDTR(False)
194 ser.setRTS(False)
195 # Restore port settings (may have been changed by RFC 2217 capable
196 # client)
197 ser.applySettingsDict(settings)
cliechtief39b8b2009-08-07 18:22:49 +0000198 except KeyboardInterrupt:
199 break
200 except socket.error, msg:
cliechti5cc3eb12009-08-11 23:04:30 +0000201 logging.error('%s' % (msg,))
cliechtief39b8b2009-08-07 18:22:49 +0000202
cliechti5cc3eb12009-08-11 23:04:30 +0000203 logging.info('--- exit ---')