Skip to content

Commit 9b24215

Browse files
jakub-kockaradimkarnis
authored andcommitted
refactor: Migrated esp_rfc2217_server into standalone subpackage
This change solves the issues when running on Windows
1 parent 92b2c68 commit 9b24215

File tree

7 files changed

+352
-288
lines changed

7 files changed

+352
-288
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ repos:
1717
- id: mypy
1818
additional_dependencies: ['types-PyYAML<=6.0.12.12']
1919
# ignore wrapper scripts because of name colision with efuse/__init__.py etc.
20-
exclude: test/|docs/|espefuse.py|espsecure.py|esptool.py
20+
exclude: test/|docs/|espefuse.py|espsecure.py|esptool.py|esp_rfc2217_server.py
2121
- repo: https://github.com/codespell-project/codespell
2222
rev: v2.2.5
2323
hooks:

esp_rfc2217_server.py

100755100644
Lines changed: 31 additions & 286 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
#!/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
62
#
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+
###################################################################################
714
# Redirect data from a TCP/IP connection to a serial port and vice versa using RFC 2217.
815
#
916
# This is a modified version of rfc2217_server.py provided by the pyserial package
@@ -21,291 +28,29 @@
2128
#
2229
# esptool.py --port rfc2217://localhost:4000?ign_set_control flash_id
2330
#
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
3131

32-
import logging
32+
import contextlib
3333
import os
34-
import socket
3534
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 ---")
30835

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
30954

31055
if __name__ == "__main__":
311-
main()
56+
esp_rfc2217_server.main()

0 commit comments

Comments
 (0)