Skip to content

Commit 3a97175

Browse files
pi-anldpgeorge
authored andcommitted
tools/mpremote: Fix disconnect handling on Windows and Linux.
Changes in this commit: - Handle SerialException on Windows when device disconnects. - Print clean 'device disconnected' message instead of stack trace. - Fix terminal formatting issues on Linux after disconnect. - Return disconnected state after console cleanup to avoid terminal issues. This ensures proper disconnect messages on both platforms without showing confusing error traces to users. Signed-off-by: Andrew Leech <[email protected]>
1 parent e33a0f4 commit 3a97175

File tree

2 files changed

+69
-44
lines changed

2 files changed

+69
-44
lines changed

tools/mpremote/mpremote/main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,11 @@ def main():
620620
# If no commands were "actions" then implicitly finish with the REPL
621621
# using default args.
622622
if state.run_repl_on_completion():
623-
do_repl(state, argparse_repl().parse_args([]))
623+
disconnected = do_repl(state, argparse_repl().parse_args([]))
624+
625+
# Handle disconnection message
626+
if disconnected:
627+
print("\ndevice disconnected")
624628

625629
return 0
626630
except CommandError as e:

tools/mpremote/mpremote/repl.py

Lines changed: 64 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,53 @@ def do_repl_main_loop(
77
state, console_in, console_out_write, *, escape_non_printable, code_to_inject, file_to_inject
88
):
99
while True:
10-
console_in.waitchar(state.transport.serial)
11-
c = console_in.readchar()
12-
if c:
13-
if c in (b"\x1d", b"\x18"): # ctrl-] or ctrl-x, quit
14-
break
15-
elif c == b"\x04": # ctrl-D
16-
# special handling needed for ctrl-D if filesystem is mounted
17-
state.transport.write_ctrl_d(console_out_write)
18-
elif c == b"\x0a" and code_to_inject is not None: # ctrl-j, inject code
19-
state.transport.serial.write(code_to_inject)
20-
elif c == b"\x0b" and file_to_inject is not None: # ctrl-k, inject script
21-
console_out_write(bytes("Injecting %s\r\n" % file_to_inject, "utf8"))
22-
state.transport.enter_raw_repl(soft_reset=False)
23-
with open(file_to_inject, "rb") as f:
24-
pyfile = f.read()
25-
try:
26-
state.transport.exec_raw_no_follow(pyfile)
27-
except TransportError as er:
28-
console_out_write(b"Error:\r\n")
29-
console_out_write(er)
30-
state.transport.exit_raw_repl()
31-
else:
32-
state.transport.serial.write(c)
33-
3410
try:
11+
console_in.waitchar(state.transport.serial)
12+
c = console_in.readchar()
13+
if c:
14+
if c in (b"\x1d", b"\x18"): # ctrl-] or ctrl-x, quit
15+
break
16+
elif c == b"\x04": # ctrl-D
17+
# special handling needed for ctrl-D if filesystem is mounted
18+
state.transport.write_ctrl_d(console_out_write)
19+
elif c == b"\x0a" and code_to_inject is not None: # ctrl-j, inject code
20+
state.transport.serial.write(code_to_inject)
21+
elif c == b"\x0b" and file_to_inject is not None: # ctrl-k, inject script
22+
console_out_write(bytes("Injecting %s\r\n" % file_to_inject, "utf8"))
23+
state.transport.enter_raw_repl(soft_reset=False)
24+
with open(file_to_inject, "rb") as f:
25+
pyfile = f.read()
26+
try:
27+
state.transport.exec_raw_no_follow(pyfile)
28+
except TransportError as er:
29+
console_out_write(b"Error:\r\n")
30+
console_out_write(er)
31+
state.transport.exit_raw_repl()
32+
else:
33+
state.transport.serial.write(c)
34+
3535
n = state.transport.serial.inWaiting()
36-
except OSError as er:
37-
if er.args[0] == 5: # IO error, device disappeared
38-
print("device disconnected")
39-
break
36+
if n > 0:
37+
dev_data_in = state.transport.serial.read(n)
38+
if dev_data_in is not None:
39+
if escape_non_printable:
40+
# Pass data through to the console, with escaping of non-printables.
41+
console_data_out = bytearray()
42+
for c in dev_data_in:
43+
if c in (8, 9, 10, 13, 27) or 32 <= c <= 126:
44+
console_data_out.append(c)
45+
else:
46+
console_data_out.extend(b"[%02x]" % c)
47+
else:
48+
console_data_out = dev_data_in
49+
console_out_write(console_data_out)
4050

41-
if n > 0:
42-
dev_data_in = state.transport.serial.read(n)
43-
if dev_data_in is not None:
44-
if escape_non_printable:
45-
# Pass data through to the console, with escaping of non-printables.
46-
console_data_out = bytearray()
47-
for c in dev_data_in:
48-
if c in (8, 9, 10, 13, 27) or 32 <= c <= 126:
49-
console_data_out.append(c)
50-
else:
51-
console_data_out.extend(b"[%02x]" % c)
52-
else:
53-
console_data_out = dev_data_in
54-
console_out_write(console_data_out)
51+
except OSError as er:
52+
if _is_disconnect_exception(er):
53+
return True
54+
else:
55+
raise
56+
return False
5557

5658

5759
def do_repl(state, args):
@@ -86,7 +88,7 @@ def console_out_write(b):
8688
capture_file.flush()
8789

8890
try:
89-
do_repl_main_loop(
91+
return do_repl_main_loop(
9092
state,
9193
console,
9294
console_out_write,
@@ -98,3 +100,22 @@ def console_out_write(b):
98100
console.exit()
99101
if capture_file is not None:
100102
capture_file.close()
103+
104+
105+
def _is_disconnect_exception(exception):
106+
"""
107+
Check if an exception indicates device disconnect.
108+
109+
Returns True if the exception indicates the device has disconnected,
110+
False otherwise.
111+
"""
112+
if isinstance(exception, OSError):
113+
if hasattr(exception, 'args') and len(exception.args) > 0:
114+
# IO error, device disappeared
115+
if exception.args[0] == 5:
116+
return True
117+
# Check for common disconnect messages in the exception string
118+
exception_str = str(exception)
119+
disconnect_indicators = ["Write timeout", "Device disconnected", "ClearCommError failed"]
120+
return any(indicator in exception_str for indicator in disconnect_indicators)
121+
return False

0 commit comments

Comments
 (0)