|
1 | 1 | #!/usr/bin/env python
|
2 |
| - |
3 |
| -# SPDX-FileCopyrightText: 2009-2015 Chris Liechti |
4 |
| -# SPDX-FileContributor: 2020-2022 Espressif Systems (Shanghai) CO LTD |
5 |
| -# SPDX-License-Identifier: BSD-3-Clause |
6 | 2 | #
|
| 3 | +# SPDX-FileCopyrightText: 2014-2024 Fredrik Ahlberg, Angus Gratton, |
| 4 | +# Espressif Systems (Shanghai) CO LTD, other contributors as noted. |
| 5 | +# |
| 6 | +# SPDX-License-Identifier: BSD-3-Clause |
| 7 | + |
| 8 | +# This executable script is a thin wrapper around the main functionality |
| 9 | +# in the esp_rfc2217_server Python package |
| 10 | + |
| 11 | +# When updating this script, please also update esptool.py, espefuse.py and espsecure.py |
| 12 | + |
| 13 | +################################################################################### |
7 | 14 | # Redirect data from a TCP/IP connection to a serial port and vice versa using RFC 2217.
|
8 | 15 | #
|
9 | 16 | # This is a modified version of rfc2217_server.py provided by the pyserial package
|
|
21 | 28 | #
|
22 | 29 | # esptool.py --port rfc2217://localhost:4000?ign_set_control flash_id
|
23 | 30 | #
|
24 |
| -################################################################################### |
25 |
| -# redirect data from a TCP/IP connection to a serial port and vice versa |
26 |
| -# using RFC 2217 |
27 |
| -# |
28 |
| -# (C) 2009-2015 Chris Liechti <[email protected]> |
29 |
| -# |
30 |
| -# SPDX-License-Identifier: BSD-3-Clause |
31 | 31 |
|
32 |
| -import logging |
| 32 | +import contextlib |
33 | 33 | import os
|
34 |
| -import socket |
35 | 34 | import sys
|
36 |
| -import threading |
37 |
| -import time |
38 |
| - |
39 |
| -from esptool.config import load_config_file |
40 |
| -from esptool.reset import ( |
41 |
| - ClassicReset, |
42 |
| - CustomReset, |
43 |
| - DEFAULT_RESET_DELAY, |
44 |
| - HardReset, |
45 |
| - UnixTightReset, |
46 |
| -) |
47 |
| - |
48 |
| -import serial |
49 |
| -import serial.rfc2217 |
50 |
| -from serial.rfc2217 import ( |
51 |
| - COM_PORT_OPTION, |
52 |
| - SET_CONTROL, |
53 |
| - SET_CONTROL_DTR_OFF, |
54 |
| - SET_CONTROL_DTR_ON, |
55 |
| - SET_CONTROL_RTS_OFF, |
56 |
| - SET_CONTROL_RTS_ON, |
57 |
| -) |
58 |
| - |
59 |
| -cfg, _ = load_config_file(verbose=True) |
60 |
| -cfg = cfg["esptool"] |
61 |
| - |
62 |
| - |
63 |
| -class EspPortManager(serial.rfc2217.PortManager): |
64 |
| - """ |
65 |
| - The beginning of the reset sequence is detected and the proper reset sequence |
66 |
| - is applied in a thread. The rest of the reset sequence received is just ignored |
67 |
| - and not sent to the serial port. |
68 |
| - """ |
69 |
| - |
70 |
| - def __init__(self, serial_port, connection, esp32r0_delay, logger=None): |
71 |
| - self.esp32r0_delay = esp32r0_delay |
72 |
| - self.is_download_mode = False |
73 |
| - super(EspPortManager, self).__init__(serial_port, connection, logger) |
74 |
| - |
75 |
| - def _telnet_process_subnegotiation(self, suboption): |
76 |
| - if suboption[0:1] == COM_PORT_OPTION and suboption[1:2] == SET_CONTROL: |
77 |
| - if suboption[2:3] == SET_CONTROL_DTR_OFF: |
78 |
| - self.is_download_mode = False |
79 |
| - self.serial.dtr = False |
80 |
| - return |
81 |
| - elif suboption[2:3] == SET_CONTROL_RTS_OFF and not self.is_download_mode: |
82 |
| - reset_thread = threading.Thread(target=self._hard_reset_thread) |
83 |
| - reset_thread.daemon = True |
84 |
| - reset_thread.name = "hard_reset_thread" |
85 |
| - reset_thread.start() |
86 |
| - return |
87 |
| - elif suboption[2:3] == SET_CONTROL_DTR_ON and not self.is_download_mode: |
88 |
| - self.is_download_mode = True |
89 |
| - reset_thread = threading.Thread(target=self._reset_thread) |
90 |
| - reset_thread.daemon = True |
91 |
| - reset_thread.name = "reset_thread" |
92 |
| - reset_thread.start() |
93 |
| - return |
94 |
| - elif suboption[2:3] in [ |
95 |
| - SET_CONTROL_DTR_ON, |
96 |
| - SET_CONTROL_RTS_ON, |
97 |
| - SET_CONTROL_RTS_OFF, |
98 |
| - ]: |
99 |
| - return |
100 |
| - # only in cases not handled above do the original implementation in PortManager |
101 |
| - super(EspPortManager, self)._telnet_process_subnegotiation(suboption) |
102 |
| - |
103 |
| - def _hard_reset_thread(self): |
104 |
| - """ |
105 |
| - The reset logic used for hard resetting the chip. |
106 |
| - """ |
107 |
| - if self.logger: |
108 |
| - self.logger.info("Activating hard reset in thread") |
109 |
| - HardReset(self.serial)() |
110 |
| - |
111 |
| - def _reset_thread(self): |
112 |
| - """ |
113 |
| - The reset logic is used from esptool.py because the RTS and DTR signals |
114 |
| - cannot be retransmitted through RFC 2217 with proper timing. |
115 |
| - """ |
116 |
| - if self.logger: |
117 |
| - self.logger.info("Activating reset in thread") |
118 |
| - |
119 |
| - delay = DEFAULT_RESET_DELAY |
120 |
| - if self.esp32r0_delay: |
121 |
| - delay += 0.5 |
122 |
| - |
123 |
| - cfg_custom_reset_sequence = cfg.get("custom_reset_sequence") |
124 |
| - if cfg_custom_reset_sequence is not None: |
125 |
| - CustomReset(self.serial, cfg_custom_reset_sequence)() |
126 |
| - elif os.name != "nt": |
127 |
| - UnixTightReset(self.serial, delay)() |
128 |
| - else: |
129 |
| - ClassicReset(self.serial, delay)() |
130 |
| - |
131 |
| - |
132 |
| -class Redirector(object): |
133 |
| - def __init__(self, serial_instance, socket, debug=False, esp32r0delay=False): |
134 |
| - self.serial = serial_instance |
135 |
| - self.socket = socket |
136 |
| - self._write_lock = threading.Lock() |
137 |
| - self.rfc2217 = EspPortManager( |
138 |
| - self.serial, |
139 |
| - self, |
140 |
| - esp32r0delay, |
141 |
| - logger=logging.getLogger("rfc2217.server") if debug else None, |
142 |
| - ) |
143 |
| - self.log = logging.getLogger("redirector") |
144 |
| - |
145 |
| - def statusline_poller(self): |
146 |
| - self.log.debug("status line poll thread started") |
147 |
| - while self.alive: |
148 |
| - time.sleep(1) |
149 |
| - self.rfc2217.check_modem_lines() |
150 |
| - self.log.debug("status line poll thread terminated") |
151 |
| - |
152 |
| - def shortcircuit(self): |
153 |
| - """connect the serial port to the TCP port by copying everything |
154 |
| - from one side to the other""" |
155 |
| - self.alive = True |
156 |
| - self.thread_read = threading.Thread(target=self.reader) |
157 |
| - self.thread_read.daemon = True |
158 |
| - self.thread_read.name = "serial->socket" |
159 |
| - self.thread_read.start() |
160 |
| - self.thread_poll = threading.Thread(target=self.statusline_poller) |
161 |
| - self.thread_poll.daemon = True |
162 |
| - self.thread_poll.name = "status line poll" |
163 |
| - self.thread_poll.start() |
164 |
| - self.writer() |
165 |
| - |
166 |
| - def reader(self): |
167 |
| - """loop forever and copy serial->socket""" |
168 |
| - self.log.debug("reader thread started") |
169 |
| - while self.alive: |
170 |
| - try: |
171 |
| - data = self.serial.read(self.serial.in_waiting or 1) |
172 |
| - if data: |
173 |
| - # escape outgoing data when needed (Telnet IAC (0xff) character) |
174 |
| - self.write(b"".join(self.rfc2217.escape(data))) |
175 |
| - except socket.error as msg: |
176 |
| - self.log.error("{}".format(msg)) |
177 |
| - # probably got disconnected |
178 |
| - break |
179 |
| - self.alive = False |
180 |
| - self.log.debug("reader thread terminated") |
181 |
| - |
182 |
| - def write(self, data): |
183 |
| - """thread safe socket write with no data escaping. used to send telnet stuff""" |
184 |
| - with self._write_lock: |
185 |
| - self.socket.sendall(data) |
186 |
| - |
187 |
| - def writer(self): |
188 |
| - """loop forever and copy socket->serial""" |
189 |
| - while self.alive: |
190 |
| - try: |
191 |
| - data = self.socket.recv(1024) |
192 |
| - if not data: |
193 |
| - break |
194 |
| - self.serial.write(b"".join(self.rfc2217.filter(data))) |
195 |
| - except socket.error as msg: |
196 |
| - self.log.error("{}".format(msg)) |
197 |
| - # probably got disconnected |
198 |
| - break |
199 |
| - self.stop() |
200 |
| - |
201 |
| - def stop(self): |
202 |
| - """Stop copying""" |
203 |
| - self.log.debug("stopping") |
204 |
| - if self.alive: |
205 |
| - self.alive = False |
206 |
| - self.thread_read.join() |
207 |
| - self.thread_poll.join() |
208 |
| - |
209 |
| - |
210 |
| -def main(): |
211 |
| - import argparse |
212 |
| - |
213 |
| - parser = argparse.ArgumentParser( |
214 |
| - description="RFC 2217 Serial to Network (TCP/IP) redirector.", |
215 |
| - epilog="NOTE: no security measures are implemented. " |
216 |
| - "Anyone can remotely connect to this service over the network.\n" |
217 |
| - "Only one connection at once is supported. " |
218 |
| - "When the connection is terminated it waits for the next connect.", |
219 |
| - ) |
220 |
| - |
221 |
| - parser.add_argument("SERIALPORT") |
222 |
| - |
223 |
| - parser.add_argument( |
224 |
| - "-p", |
225 |
| - "--localport", |
226 |
| - type=int, |
227 |
| - help="local TCP port, default: %(default)s", |
228 |
| - metavar="TCPPORT", |
229 |
| - default=2217, |
230 |
| - ) |
231 |
| - |
232 |
| - parser.add_argument( |
233 |
| - "-v", |
234 |
| - "--verbose", |
235 |
| - dest="verbosity", |
236 |
| - action="count", |
237 |
| - help="print more diagnostic messages (option can be given multiple times)", |
238 |
| - default=0, |
239 |
| - ) |
240 |
| - |
241 |
| - parser.add_argument( |
242 |
| - "--r0", |
243 |
| - help="Use delays necessary for ESP32 revision 0 chips", |
244 |
| - action="store_true", |
245 |
| - ) |
246 |
| - |
247 |
| - args = parser.parse_args() |
248 |
| - |
249 |
| - if args.verbosity > 3: |
250 |
| - args.verbosity = 3 |
251 |
| - level = (logging.WARNING, logging.INFO, logging.DEBUG, logging.NOTSET)[ |
252 |
| - args.verbosity |
253 |
| - ] |
254 |
| - logging.basicConfig(level=logging.INFO) |
255 |
| - # logging.getLogger('root').setLevel(logging.INFO) |
256 |
| - logging.getLogger("rfc2217").setLevel(level) |
257 |
| - |
258 |
| - # connect to serial port |
259 |
| - ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True, exclusive=True) |
260 |
| - ser.timeout = 3 # required so that the reader thread can exit |
261 |
| - # reset control line as no _remote_ "terminal" has been connected yet |
262 |
| - ser.dtr = False |
263 |
| - ser.rts = False |
264 |
| - |
265 |
| - logging.info("RFC 2217 TCP/IP to Serial redirector - type Ctrl-C / BREAK to quit") |
266 |
| - |
267 |
| - try: |
268 |
| - ser.open() |
269 |
| - except serial.SerialException as e: |
270 |
| - logging.error("Could not open serial port {}: {}".format(ser.name, e)) |
271 |
| - sys.exit(1) |
272 |
| - |
273 |
| - logging.info("Serving serial port: {}".format(ser.name)) |
274 |
| - settings = ser.get_settings() |
275 |
| - |
276 |
| - srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
277 |
| - srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
278 |
| - srv.bind(("", args.localport)) |
279 |
| - srv.listen(1) |
280 |
| - logging.info("TCP/IP port: {}".format(args.localport)) |
281 |
| - while True: |
282 |
| - try: |
283 |
| - client_socket, addr = srv.accept() |
284 |
| - logging.info("Connected by {}:{}".format(addr[0], addr[1])) |
285 |
| - client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) |
286 |
| - ser.rts = True |
287 |
| - ser.dtr = True |
288 |
| - # enter network <-> serial loop |
289 |
| - r = Redirector(ser, client_socket, args.verbosity > 0, args.r0) |
290 |
| - try: |
291 |
| - r.shortcircuit() |
292 |
| - finally: |
293 |
| - logging.info("Disconnected") |
294 |
| - r.stop() |
295 |
| - client_socket.close() |
296 |
| - ser.dtr = False |
297 |
| - ser.rts = False |
298 |
| - # Restore port settings (may have been changed by RFC 2217 |
299 |
| - # capable client) |
300 |
| - ser.apply_settings(settings) |
301 |
| - except KeyboardInterrupt: |
302 |
| - sys.stdout.write("\n") |
303 |
| - break |
304 |
| - except socket.error as msg: |
305 |
| - logging.error(str(msg)) |
306 |
| - |
307 |
| - logging.info("--- exit ---") |
308 | 35 |
|
| 36 | +if os.name != "nt": |
| 37 | + # Linux/macOS: remove current script directory to avoid importing this file |
| 38 | + # as a module; we want to import the installed esp_rfc2217_server module instead |
| 39 | + with contextlib.suppress(ValueError): |
| 40 | + executable_dir = os.path.dirname(sys.executable) |
| 41 | + sys.path = [ |
| 42 | + path |
| 43 | + for path in sys.path |
| 44 | + if not path.endswith(("/bin", "/sbin")) and path != executable_dir |
| 45 | + ] |
| 46 | + |
| 47 | + # Linux/macOS: delete imported module entry to force Python to load |
| 48 | + # the module from scratch; this enables importing esp_rfc2217_server module in |
| 49 | + # other Python scripts |
| 50 | + with contextlib.suppress(KeyError): |
| 51 | + del sys.modules["esp_rfc2217_server"] |
| 52 | + |
| 53 | +import esp_rfc2217_server |
309 | 54 |
|
310 | 55 | if __name__ == "__main__":
|
311 |
| - main() |
| 56 | + esp_rfc2217_server.main() |
0 commit comments