diff options
author | Chris Liechti <cliechti@gmx.net> | 2015-09-22 23:13:44 +0200 |
---|---|---|
committer | Chris Liechti <cliechti@gmx.net> | 2015-09-22 23:13:44 +0200 |
commit | f565693c462d9987456fe22b44cce1a7b5036711 (patch) | |
tree | f47db4bdf720acbc1fe3c55eb1a193f374873124 /examples | |
parent | 087bee33c811a93bfe24c731ee599ed9d28c1956 (diff) | |
download | pyserial-git-f565693c462d9987456fe22b44cce1a7b5036711.tar.gz |
fix EOL issues
Diffstat (limited to 'examples')
-rwxr-xr-x | examples/port_publisher.py | 1140 |
1 files changed, 570 insertions, 570 deletions
diff --git a/examples/port_publisher.py b/examples/port_publisher.py index d426da0..074a529 100755 --- a/examples/port_publisher.py +++ b/examples/port_publisher.py @@ -1,570 +1,570 @@ -#! /usr/bin/env python
-#
-# (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
-#
-# SPDX-License-Identifier: BSD-3-Clause
-"""\
-Multi-port serial<->TCP/IP forwarder.
-- RFC 2217
-- check existence of serial port periodically
-- start/stop forwarders
-- each forwarder creates a server socket and opens the serial port
-- serial ports are opened only once. network connect/disconnect
- does not influence serial port
-- only one client per connection
-"""
-import os
-import select
-import socket
-import sys
-import time
-import traceback
-
-import serial
-import serial.rfc2217
-import serial.tools.list_ports
-
-import dbus
-
-# Try to import the avahi service definitions properly. If the avahi module is
-# not available, fall back to a hard-coded solution that hopefully still works.
-try:
- import avahi
-except ImportError:
- class avahi:
- DBUS_NAME = "org.freedesktop.Avahi"
- DBUS_PATH_SERVER = "/"
- DBUS_INTERFACE_SERVER = "org.freedesktop.Avahi.Server"
- DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup"
- IF_UNSPEC = -1
- PROTO_UNSPEC, PROTO_INET, PROTO_INET6 = -1, 0, 1
-
-
-class ZeroconfService:
- """\
- A simple class to publish a network service with zeroconf using avahi.
- """
-
- def __init__(self, name, port, stype="_http._tcp",
- domain="", host="", text=""):
- self.name = name
- self.stype = stype
- self.domain = domain
- self.host = host
- self.port = port
- self.text = text
- self.group = None
-
- def publish(self):
- bus = dbus.SystemBus()
- server = dbus.Interface(
- bus.get_object(
- avahi.DBUS_NAME,
- avahi.DBUS_PATH_SERVER
- ),
- avahi.DBUS_INTERFACE_SERVER
- )
-
- g = dbus.Interface(
- bus.get_object(
- avahi.DBUS_NAME,
- server.EntryGroupNew()
- ),
- avahi.DBUS_INTERFACE_ENTRY_GROUP
- )
-
- g.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, dbus.UInt32(0),
- self.name, self.stype, self.domain, self.host,
- dbus.UInt16(self.port), self.text)
-
- g.Commit()
- self.group = g
-
- def unpublish(self):
- if self.group is not None:
- self.group.Reset()
- self.group = None
-
- def __str__(self):
- return "%r @ %s:%s (%s)" % (self.name, self.host, self.port, self.stype)
-
-
-class Forwarder(ZeroconfService):
- """\
- Single port serial<->TCP/IP forarder that depends on an external select
- loop.
- - Buffers for serial -> network and network -> serial
- - RFC 2217 state
- - Zeroconf publish/unpublish on open/close.
- """
-
- def __init__(self, device, name, network_port, on_close=None, log=None):
- ZeroconfService.__init__(self, name, network_port, stype='_serial_port._tcp')
- self.alive = False
- self.network_port = network_port
- self.on_close = on_close
- self.log = log
- self.device = device
- self.serial = serial.Serial()
- self.serial.port = device
- self.serial.baudrate = 115200
- self.serial.timeout = 0
- self.socket = None
- self.server_socket = None
- self.rfc2217 = None # instantiate later, when connecting
-
- def __del__(self):
- try:
- if self.alive:
- self.close()
- except:
- pass # XXX errors on shutdown
-
- def open(self):
- """open serial port, start network server and publish service"""
- self.buffer_net2ser = bytearray()
- self.buffer_ser2net = bytearray()
-
- # open serial port
- try:
- self.serial.rts = False
- self.serial.open()
- except Exception as msg:
- self.handle_serial_error(msg)
-
- self.serial_settings_backup = self.serial.get_settings()
-
- # start the socket server
- # XXX add IPv6 support: use getaddrinfo for socket options, bind to multiple sockets?
- # info_list = socket.getaddrinfo(None, port, 0, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
- self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.server_socket.setsockopt(
- socket.SOL_SOCKET,
- socket.SO_REUSEADDR,
- self.server_socket.getsockopt(
- socket.SOL_SOCKET,
- socket.SO_REUSEADDR
- ) | 1
- )
- self.server_socket.setblocking(0)
- try:
- self.server_socket.bind(('', self.network_port))
- self.server_socket.listen(1)
- except socket.error as msg:
- self.handle_server_error()
- #~ raise
- if self.log is not None:
- self.log.info("%s: Waiting for connection on %s..." % (self.device, self.network_port))
-
- # zeroconfig
- self.publish()
-
- # now we are ready
- self.alive = True
-
- def close(self):
- """Close all resources and unpublish service"""
- if self.log is not None:
- self.log.info("%s: closing..." % (self.device, ))
- self.alive = False
- self.unpublish()
- if self.server_socket:
- self.server_socket.close()
- if self.socket:
- self.handle_disconnect()
- self.serial.close()
- if self.on_close is not None:
- # ensure it is only called once
- callback = self.on_close
- self.on_close = None
- callback(self)
-
- def write(self, data):
- """the write method is used by serial.rfc2217.PortManager. it has to
- write to the network."""
- self.buffer_ser2net += data
-
- def update_select_maps(self, read_map, write_map, error_map):
- """Update dictionaries for select call. insert fd->callback mapping"""
- if self.alive:
- # always handle serial port reads
- read_map[self.serial] = self.handle_serial_read
- error_map[self.serial] = self.handle_serial_error
- # handle serial port writes if buffer is not empty
- if self.buffer_net2ser:
- write_map[self.serial] = self.handle_serial_write
- # handle network
- if self.socket is not None:
- # handle socket if connected
- # only read from network if the internal buffer is not
- # already filled. the TCP flow control will hold back data
- if len(self.buffer_net2ser) < 2048:
- read_map[self.socket] = self.handle_socket_read
- # only check for write readiness when there is data
- if self.buffer_ser2net:
- write_map[self.socket] = self.handle_socket_write
- error_map[self.socket] = self.handle_socket_error
- else:
- # no connection, ensure clear buffer
- self.buffer_ser2net = bytearray()
- # check the server socket
- read_map[self.server_socket] = self.handle_connect
- error_map[self.server_socket] = self.handle_server_error
-
- def handle_serial_read(self):
- """Reading from serial port"""
- try:
- data = os.read(self.serial.fileno(), 1024)
- if data:
- # store data in buffer if there is a client connected
- if self.socket is not None:
- # escape outgoing data when needed (Telnet IAC (0xff) character)
- if self.rfc2217:
- data = serial.to_bytes(self.rfc2217.escape(data))
- self.buffer_ser2net += data
- else:
- self.handle_serial_error()
- except Exception as msg:
- self.handle_serial_error(msg)
-
- def handle_serial_write(self):
- """Writing to serial port"""
- try:
- # write a chunk
- n = os.write(self.serial.fileno(), bytes(self.buffer_net2ser))
- # and see how large that chunk was, remove that from buffer
- self.buffer_net2ser = self.buffer_net2ser[n:]
- except Exception as msg:
- self.handle_serial_error(msg)
-
- def handle_serial_error(self, error=None):
- """Serial port error"""
- # terminate connection
- self.close()
-
- def handle_socket_read(self):
- """Read from socket"""
- try:
- # read a chunk from the serial port
- data = self.socket.recv(1024)
- if data:
- # Process RFC 2217 stuff when enabled
- if self.rfc2217:
- data = serial.to_bytes(self.rfc2217.filter(data))
- # add data to buffer
- self.buffer_net2ser += data
- else:
- # empty read indicates disconnection
- self.handle_disconnect()
- except socket.error:
- self.handle_socket_error()
-
- def handle_socket_write(self):
- """Write to socket"""
- try:
- # write a chunk
- count = self.socket.send(bytes(self.buffer_ser2net))
- # and remove the sent data from the buffer
- self.buffer_ser2net = self.buffer_ser2net[count:]
- except socket.error:
- self.handle_socket_error()
-
- def handle_socket_error(self):
- """Socket connection fails"""
- self.handle_disconnect()
-
- def handle_connect(self):
- """Server socket gets a connection"""
- # accept a connection in any case, close connection
- # below if already busy
- connection, addr = self.server_socket.accept()
- if self.socket is None:
- self.socket = connection
- # More quickly detect bad clients who quit without closing the
- # connection: After 1 second of idle, start sending TCP keep-alive
- # packets every 1 second. If 3 consecutive keep-alive packets
- # fail, assume the client is gone and close the connection.
- self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
- self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
- self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 1)
- self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
- self.socket.setblocking(0)
- self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
- if self.log is not None:
- self.log.warning('%s: Connected by %s:%s' % (self.device, addr[0], addr[1]))
- self.serial.rts = True
- self.serial.dtr = True
- if self.log is not None:
- self.rfc2217 = serial.rfc2217.PortManager(self.serial, self, logger=log.getChild(self.device))
- else:
- self.rfc2217 = serial.rfc2217.PortManager(self.serial, self)
- else:
- # reject connection if there is already one
- connection.close()
- if self.log is not None:
- self.log.warning('%s: Rejecting connect from %s:%s' % (self.device, addr[0], addr[1]))
-
- def handle_server_error(self):
- """Socket server fails"""
- self.close()
-
- def handle_disconnect(self):
- """Socket gets disconnected"""
- # signal disconnected terminal with control lines
- try:
- self.serial.rts = False
- self.serial.dtr = False
- finally:
- # restore original port configuration in case it was changed
- self.serial.apply_settings(self.serial_settings_backup)
- # stop RFC 2217 state machine
- self.rfc2217 = None
- # clear send buffer
- self.buffer_ser2net = bytearray()
- # close network connection
- if self.socket is not None:
- self.socket.close()
- self.socket = None
- if self.log is not None:
- self.log.warning('%s: Disconnected' % self.device)
-
-
-def test():
- service = ZeroconfService(name="TestService", port=3000)
- service.publish()
- raw_input("Press any key to unpublish the service ")
- service.unpublish()
-
-
-if __name__ == '__main__':
- import logging
- import argparse
-
- VERBOSTIY = [
- logging.ERROR, # 0
- logging.WARNING, # 1 (default)
- logging.INFO, # 2
- logging.DEBUG, # 3
- ]
-
- parser = argparse.ArgumentParser(usage="""\
-%(prog)s [options]
-
-Announce the existence of devices using zeroconf and provide
-a TCP/IP <-> serial port gateway (implements RFC 2217).
-
-If running as daemon, write to syslog. Otherwise write to stdout.
-""",
- epilog="""\
-NOTE: no security measures are implemented. Anyone can remotely connect
-to this service over the network.
-
-Only one connection at once, per port, is supported. When the connection is
-terminated, it waits for the next connect.
-""")
-
- group = parser.add_argument_group("serial port settings")
-
- group.add_argument(
- "--ports-regex",
- help="specify a regex to search against the serial devices and their descriptions (default: %(default)s)",
- default='/dev/ttyUSB[0-9]+',
- metavar="REGEX")
-
- group = parser.add_argument_group("network settings")
-
- group.add_argument(
- "--tcp-port",
- dest="base_port",
- help="specify lowest TCP port number (default: %(default)s)",
- default=7000,
- type=int,
- metavar="PORT")
-
- group = parser.add_argument_group("daemon")
-
- group.add_argument(
- "-d", "--daemon",
- dest="daemonize",
- action="store_true",
- help="start as daemon",
- default=False)
-
- group.add_argument(
- "--pidfile",
- help="specify a name for the PID file",
- default=None,
- metavar="FILE")
-
- group = parser.add_argument_group("diagnostics")
-
- group.add_argument(
- "-o", "--logfile",
- help="write messages file instead of stdout",
- default=None,
- metavar="FILE")
-
- group.add_argument(
- "-q", "--quiet",
- dest="verbosity",
- action="store_const",
- const=0,
- help="suppress most diagnostic messages",
- default=1)
-
- group.add_argument(
- "-v", "--verbose",
- dest="verbosity",
- action="count",
- help="increase diagnostic messages")
-
-
- args = parser.parse_args()
-
- # set up logging
- logging.basicConfig(level=VERBOSTIY[min(args.verbosity, len(VERBOSTIY) - 1)])
- log = logging.getLogger('port_publisher')
-
- # redirect output if specified
- if args.logfile is not None:
- class WriteFlushed:
- def __init__(self, fileobj):
- self.fileobj = fileobj
- def write(self, s):
- self.fileobj.write(s)
- self.fileobj.flush()
- def close(self):
- self.fileobj.close()
- sys.stdout = sys.stderr = WriteFlushed(open(args.logfile, 'a'))
- # atexit.register(lambda: sys.stdout.close())
-
- if args.daemonize:
- # if running as daemon is requested, do the fork magic
- # args.quiet = True
- # do the UNIX double-fork magic, see Stevens' "Advanced
- # Programming in the UNIX Environment" for details (ISBN 0201563177)
- try:
- pid = os.fork()
- if pid > 0:
- # exit first parent
- sys.exit(0)
- except OSError as e:
- log.critical("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
- sys.exit(1)
-
- # decouple from parent environment
- os.chdir("/") # don't prevent unmounting....
- os.setsid()
- os.umask(0)
-
- # do second fork
- try:
- pid = os.fork()
- if pid > 0:
- # exit from second parent, save eventual PID before
- if args.pidfile is not None:
- open(args.pidfile, 'w').write("%d" % pid)
- sys.exit(0)
- except OSError as e:
- log.critical("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
- sys.exit(1)
-
- if args.logfile is None:
- import syslog
- syslog.openlog("serial port publisher")
- # redirect output to syslog
- class WriteToSysLog:
- def __init__(self):
- self.buffer = ''
- def write(self, s):
- self.buffer += s
- if '\n' in self.buffer:
- output, self.buffer = self.buffer.split('\n', 1)
- syslog.syslog(output)
- def flush(self):
- syslog.syslog(self.buffer)
- self.buffer = ''
- def close(self):
- self.flush()
- sys.stdout = sys.stderr = WriteToSysLog()
-
- # ensure the that the daemon runs a normal user, if run as root
- #if os.getuid() == 0:
- # name, passwd, uid, gid, desc, home, shell = pwd.getpwnam('someuser')
- # os.setgid(gid) # set group first
- # os.setuid(uid) # set user
-
- # keep the published stuff in a dictionary
- published = {}
- # get a nice hostname
- hostname = socket.gethostname()
-
- def unpublish(forwarder):
- """when forwarders die, we need to unregister them"""
- try:
- del published[forwarder.device]
- except KeyError:
- pass
- else:
- log.info("unpublish: %s" % (forwarder))
-
- alive = True
- next_check = 0
- # main loop
- while alive:
- try:
- # if it is time, check for serial port devices
- now = time.time()
- if now > next_check:
- next_check = now + 5
- connected = [d for d, p, i in serial.tools.list_ports.grep(args.ports_regex)]
- # Handle devices that are published, but no longer connected
- for device in set(published).difference(connected):
- log.info("unpublish: %s" % (published[device]))
- unpublish(published[device])
- # Handle devices that are connected but not yet published
- for device in set(connected).difference(published):
- # Find the first available port, starting from 7000
- port = args.base_port
- ports_in_use = [f.network_port for f in published.values()]
- while port in ports_in_use:
- port += 1
- published[device] = Forwarder(
- device,
- "%s on %s" % (device, hostname),
- port,
- on_close=unpublish,
- log=log
- )
- log.warning("publish: %s" % (published[device]))
- published[device].open()
-
- # select_start = time.time()
- read_map = {}
- write_map = {}
- error_map = {}
- for publisher in published.values():
- publisher.update_select_maps(read_map, write_map, error_map)
- readers, writers, errors = select.select(
- read_map.keys(),
- write_map.keys(),
- error_map.keys(),
- 5
- )
- # select_end = time.time()
- # print "select used %.3f s" % (select_end - select_start)
- for reader in readers:
- read_map[reader]()
- for writer in writers:
- write_map[writer]()
- for error in errors:
- error_map[error]()
- # print "operation used %.3f s" % (time.time() - select_end)
- except KeyboardInterrupt:
- alive = False
- sys.stdout.write('\n')
- except SystemExit:
- raise
- except:
- #~ raise
- traceback.print_exc()
+#! /usr/bin/env python +# +# (C) 2001-2015 Chris Liechti <cliechti@gmx.net> +# +# SPDX-License-Identifier: BSD-3-Clause +"""\ +Multi-port serial<->TCP/IP forwarder. +- RFC 2217 +- check existence of serial port periodically +- start/stop forwarders +- each forwarder creates a server socket and opens the serial port +- serial ports are opened only once. network connect/disconnect + does not influence serial port +- only one client per connection +""" +import os +import select +import socket +import sys +import time +import traceback + +import serial +import serial.rfc2217 +import serial.tools.list_ports + +import dbus + +# Try to import the avahi service definitions properly. If the avahi module is +# not available, fall back to a hard-coded solution that hopefully still works. +try: + import avahi +except ImportError: + class avahi: + DBUS_NAME = "org.freedesktop.Avahi" + DBUS_PATH_SERVER = "/" + DBUS_INTERFACE_SERVER = "org.freedesktop.Avahi.Server" + DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup" + IF_UNSPEC = -1 + PROTO_UNSPEC, PROTO_INET, PROTO_INET6 = -1, 0, 1 + + +class ZeroconfService: + """\ + A simple class to publish a network service with zeroconf using avahi. + """ + + def __init__(self, name, port, stype="_http._tcp", + domain="", host="", text=""): + self.name = name + self.stype = stype + self.domain = domain + self.host = host + self.port = port + self.text = text + self.group = None + + def publish(self): + bus = dbus.SystemBus() + server = dbus.Interface( + bus.get_object( + avahi.DBUS_NAME, + avahi.DBUS_PATH_SERVER + ), + avahi.DBUS_INTERFACE_SERVER + ) + + g = dbus.Interface( + bus.get_object( + avahi.DBUS_NAME, + server.EntryGroupNew() + ), + avahi.DBUS_INTERFACE_ENTRY_GROUP + ) + + g.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, dbus.UInt32(0), + self.name, self.stype, self.domain, self.host, + dbus.UInt16(self.port), self.text) + + g.Commit() + self.group = g + + def unpublish(self): + if self.group is not None: + self.group.Reset() + self.group = None + + def __str__(self): + return "%r @ %s:%s (%s)" % (self.name, self.host, self.port, self.stype) + + +class Forwarder(ZeroconfService): + """\ + Single port serial<->TCP/IP forarder that depends on an external select + loop. + - Buffers for serial -> network and network -> serial + - RFC 2217 state + - Zeroconf publish/unpublish on open/close. + """ + + def __init__(self, device, name, network_port, on_close=None, log=None): + ZeroconfService.__init__(self, name, network_port, stype='_serial_port._tcp') + self.alive = False + self.network_port = network_port + self.on_close = on_close + self.log = log + self.device = device + self.serial = serial.Serial() + self.serial.port = device + self.serial.baudrate = 115200 + self.serial.timeout = 0 + self.socket = None + self.server_socket = None + self.rfc2217 = None # instantiate later, when connecting + + def __del__(self): + try: + if self.alive: + self.close() + except: + pass # XXX errors on shutdown + + def open(self): + """open serial port, start network server and publish service""" + self.buffer_net2ser = bytearray() + self.buffer_ser2net = bytearray() + + # open serial port + try: + self.serial.rts = False + self.serial.open() + except Exception as msg: + self.handle_serial_error(msg) + + self.serial_settings_backup = self.serial.get_settings() + + # start the socket server + # XXX add IPv6 support: use getaddrinfo for socket options, bind to multiple sockets? + # info_list = socket.getaddrinfo(None, port, 0, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.setsockopt( + socket.SOL_SOCKET, + socket.SO_REUSEADDR, + self.server_socket.getsockopt( + socket.SOL_SOCKET, + socket.SO_REUSEADDR + ) | 1 + ) + self.server_socket.setblocking(0) + try: + self.server_socket.bind(('', self.network_port)) + self.server_socket.listen(1) + except socket.error as msg: + self.handle_server_error() + #~ raise + if self.log is not None: + self.log.info("%s: Waiting for connection on %s..." % (self.device, self.network_port)) + + # zeroconfig + self.publish() + + # now we are ready + self.alive = True + + def close(self): + """Close all resources and unpublish service""" + if self.log is not None: + self.log.info("%s: closing..." % (self.device, )) + self.alive = False + self.unpublish() + if self.server_socket: + self.server_socket.close() + if self.socket: + self.handle_disconnect() + self.serial.close() + if self.on_close is not None: + # ensure it is only called once + callback = self.on_close + self.on_close = None + callback(self) + + def write(self, data): + """the write method is used by serial.rfc2217.PortManager. it has to + write to the network.""" + self.buffer_ser2net += data + + def update_select_maps(self, read_map, write_map, error_map): + """Update dictionaries for select call. insert fd->callback mapping""" + if self.alive: + # always handle serial port reads + read_map[self.serial] = self.handle_serial_read + error_map[self.serial] = self.handle_serial_error + # handle serial port writes if buffer is not empty + if self.buffer_net2ser: + write_map[self.serial] = self.handle_serial_write + # handle network + if self.socket is not None: + # handle socket if connected + # only read from network if the internal buffer is not + # already filled. the TCP flow control will hold back data + if len(self.buffer_net2ser) < 2048: + read_map[self.socket] = self.handle_socket_read + # only check for write readiness when there is data + if self.buffer_ser2net: + write_map[self.socket] = self.handle_socket_write + error_map[self.socket] = self.handle_socket_error + else: + # no connection, ensure clear buffer + self.buffer_ser2net = bytearray() + # check the server socket + read_map[self.server_socket] = self.handle_connect + error_map[self.server_socket] = self.handle_server_error + + def handle_serial_read(self): + """Reading from serial port""" + try: + data = os.read(self.serial.fileno(), 1024) + if data: + # store data in buffer if there is a client connected + if self.socket is not None: + # escape outgoing data when needed (Telnet IAC (0xff) character) + if self.rfc2217: + data = serial.to_bytes(self.rfc2217.escape(data)) + self.buffer_ser2net += data + else: + self.handle_serial_error() + except Exception as msg: + self.handle_serial_error(msg) + + def handle_serial_write(self): + """Writing to serial port""" + try: + # write a chunk + n = os.write(self.serial.fileno(), bytes(self.buffer_net2ser)) + # and see how large that chunk was, remove that from buffer + self.buffer_net2ser = self.buffer_net2ser[n:] + except Exception as msg: + self.handle_serial_error(msg) + + def handle_serial_error(self, error=None): + """Serial port error""" + # terminate connection + self.close() + + def handle_socket_read(self): + """Read from socket""" + try: + # read a chunk from the serial port + data = self.socket.recv(1024) + if data: + # Process RFC 2217 stuff when enabled + if self.rfc2217: + data = serial.to_bytes(self.rfc2217.filter(data)) + # add data to buffer + self.buffer_net2ser += data + else: + # empty read indicates disconnection + self.handle_disconnect() + except socket.error: + self.handle_socket_error() + + def handle_socket_write(self): + """Write to socket""" + try: + # write a chunk + count = self.socket.send(bytes(self.buffer_ser2net)) + # and remove the sent data from the buffer + self.buffer_ser2net = self.buffer_ser2net[count:] + except socket.error: + self.handle_socket_error() + + def handle_socket_error(self): + """Socket connection fails""" + self.handle_disconnect() + + def handle_connect(self): + """Server socket gets a connection""" + # accept a connection in any case, close connection + # below if already busy + connection, addr = self.server_socket.accept() + if self.socket is None: + self.socket = connection + # More quickly detect bad clients who quit without closing the + # connection: After 1 second of idle, start sending TCP keep-alive + # packets every 1 second. If 3 consecutive keep-alive packets + # fail, assume the client is gone and close the connection. + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1) + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 1) + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) + self.socket.setblocking(0) + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + if self.log is not None: + self.log.warning('%s: Connected by %s:%s' % (self.device, addr[0], addr[1])) + self.serial.rts = True + self.serial.dtr = True + if self.log is not None: + self.rfc2217 = serial.rfc2217.PortManager(self.serial, self, logger=log.getChild(self.device)) + else: + self.rfc2217 = serial.rfc2217.PortManager(self.serial, self) + else: + # reject connection if there is already one + connection.close() + if self.log is not None: + self.log.warning('%s: Rejecting connect from %s:%s' % (self.device, addr[0], addr[1])) + + def handle_server_error(self): + """Socket server fails""" + self.close() + + def handle_disconnect(self): + """Socket gets disconnected""" + # signal disconnected terminal with control lines + try: + self.serial.rts = False + self.serial.dtr = False + finally: + # restore original port configuration in case it was changed + self.serial.apply_settings(self.serial_settings_backup) + # stop RFC 2217 state machine + self.rfc2217 = None + # clear send buffer + self.buffer_ser2net = bytearray() + # close network connection + if self.socket is not None: + self.socket.close() + self.socket = None + if self.log is not None: + self.log.warning('%s: Disconnected' % self.device) + + +def test(): + service = ZeroconfService(name="TestService", port=3000) + service.publish() + raw_input("Press any key to unpublish the service ") + service.unpublish() + + +if __name__ == '__main__': + import logging + import argparse + + VERBOSTIY = [ + logging.ERROR, # 0 + logging.WARNING, # 1 (default) + logging.INFO, # 2 + logging.DEBUG, # 3 + ] + + parser = argparse.ArgumentParser(usage="""\ +%(prog)s [options] + +Announce the existence of devices using zeroconf and provide +a TCP/IP <-> serial port gateway (implements RFC 2217). + +If running as daemon, write to syslog. Otherwise write to stdout. +""", + epilog="""\ +NOTE: no security measures are implemented. Anyone can remotely connect +to this service over the network. + +Only one connection at once, per port, is supported. When the connection is +terminated, it waits for the next connect. +""") + + group = parser.add_argument_group("serial port settings") + + group.add_argument( + "--ports-regex", + help="specify a regex to search against the serial devices and their descriptions (default: %(default)s)", + default='/dev/ttyUSB[0-9]+', + metavar="REGEX") + + group = parser.add_argument_group("network settings") + + group.add_argument( + "--tcp-port", + dest="base_port", + help="specify lowest TCP port number (default: %(default)s)", + default=7000, + type=int, + metavar="PORT") + + group = parser.add_argument_group("daemon") + + group.add_argument( + "-d", "--daemon", + dest="daemonize", + action="store_true", + help="start as daemon", + default=False) + + group.add_argument( + "--pidfile", + help="specify a name for the PID file", + default=None, + metavar="FILE") + + group = parser.add_argument_group("diagnostics") + + group.add_argument( + "-o", "--logfile", + help="write messages file instead of stdout", + default=None, + metavar="FILE") + + group.add_argument( + "-q", "--quiet", + dest="verbosity", + action="store_const", + const=0, + help="suppress most diagnostic messages", + default=1) + + group.add_argument( + "-v", "--verbose", + dest="verbosity", + action="count", + help="increase diagnostic messages") + + + args = parser.parse_args() + + # set up logging + logging.basicConfig(level=VERBOSTIY[min(args.verbosity, len(VERBOSTIY) - 1)]) + log = logging.getLogger('port_publisher') + + # redirect output if specified + if args.logfile is not None: + class WriteFlushed: + def __init__(self, fileobj): + self.fileobj = fileobj + def write(self, s): + self.fileobj.write(s) + self.fileobj.flush() + def close(self): + self.fileobj.close() + sys.stdout = sys.stderr = WriteFlushed(open(args.logfile, 'a')) + # atexit.register(lambda: sys.stdout.close()) + + if args.daemonize: + # if running as daemon is requested, do the fork magic + # args.quiet = True + # do the UNIX double-fork magic, see Stevens' "Advanced + # Programming in the UNIX Environment" for details (ISBN 0201563177) + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError as e: + log.critical("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # decouple from parent environment + os.chdir("/") # don't prevent unmounting.... + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent, save eventual PID before + if args.pidfile is not None: + open(args.pidfile, 'w').write("%d" % pid) + sys.exit(0) + except OSError as e: + log.critical("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + if args.logfile is None: + import syslog + syslog.openlog("serial port publisher") + # redirect output to syslog + class WriteToSysLog: + def __init__(self): + self.buffer = '' + def write(self, s): + self.buffer += s + if '\n' in self.buffer: + output, self.buffer = self.buffer.split('\n', 1) + syslog.syslog(output) + def flush(self): + syslog.syslog(self.buffer) + self.buffer = '' + def close(self): + self.flush() + sys.stdout = sys.stderr = WriteToSysLog() + + # ensure the that the daemon runs a normal user, if run as root + #if os.getuid() == 0: + # name, passwd, uid, gid, desc, home, shell = pwd.getpwnam('someuser') + # os.setgid(gid) # set group first + # os.setuid(uid) # set user + + # keep the published stuff in a dictionary + published = {} + # get a nice hostname + hostname = socket.gethostname() + + def unpublish(forwarder): + """when forwarders die, we need to unregister them""" + try: + del published[forwarder.device] + except KeyError: + pass + else: + log.info("unpublish: %s" % (forwarder)) + + alive = True + next_check = 0 + # main loop + while alive: + try: + # if it is time, check for serial port devices + now = time.time() + if now > next_check: + next_check = now + 5 + connected = [d for d, p, i in serial.tools.list_ports.grep(args.ports_regex)] + # Handle devices that are published, but no longer connected + for device in set(published).difference(connected): + log.info("unpublish: %s" % (published[device])) + unpublish(published[device]) + # Handle devices that are connected but not yet published + for device in set(connected).difference(published): + # Find the first available port, starting from 7000 + port = args.base_port + ports_in_use = [f.network_port for f in published.values()] + while port in ports_in_use: + port += 1 + published[device] = Forwarder( + device, + "%s on %s" % (device, hostname), + port, + on_close=unpublish, + log=log + ) + log.warning("publish: %s" % (published[device])) + published[device].open() + + # select_start = time.time() + read_map = {} + write_map = {} + error_map = {} + for publisher in published.values(): + publisher.update_select_maps(read_map, write_map, error_map) + readers, writers, errors = select.select( + read_map.keys(), + write_map.keys(), + error_map.keys(), + 5 + ) + # select_end = time.time() + # print "select used %.3f s" % (select_end - select_start) + for reader in readers: + read_map[reader]() + for writer in writers: + write_map[writer]() + for error in errors: + error_map[error]() + # print "operation used %.3f s" % (time.time() - select_end) + except KeyboardInterrupt: + alive = False + sys.stdout.write('\n') + except SystemExit: + raise + except: + #~ raise + traceback.print_exc() |