blob: bd7db77894573c10335e8c3b53870a53a780e8ad [file] [log] [blame]
#!/usr/bin/env python
#
# Redirect data from a TCP/IP connection to a serial port and vice versa.
#
# (C) 2002-2020 Chris Liechti <cliechti@gmx.net>
#
# SPDX-License-Identifier: BSD-3-Clause
import sys
import socket
import serial
import serial.threaded
import time
class SerialToNet(serial.threaded.Protocol):
"""serial->socket"""
def __init__(self):
self.socket = None
def __call__(self):
return self
def data_received(self, data):
if self.socket is not None:
self.socket.sendall(data)
if __name__ == '__main__': # noqa
import argparse
parser = argparse.ArgumentParser(
description='Simple Serial to Network (TCP/IP) redirector.',
epilog="""\
NOTE: no security measures are implemented. Anyone can remotely connect
to this service over the network.
Only one connection at once is supported. When the connection is terminated
it waits for the next connect.
""")
parser.add_argument(
'SERIALPORT',
help="serial port name")
parser.add_argument(
'BAUDRATE',
type=int,
nargs='?',
help='set baud rate, default: %(default)s',
default=9600)
parser.add_argument(
'-q', '--quiet',
action='store_true',
help='suppress non error messages',
default=False)
parser.add_argument(
'--develop',
action='store_true',
help='Development mode, prints Python internals on errors',
default=False)
group = parser.add_argument_group('serial port')
group.add_argument(
"--bytesize",
choices=[5, 6, 7, 8],
type=int,
help="set bytesize, one of {5 6 7 8}, default: 8",
default=8)
group.add_argument(
"--parity",
choices=['N', 'E', 'O', 'S', 'M'],
type=lambda c: c.upper(),
help="set parity, one of {N E O S M}, default: N",
default='N')
group.add_argument(
"--stopbits",
choices=[1, 1.5, 2],
type=float,
help="set stopbits, one of {1 1.5 2}, default: 1",
default=1)
group.add_argument(
'--rtscts',
action='store_true',
help='enable RTS/CTS flow control (default off)',
default=False)
group.add_argument(
'--xonxoff',
action='store_true',
help='enable software flow control (default off)',
default=False)
group.add_argument(
'--rts',
type=int,
help='set initial RTS line state (possible values: 0, 1)',
default=None)
group.add_argument(
'--dtr',
type=int,
help='set initial DTR line state (possible values: 0, 1)',
default=None)
group = parser.add_argument_group('network settings')
exclusive_group = group.add_mutually_exclusive_group()
exclusive_group.add_argument(
'-P', '--localport',
type=int,
help='local TCP port',
default=7777)
exclusive_group.add_argument(
'-c', '--client',
metavar='HOST:PORT',
help='make the connection as a client, instead of running a server',
default=False)
args = parser.parse_args()
# connect to serial port
ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True)
ser.baudrate = args.BAUDRATE
ser.bytesize = args.bytesize
ser.parity = args.parity
ser.stopbits = args.stopbits
ser.rtscts = args.rtscts
ser.xonxoff = args.xonxoff
if args.rts is not None:
ser.rts = args.rts
if args.dtr is not None:
ser.dtr = args.dtr
if not args.quiet:
sys.stderr.write(
'--- TCP/IP to Serial redirect on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'
'--- type Ctrl-C / BREAK to quit\n'.format(p=ser))
try:
ser.open()
except serial.SerialException as e:
sys.stderr.write('Could not open serial port {}: {}\n'.format(ser.name, e))
sys.exit(1)
ser_to_net = SerialToNet()
serial_worker = serial.threaded.ReaderThread(ser, ser_to_net)
serial_worker.start()
if not args.client:
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('', args.localport))
srv.listen(1)
try:
intentional_exit = False
while True:
if args.client:
host, port = args.client.split(':')
sys.stderr.write("Opening connection to {}:{}...\n".format(host, port))
client_socket = socket.socket()
try:
client_socket.connect((host, int(port)))
except socket.error as msg:
sys.stderr.write('WARNING: {}\n'.format(msg))
time.sleep(5) # intentional delay on reconnection as client
continue
sys.stderr.write('Connected\n')
client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
#~ client_socket.settimeout(5)
else:
sys.stderr.write('Waiting for connection on {}...\n'.format(args.localport))
client_socket, addr = srv.accept()
sys.stderr.write('Connected by {}\n'.format(addr))
# 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.
try:
client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 1)
client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
except AttributeError:
pass # XXX not available on windows
client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
try:
ser_to_net.socket = client_socket
# enter network <-> serial loop
while True:
try:
data = client_socket.recv(1024)
if not data:
break
ser.write(data) # get a bunch of bytes and send them
except socket.error as msg:
if args.develop:
raise
sys.stderr.write('ERROR: {}\n'.format(msg))
# probably got disconnected
break
except KeyboardInterrupt:
intentional_exit = True
raise
except socket.error as msg:
if args.develop:
raise
sys.stderr.write('ERROR: {}\n'.format(msg))
finally:
ser_to_net.socket = None
sys.stderr.write('Disconnected\n')
client_socket.close()
if args.client and not intentional_exit:
time.sleep(5) # intentional delay on reconnection as client
except KeyboardInterrupt:
pass
sys.stderr.write('\n--- exit ---\n')
serial_worker.stop()