#!/usr/bin/env python
#
# Redirect data from a TCP/IP connection to a serial port and vice versa.
#
# (C) 2002-2015 Chris Liechti <cliechti@gmx.net>
#
# SPDX-License-Identifier:    BSD-3-Clause

import sys
import socket
import serial
import serial.threaded


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__':
    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)

    group = parser.add_argument_group('serial port')

    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(
            '--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')

    group.add_argument(
            '-P', '--localport',
            type=int,
            help='local TCP port',
            default=7777)

    args = parser.parse_args()

    # connect to serial port
    ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True)
    ser.baudrate = args.BAUDRATE
    ser.parity = args.parity
    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'.format(
                p=ser))
        sys.stderr.write("--- type Ctrl-C / BREAK to quit\n")

    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()

    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:
        while True:
            sys.stderr.write("Waiting for connection on {}...\n".format(args.localport))
            client_socket, addr = srv.accept()
            sys.stderr.write('Connected by {}\n'.format(addr))
            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:
                        sys.stderr.write('ERROR: %s\n' % msg)
                        # probably got disconnected
                        break
            except socket.error as msg:
                sys.stderr.write('ERROR: {}\n'.format(msg))
            finally:
                ser_to_net.socket = None
                sys.stderr.write('Disconnected\n')
                client_socket.close()
    except KeyboardInterrupt:
        pass

    sys.stderr.write('\n--- exit ---\n')
    serial_worker.stop()
