blob: 393beb5d908ccfb9e2d612b1c76d3eef70e8b08d [file] [log] [blame]
cliechtif1c882c2009-07-23 02:30:06 +00001#! /usr/bin/env python
2"""\
3Multi-port serial<->TCP/IP forwarder.
cliechti32c10332009-08-05 13:23:43 +00004- RFC 2217
cliechtif1c882c2009-07-23 02:30:06 +00005- check existence of serial port periodically
6- start/stop forwarders
7- each forwarder creates a server socket and opens the serial port
8- serial ports are opened only once. network connect/disconnect
9 does not influence serial port
10- only one client per connection
11"""
Chris Liechti4caf6a52015-08-04 01:07:45 +020012import os
cliechtif1c882c2009-07-23 02:30:06 +000013import select
Chris Liechti4caf6a52015-08-04 01:07:45 +020014import socket
15import sys
16import time
17import traceback
cliechti32c10332009-08-05 13:23:43 +000018
cliechtif1c882c2009-07-23 02:30:06 +000019import serial
cliechti32c10332009-08-05 13:23:43 +000020import serial.rfc2217
21
cliechtif1c882c2009-07-23 02:30:06 +000022import avahi
23import dbus
24
25class ZeroconfService:
26 """\
27 A simple class to publish a network service with zeroconf using avahi.
28 """
29
30 def __init__(self, name, port, stype="_http._tcp",
31 domain="", host="", text=""):
32 self.name = name
33 self.stype = stype
34 self.domain = domain
35 self.host = host
36 self.port = port
37 self.text = text
38 self.group = None
39
40 def publish(self):
41 bus = dbus.SystemBus()
42 server = dbus.Interface(
43 bus.get_object(
44 avahi.DBUS_NAME,
45 avahi.DBUS_PATH_SERVER
46 ),
47 avahi.DBUS_INTERFACE_SERVER
48 )
49
50 g = dbus.Interface(
51 bus.get_object(
52 avahi.DBUS_NAME,
53 server.EntryGroupNew()
54 ),
55 avahi.DBUS_INTERFACE_ENTRY_GROUP
56 )
57
58 g.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, dbus.UInt32(0),
59 self.name, self.stype, self.domain, self.host,
60 dbus.UInt16(self.port), self.text)
61
62 g.Commit()
63 self.group = g
64
65 def unpublish(self):
66 if self.group is not None:
67 self.group.Reset()
68 self.group = None
69
70 def __str__(self):
71 return "%r @ %s:%s (%s)" % (self.name, self.host, self.port, self.stype)
72
73
74
75class Forwarder(ZeroconfService):
76 """\
77 Single port serial<->TCP/IP forarder that depends on an external select
cliechti32c10332009-08-05 13:23:43 +000078 loop.
79 - Buffers for serial -> network and network -> serial
80 - RFC 2217 state
81 - Zeroconf publish/unpublish on open/close.
cliechtif1c882c2009-07-23 02:30:06 +000082 """
83
84 def __init__(self, device, name, network_port, on_close=None):
85 ZeroconfService.__init__(self, name, network_port, stype='_serial_port._tcp')
86 self.alive = False
87 self.network_port = network_port
88 self.on_close = on_close
89 self.device = device
90 self.serial = serial.Serial()
91 self.serial.port = device
92 self.serial.baudrate = 115200
93 self.serial.timeout = 0
94 self.socket = None
95 self.server_socket = None
cliechti32c10332009-08-05 13:23:43 +000096 self.rfc2217 = None # instantiate later, when connecting
cliechtif1c882c2009-07-23 02:30:06 +000097
98 def __del__(self):
99 try:
100 if self.alive: self.close()
101 except:
102 pass # XXX errors on shutdown
103
104 def open(self):
105 """open serial port, start network server and publish service"""
106 self.buffer_net2ser = ''
107 self.buffer_ser2net = ''
108
109 # open serial port
110 try:
111 self.serial.open()
112 self.serial.setRTS(False)
Chris Liechti4caf6a52015-08-04 01:07:45 +0200113 except Exception as msg:
cliechtif1c882c2009-07-23 02:30:06 +0000114 self.handle_serial_error(msg)
115
cliechtid9a06ce2009-08-10 01:30:53 +0000116 self.serial_settings_backup = self.serial.getSettingsDict()
117
cliechtif1c882c2009-07-23 02:30:06 +0000118 # start the socket server
119 self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
120 self.server_socket.setsockopt(
121 socket.SOL_SOCKET,
122 socket.SO_REUSEADDR,
123 self.server_socket.getsockopt(
124 socket.SOL_SOCKET,
125 socket.SO_REUSEADDR
126 ) | 1
127 )
128 self.server_socket.setblocking(0)
129 try:
130 self.server_socket.bind( ('', self.network_port) )
131 self.server_socket.listen(1)
Chris Liechti4caf6a52015-08-04 01:07:45 +0200132 except socket.error as msg:
cliechtif1c882c2009-07-23 02:30:06 +0000133 self.handle_server_error()
134 #~ raise
135 if not options.quiet:
136 print "%s: Waiting for connection on %s..." % (self.device, self.network_port)
137
138 # zeroconfig
139 self.publish()
140
141 # now we are ready
142 self.alive = True
143
144 def close(self):
145 """Close all resources and unpublish service"""
146 if not options.quiet:
Chris Liechti4caf6a52015-08-04 01:07:45 +0200147 print("%s: closing..." % (self.device, ))
cliechtif1c882c2009-07-23 02:30:06 +0000148 self.alive = False
149 self.unpublish()
150 if self.server_socket: self.server_socket.close()
151 if self.socket:
152 self.handle_disconnect()
153 self.serial.close()
154 if self.on_close is not None:
155 # ensure it is only called once
156 callback = self.on_close
157 self.on_close = None
158 callback(self)
159
cliechti32c10332009-08-05 13:23:43 +0000160 def write(self, data):
161 """the write method is used by serial.rfc2217.PortManager. it has to
162 write to the network."""
163 self.buffer_ser2net += data
164
cliechtif1c882c2009-07-23 02:30:06 +0000165 def update_select_maps(self, read_map, write_map, error_map):
166 """Update dictionaries for select call. insert fd->callback mapping"""
167 if self.alive:
168 # always handle serial port reads
169 read_map[self.serial] = self.handle_serial_read
170 error_map[self.serial] = self.handle_serial_error
171 # handle serial port writes if buffer is not empty
172 if self.buffer_net2ser:
173 write_map[self.serial] = self.handle_serial_write
174 # handle network
175 if self.socket is not None:
176 # handle socket if connected
177 # only read from network if the internal buffer is not
178 # already filled. the TCP flow control will hold back data
179 if len(self.buffer_net2ser) < 2048:
180 read_map[self.socket] = self.handle_socket_read
181 # only check for write readiness when there is data
182 if self.buffer_ser2net:
183 write_map[self.socket] = self.handle_socket_write
184 error_map[self.socket] = self.handle_socket_error
185 else:
186 # no connection, ensure clear buffer
187 self.buffer_ser2net = ''
188 # check the server socket
189 read_map[self.server_socket] = self.handle_connect
190 error_map[self.server_socket] = self.handle_server_error
191
192
193 def handle_serial_read(self):
194 """Reading from serial port"""
195 try:
196 data = os.read(self.serial.fileno(), 1024)
197 if data:
198 # store data in buffer if there is a client connected
199 if self.socket is not None:
cliechti32c10332009-08-05 13:23:43 +0000200 # escape outgoing data when needed (Telnet IAC (0xff) character)
201 if self.rfc2217:
202 data = serial.to_bytes(self.rfc2217.escape(data))
cliechtif1c882c2009-07-23 02:30:06 +0000203 self.buffer_ser2net += data
204 else:
205 self.handle_serial_error()
Chris Liechti4caf6a52015-08-04 01:07:45 +0200206 except Exception as msg:
cliechtif1c882c2009-07-23 02:30:06 +0000207 self.handle_serial_error(msg)
208
209 def handle_serial_write(self):
210 """Writing to serial port"""
211 try:
212 # write a chunk
213 n = os.write(self.serial.fileno(), self.buffer_net2ser)
214 # and see how large that chunk was, remove that from buffer
215 self.buffer_net2ser = self.buffer_net2ser[n:]
Chris Liechti4caf6a52015-08-04 01:07:45 +0200216 except Exception as msg:
cliechtif1c882c2009-07-23 02:30:06 +0000217 self.handle_serial_error(msg)
218
219 def handle_serial_error(self, error=None):
220 """Serial port error"""
221 # terminate connection
222 self.close()
223
224 def handle_socket_read(self):
225 """Read from socket"""
226 try:
227 # read a chunk from the serial port
228 data = self.socket.recv(1024)
229 if data:
cliechti32c10332009-08-05 13:23:43 +0000230 # Process RFC 2217 stuff when enabled
231 if self.rfc2217:
232 data = serial.to_bytes(self.rfc2217.filter(data))
cliechtif1c882c2009-07-23 02:30:06 +0000233 # add data to buffer
234 self.buffer_net2ser += data
235 else:
236 # empty read indicates disconnection
237 self.handle_disconnect()
238 except socket.error:
239 self.handle_socket_error()
240
241 def handle_socket_write(self):
242 """Write to socket"""
243 try:
244 # write a chunk
245 count = self.socket.send(self.buffer_ser2net)
246 # and remove the sent data from the buffer
247 self.buffer_ser2net = self.buffer_ser2net[count:]
248 except socket.error:
249 self.handle_socket_error()
250
251 def handle_socket_error(self):
252 """Socket connection fails"""
253 self.handle_disconnect()
254
255 def handle_connect(self):
256 """Server socket gets a connection"""
257 # accept a connection in any case, close connection
258 # below if already busy
259 connection, addr = self.server_socket.accept()
260 if self.socket is None:
261 self.socket = connection
262 self.socket.setblocking(0)
cliechtia35cad42009-08-10 20:57:48 +0000263 self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
cliechtif1c882c2009-07-23 02:30:06 +0000264 if not options.quiet:
Chris Liechti4caf6a52015-08-04 01:07:45 +0200265 print('%s: Connected by %s:%s' % (self.device, addr[0], addr[1]))
cliechtid9a06ce2009-08-10 01:30:53 +0000266 self.serial.setRTS(True)
267 self.serial.setDTR(True)
cliechti568dd3c2009-08-17 21:39:47 +0000268 self.rfc2217 = serial.rfc2217.PortManager(self.serial, self)
cliechtif1c882c2009-07-23 02:30:06 +0000269 else:
270 # reject connection if there is already one
271 connection.close()
272 if not options.quiet:
Chris Liechti4caf6a52015-08-04 01:07:45 +0200273 print('%s: Rejecting connect from %s:%s' % (self.device, addr[0], addr[1]))
cliechtif1c882c2009-07-23 02:30:06 +0000274
275 def handle_server_error(self):
cliechti7aed8332009-08-05 14:19:31 +0000276 """Socket server fails"""
cliechtif1c882c2009-07-23 02:30:06 +0000277 self.close()
278
279 def handle_disconnect(self):
280 """Socket gets disconnected"""
cliechtid9a06ce2009-08-10 01:30:53 +0000281 # signal disconnected terminal with control lines
cliechtie67c1f42009-09-10 15:07:44 +0000282 try:
283 self.serial.setRTS(False)
284 self.serial.setDTR(False)
285 finally:
286 # restore original port configuration in case it was changed
287 self.serial.applySettingsDict(self.serial_settings_backup)
288 # stop RFC 2217 state machine
289 self.rfc2217 = None
290 # clear send buffer
291 self.buffer_ser2net = ''
292 # close network connection
293 if self.socket is not None:
294 self.socket.close()
295 self.socket = None
296 if not options.quiet:
Chris Liechti4caf6a52015-08-04 01:07:45 +0200297 print('%s: Disconnected' % self.device)
cliechtif1c882c2009-07-23 02:30:06 +0000298
299
300def test():
301 service = ZeroconfService(name="TestService", port=3000)
302 service.publish()
303 raw_input("Press any key to unpublish the service ")
304 service.unpublish()
305
306
307if __name__ == '__main__':
308 import optparse
309
310 parser = optparse.OptionParser(usage="""\
311%prog [options]
312
313Announce the existence of devices using zeroconf and provide
cliechti32c10332009-08-05 13:23:43 +0000314a TCP/IP <-> serial port gateway (implements RFC 2217).
cliechtif1c882c2009-07-23 02:30:06 +0000315
316Note that the TCP/IP server is not protected. Everyone can connect
317to it!
318
319If running as daemon, write to syslog. Otherwise write to stdout.
320""")
321
322 parser.add_option("-q", "--quiet", dest="quiet", action="store_true",
323 help="suppress non error messages", default=False)
324
325 parser.add_option("-o", "--logfile", dest="log_file",
326 help="write messages file instead of stdout", default=None, metavar="FILE")
327
328 parser.add_option("-d", "--daemon", dest="daemonize", action="store_true",
329 help="start as daemon", default=False)
330
331 parser.add_option("", "--pidfile", dest="pid_file",
332 help="specify a name for the PID file", default=None, metavar="FILE")
333
334 (options, args) = parser.parse_args()
335
336 # redirect output if specified
337 if options.log_file is not None:
338 class WriteFlushed:
339 def __init__(self, fileobj):
340 self.fileobj = fileobj
341 def write(self, s):
342 self.fileobj.write(s)
343 self.fileobj.flush()
344 def close(self):
345 self.fileobj.close()
346 sys.stdout = sys.stderr = WriteFlushed(open(options.log_file, 'a'))
347 # atexit.register(lambda: sys.stdout.close())
348
349 if options.daemonize:
350 # if running as daemon is requested, do the fork magic
351 # options.quiet = True
352 import pwd
353 # do the UNIX double-fork magic, see Stevens' "Advanced
354 # Programming in the UNIX Environment" for details (ISBN 0201563177)
355 try:
356 pid = os.fork()
357 if pid > 0:
358 # exit first parent
359 sys.exit(0)
Chris Liechti4caf6a52015-08-04 01:07:45 +0200360 except OSError as e:
cliechtif1c882c2009-07-23 02:30:06 +0000361 sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
362 sys.exit(1)
363
364 # decouple from parent environment
365 os.chdir("/") # don't prevent unmounting....
366 os.setsid()
367 os.umask(0)
368
369 # do second fork
370 try:
371 pid = os.fork()
372 if pid > 0:
373 # exit from second parent, print eventual PID before
374 # print "Daemon PID %d" % pid
375 if options.pid_file is not None:
376 open(options.pid_file,'w').write("%d"%pid)
377 sys.exit(0)
Chris Liechti4caf6a52015-08-04 01:07:45 +0200378 except OSError as e:
cliechtif1c882c2009-07-23 02:30:06 +0000379 sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
380 sys.exit(1)
381
382 if options.log_file is None:
383 import syslog
384 syslog.openlog("serial port publisher")
385 # redirect output to syslog
386 class WriteToSysLog:
387 def __init__(self):
388 self.buffer = ''
389 def write(self, s):
390 self.buffer += s
391 if '\n' in self.buffer:
392 output, self.buffer = self.buffer.split('\n', 1)
393 syslog.syslog(output)
394 def flush(self):
395 syslog.syslog(self.buffer)
396 self.buffer = ''
397 def close(self):
398 self.flush()
399 sys.stdout = sys.stderr = WriteToSysLog()
400
401 # ensure the that the daemon runs a normal user, if run as root
402 #if os.getuid() == 0:
403 # name, passwd, uid, gid, desc, home, shell = pwd.getpwnam('someuser')
404 # os.setgid(gid) # set group first
405 # os.setuid(uid) # set user
406
407 # keep the published stuff in a dictionary
408 published = {}
409 # prepare list of device names (hard coded)
410 device_list = ['/dev/ttyUSB%d' % p for p in range(8)]
411 # get a nice hostname
412 hostname = socket.gethostname()
413
414 def unpublish(forwarder):
415 """when forwarders die, we need to unregister them"""
416 try:
417 del published[forwarder.device]
418 except KeyError:
419 pass
420 else:
Chris Liechti4caf6a52015-08-04 01:07:45 +0200421 if not options.quiet: print("unpublish: %s" % (forwarder))
cliechtif1c882c2009-07-23 02:30:06 +0000422
423 alive = True
424 next_check = 0
425 # main loop
426 while alive:
427 try:
428 # if it is time, check for serial port devices
429 now = time.time()
430 if now > next_check:
431 next_check = now + 5
432 # check each device
433 for device in device_list:
434 # if it appeared
435 if os.path.exists(device):
436 if device not in published:
437 num = int(device[-1])
438 published[device] = Forwarder(
439 device,
440 "%s on %s" % (device, hostname),
441 7000+num,
442 on_close=unpublish
443 )
Chris Liechti4caf6a52015-08-04 01:07:45 +0200444 if not options.quiet: print("publish: %s" % (published[device]))
cliechtif1c882c2009-07-23 02:30:06 +0000445 published[device].open()
446 else:
447 # or when it disappeared
448 if device in published:
Chris Liechti4caf6a52015-08-04 01:07:45 +0200449 if not options.quiet: print("unpublish: %s" % (published[device]))
cliechtif1c882c2009-07-23 02:30:06 +0000450 published[device].close()
451 try:
452 del published[device]
453 except KeyError:
454 pass
455
456 # select_start = time.time()
457 read_map = {}
458 write_map = {}
459 error_map = {}
460 for publisher in published.values():
461 publisher.update_select_maps(read_map, write_map, error_map)
462 try:
463 readers, writers, errors = select.select(
464 read_map.keys(),
465 write_map.keys(),
466 error_map.keys(),
467 5
468 )
Chris Liechti4caf6a52015-08-04 01:07:45 +0200469 except select.error as err:
cliechtif1c882c2009-07-23 02:30:06 +0000470 if err[0] != EINTR:
471 raise
472 # select_end = time.time()
473 # print "select used %.3f s" % (select_end - select_start)
474 for reader in readers:
475 read_map[reader]()
476 for writer in writers:
477 write_map[writer]()
478 for error in errors:
479 error_map[error]()
480 # print "operation used %.3f s" % (time.time() - select_end)
481 except KeyboardInterrupt:
482 alive = False
483 except SystemExit:
484 raise
485 except:
cliechtie67c1f42009-09-10 15:07:44 +0000486 #~ raise
cliechtif1c882c2009-07-23 02:30:06 +0000487 traceback.print_exc()