189 lines
6.0 KiB
Python
Executable File
189 lines
6.0 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# redirect data from a TCP/IP connection to a serial port and vice versa
|
|
# using RFC 2217
|
|
#
|
|
# (C) 2009-2015 Chris Liechti <cliechti@gmx.net>
|
|
#
|
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
import logging
|
|
import socket
|
|
import sys
|
|
import time
|
|
import threading
|
|
import serial
|
|
import serial.rfc2217
|
|
|
|
|
|
class Redirector(object):
|
|
def __init__(self, serial_instance, socket, debug=False):
|
|
self.serial = serial_instance
|
|
self.socket = socket
|
|
self._write_lock = threading.Lock()
|
|
self.rfc2217 = serial.rfc2217.PortManager(
|
|
self.serial,
|
|
self,
|
|
logger=logging.getLogger('rfc2217.server') if debug else None)
|
|
self.log = logging.getLogger('redirector')
|
|
|
|
def statusline_poller(self):
|
|
self.log.debug('status line poll thread started')
|
|
while self.alive:
|
|
time.sleep(1)
|
|
self.rfc2217.check_modem_lines()
|
|
self.log.debug('status line poll thread terminated')
|
|
|
|
def shortcircuit(self):
|
|
"""connect the serial port to the TCP port by copying everything
|
|
from one side to the other"""
|
|
self.alive = True
|
|
self.thread_read = threading.Thread(target=self.reader)
|
|
self.thread_read.daemon = True
|
|
self.thread_read.name = 'serial->socket'
|
|
self.thread_read.start()
|
|
self.thread_poll = threading.Thread(target=self.statusline_poller)
|
|
self.thread_poll.daemon = True
|
|
self.thread_poll.name = 'status line poll'
|
|
self.thread_poll.start()
|
|
self.writer()
|
|
|
|
def reader(self):
|
|
"""loop forever and copy serial->socket"""
|
|
self.log.debug('reader thread started')
|
|
while self.alive:
|
|
try:
|
|
data = self.serial.read(self.serial.in_waiting or 1)
|
|
if data:
|
|
# escape outgoing data when needed (Telnet IAC (0xff) character)
|
|
self.write(b''.join(self.rfc2217.escape(data)))
|
|
except socket.error as msg:
|
|
self.log.error('{}'.format(msg))
|
|
# probably got disconnected
|
|
break
|
|
self.alive = False
|
|
self.log.debug('reader thread terminated')
|
|
|
|
def write(self, data):
|
|
"""thread safe socket write with no data escaping. used to send telnet stuff"""
|
|
with self._write_lock:
|
|
self.socket.sendall(data)
|
|
|
|
def writer(self):
|
|
"""loop forever and copy socket->serial"""
|
|
while self.alive:
|
|
try:
|
|
data = self.socket.recv(1024)
|
|
if not data:
|
|
break
|
|
self.serial.write(b''.join(self.rfc2217.filter(data)))
|
|
except socket.error as msg:
|
|
self.log.error('{}'.format(msg))
|
|
# probably got disconnected
|
|
break
|
|
self.stop()
|
|
|
|
def stop(self):
|
|
"""Stop copying"""
|
|
self.log.debug('stopping')
|
|
if self.alive:
|
|
self.alive = False
|
|
self.thread_read.join()
|
|
self.thread_poll.join()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="RFC 2217 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')
|
|
|
|
parser.add_argument(
|
|
'-p', '--localport',
|
|
type=int,
|
|
help='local TCP port, default: %(default)s',
|
|
metavar='TCPPORT',
|
|
default=2217)
|
|
|
|
parser.add_argument(
|
|
'-v', '--verbose',
|
|
dest='verbosity',
|
|
action='count',
|
|
help='print more diagnostic messages (option can be given multiple times)',
|
|
default=0)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.verbosity > 3:
|
|
args.verbosity = 3
|
|
level = (logging.WARNING,
|
|
logging.INFO,
|
|
logging.DEBUG,
|
|
logging.NOTSET)[args.verbosity]
|
|
logging.basicConfig(level=logging.INFO)
|
|
#~ logging.getLogger('root').setLevel(logging.INFO)
|
|
logging.getLogger('rfc2217').setLevel(level)
|
|
|
|
# connect to serial port
|
|
ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True)
|
|
ser.timeout = 3 # required so that the reader thread can exit
|
|
# reset control line as no _remote_ "terminal" has been connected yet
|
|
ser.dtr = False
|
|
ser.rts = False
|
|
|
|
logging.info("RFC 2217 TCP/IP to Serial redirector - type Ctrl-C / BREAK to quit")
|
|
|
|
try:
|
|
ser.open()
|
|
except serial.SerialException as e:
|
|
logging.error("Could not open serial port {}: {}".format(ser.name, e))
|
|
sys.exit(1)
|
|
|
|
logging.info("Serving serial port: {}".format(ser.name))
|
|
settings = ser.get_settings()
|
|
|
|
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)
|
|
logging.info("TCP/IP port: {}".format(args.localport))
|
|
while True:
|
|
try:
|
|
client_socket, addr = srv.accept()
|
|
logging.info('Connected by {}:{}'.format(addr[0], addr[1]))
|
|
client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
ser.rts = True
|
|
ser.dtr = True
|
|
# enter network <-> serial loop
|
|
r = Redirector(
|
|
ser,
|
|
client_socket,
|
|
args.verbosity > 0)
|
|
try:
|
|
r.shortcircuit()
|
|
finally:
|
|
logging.info('Disconnected')
|
|
r.stop()
|
|
client_socket.close()
|
|
ser.dtr = False
|
|
ser.rts = False
|
|
# Restore port settings (may have been changed by RFC 2217
|
|
# capable client)
|
|
ser.apply_settings(settings)
|
|
except KeyboardInterrupt:
|
|
sys.stdout.write('\n')
|
|
break
|
|
except socket.error as msg:
|
|
logging.error(str(msg))
|
|
|
|
logging.info('--- exit ---')
|