• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Redirect data from a TCP/IP connection to a serial port and vice versa.
4#
5# (C) 2002-2020 Chris Liechti <cliechti@gmx.net>
6#
7# SPDX-License-Identifier:    BSD-3-Clause
8
9import sys
10import socket
11import serial
12import serial.threaded
13import time
14
15
16class SerialToNet(serial.threaded.Protocol):
17    """serial->socket"""
18
19    def __init__(self):
20        self.socket = None
21
22    def __call__(self):
23        return self
24
25    def data_received(self, data):
26        if self.socket is not None:
27            self.socket.sendall(data)
28
29
30if __name__ == '__main__':  # noqa
31    import argparse
32
33    parser = argparse.ArgumentParser(
34        description='Simple Serial to Network (TCP/IP) redirector.',
35        epilog="""\
36NOTE: no security measures are implemented. Anyone can remotely connect
37to this service over the network.
38
39Only one connection at once is supported. When the connection is terminated
40it waits for the next connect.
41""")
42
43    parser.add_argument(
44        'SERIALPORT',
45        help="serial port name")
46
47    parser.add_argument(
48        'BAUDRATE',
49        type=int,
50        nargs='?',
51        help='set baud rate, default: %(default)s',
52        default=9600)
53
54    parser.add_argument(
55        '-q', '--quiet',
56        action='store_true',
57        help='suppress non error messages',
58        default=False)
59
60    parser.add_argument(
61        '--develop',
62        action='store_true',
63        help='Development mode, prints Python internals on errors',
64        default=False)
65
66    group = parser.add_argument_group('serial port')
67
68    group.add_argument(
69        "--bytesize",
70        choices=[5, 6, 7, 8],
71        type=int,
72        help="set bytesize, one of {5 6 7 8}, default: 8",
73        default=8)
74
75    group.add_argument(
76        "--parity",
77        choices=['N', 'E', 'O', 'S', 'M'],
78        type=lambda c: c.upper(),
79        help="set parity, one of {N E O S M}, default: N",
80        default='N')
81
82    group.add_argument(
83        "--stopbits",
84        choices=[1, 1.5, 2],
85        type=float,
86        help="set stopbits, one of {1 1.5 2}, default: 1",
87        default=1)
88
89    group.add_argument(
90        '--rtscts',
91        action='store_true',
92        help='enable RTS/CTS flow control (default off)',
93        default=False)
94
95    group.add_argument(
96        '--xonxoff',
97        action='store_true',
98        help='enable software flow control (default off)',
99        default=False)
100
101    group.add_argument(
102        '--rts',
103        type=int,
104        help='set initial RTS line state (possible values: 0, 1)',
105        default=None)
106
107    group.add_argument(
108        '--dtr',
109        type=int,
110        help='set initial DTR line state (possible values: 0, 1)',
111        default=None)
112
113    group = parser.add_argument_group('network settings')
114
115    exclusive_group = group.add_mutually_exclusive_group()
116
117    exclusive_group.add_argument(
118        '-P', '--localport',
119        type=int,
120        help='local TCP port',
121        default=7777)
122
123    exclusive_group.add_argument(
124        '-c', '--client',
125        metavar='HOST:PORT',
126        help='make the connection as a client, instead of running a server',
127        default=False)
128
129    args = parser.parse_args()
130
131    # connect to serial port
132    ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True)
133    ser.baudrate = args.BAUDRATE
134    ser.bytesize = args.bytesize
135    ser.parity = args.parity
136    ser.stopbits = args.stopbits
137    ser.rtscts = args.rtscts
138    ser.xonxoff = args.xonxoff
139
140    if args.rts is not None:
141        ser.rts = args.rts
142
143    if args.dtr is not None:
144        ser.dtr = args.dtr
145
146    if not args.quiet:
147        sys.stderr.write(
148            '--- TCP/IP to Serial redirect on {p.name}  {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'
149            '--- type Ctrl-C / BREAK to quit\n'.format(p=ser))
150
151    try:
152        ser.open()
153    except serial.SerialException as e:
154        sys.stderr.write('Could not open serial port {}: {}\n'.format(ser.name, e))
155        sys.exit(1)
156
157    ser_to_net = SerialToNet()
158    serial_worker = serial.threaded.ReaderThread(ser, ser_to_net)
159    serial_worker.start()
160
161    if not args.client:
162        srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
163        srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
164        srv.bind(('', args.localport))
165        srv.listen(1)
166    try:
167        intentional_exit = False
168        while True:
169            if args.client:
170                host, port = args.client.split(':')
171                sys.stderr.write("Opening connection to {}:{}...\n".format(host, port))
172                client_socket = socket.socket()
173                try:
174                    client_socket.connect((host, int(port)))
175                except socket.error as msg:
176                    sys.stderr.write('WARNING: {}\n'.format(msg))
177                    time.sleep(5)  # intentional delay on reconnection as client
178                    continue
179                sys.stderr.write('Connected\n')
180                client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
181                #~ client_socket.settimeout(5)
182            else:
183                sys.stderr.write('Waiting for connection on {}...\n'.format(args.localport))
184                client_socket, addr = srv.accept()
185                sys.stderr.write('Connected by {}\n'.format(addr))
186                # More quickly detect bad clients who quit without closing the
187                # connection: After 1 second of idle, start sending TCP keep-alive
188                # packets every 1 second. If 3 consecutive keep-alive packets
189                # fail, assume the client is gone and close the connection.
190                try:
191                    client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
192                    client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 1)
193                    client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
194                    client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
195                except AttributeError:
196                    pass # XXX not available on windows
197                client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
198            try:
199                ser_to_net.socket = client_socket
200                # enter network <-> serial loop
201                while True:
202                    try:
203                        data = client_socket.recv(1024)
204                        if not data:
205                            break
206                        ser.write(data)                 # get a bunch of bytes and send them
207                    except socket.error as msg:
208                        if args.develop:
209                            raise
210                        sys.stderr.write('ERROR: {}\n'.format(msg))
211                        # probably got disconnected
212                        break
213            except KeyboardInterrupt:
214                intentional_exit = True
215                raise
216            except socket.error as msg:
217                if args.develop:
218                    raise
219                sys.stderr.write('ERROR: {}\n'.format(msg))
220            finally:
221                ser_to_net.socket = None
222                sys.stderr.write('Disconnected\n')
223                client_socket.close()
224                if args.client and not intentional_exit:
225                    time.sleep(5)  # intentional delay on reconnection as client
226    except KeyboardInterrupt:
227        pass
228
229    sys.stderr.write('\n--- exit ---\n')
230    serial_worker.stop()
231