macOS: do not reset the box on connect
On macOS/Linux the OS asserts DTR and RTS when the port is opened and pyserial then writes DTR before RTS, passing through RTS=1/DTR=0 - the USB-Serial-JTAG reset state. Every Connect restarted the box. Drop RTS before open and DTR right after it on posix; Windows is unchanged. Verified on macOS 27 / arm64 with box A4:CB:8F:C9:47:3C (fw 1.0.16, build 3ae273ec): connect keeps the box running, Reset & capture still restarts it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
834bad2bce
commit
2b6d43eb45
2 files changed
+52
-3
No files matched your search
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import enum
|
import enum
|
||||||
import queue
|
import queue
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
@@ -92,13 +93,21 @@ class SerialManager:
|
|||||||
ser = serial.serial_for_url(
|
ser = serial.serial_for_url(
|
||||||
info.device, baudrate=self._baud, timeout=READ_TIMEOUT_S, do_not_open=True
|
info.device, baudrate=self._baud, timeout=READ_TIMEOUT_S, do_not_open=True
|
||||||
)
|
)
|
||||||
# Deassert both lines before open. On USB-Serial-JTAG, RTS=1/DTR=0
|
# On USB-Serial-JTAG, RTS=1/DTR=0 holds the chip in reset and DTR=1
|
||||||
# holds the chip in reset and DTR=1 during reset selects download
|
# during reset selects download mode.
|
||||||
# mode; pyserial would otherwise assert both on open (Windows).
|
# Windows: pyserial applies both lines atomically at open, so
|
||||||
|
# deassert both beforehand (it would otherwise assert both).
|
||||||
|
# macOS/Linux: the OS asserts both lines on open and pyserial then
|
||||||
|
# writes DTR before RTS, passing through RTS=1/DTR=0 and resetting
|
||||||
|
# the box on every connect. Drop RTS first, then DTR.
|
||||||
|
posix = sys.platform != "win32"
|
||||||
|
if not posix:
|
||||||
ser.dtr = False
|
ser.dtr = False
|
||||||
ser.rts = False
|
ser.rts = False
|
||||||
try:
|
try:
|
||||||
ser.open()
|
ser.open()
|
||||||
|
if posix:
|
||||||
|
ser.dtr = False
|
||||||
except (serial.SerialException, OSError):
|
except (serial.SerialException, OSError):
|
||||||
return None
|
return None
|
||||||
self._target.device = info.device # COM number / node may change
|
self._target.device = info.device # COM number / node may change
|
||||||
|
|||||||
@@ -86,6 +86,46 @@ def test_hard_reset_pushes_rts_via_dtr_write(monkeypatch):
|
|||||||
assert all(c[1] is False for c in ser.calls if c[0] == "dtr")
|
assert all(c[1] is False for c in ser.calls if c[0] == "dtr")
|
||||||
|
|
||||||
|
|
||||||
|
class _PosixOpenSerial:
|
||||||
|
"""pyserial on macOS/Linux: the OS asserts both lines on open, then
|
||||||
|
pyserial writes the requested DTR, then RTS. Records every line state."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.port = args[0] if args else "fake"
|
||||||
|
self.dtr = self.rts = True # pyserial defaults
|
||||||
|
self.states = []
|
||||||
|
|
||||||
|
def __setattr__(self, name, value):
|
||||||
|
object.__setattr__(self, name, value)
|
||||||
|
if name in ("dtr", "rts") and getattr(self, "is_open", False):
|
||||||
|
self.states.append((self.dtr, self.rts))
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
want_dtr, want_rts = self.dtr, self.rts
|
||||||
|
object.__setattr__(self, "dtr", True)
|
||||||
|
object.__setattr__(self, "rts", True)
|
||||||
|
self.is_open = True
|
||||||
|
self.states.append((True, True))
|
||||||
|
self.dtr = want_dtr
|
||||||
|
self.rts = want_rts
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_on_posix_never_passes_through_reset(monkeypatch):
|
||||||
|
monkeypatch.setattr(mgr_mod.sys, "platform", "darwin")
|
||||||
|
monkeypatch.setattr(mgr_mod, "find_port", lambda sn, dev: LOOP)
|
||||||
|
opened = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
mgr_mod.serial, "serial_for_url",
|
||||||
|
lambda *a, **k: opened.append(_PosixOpenSerial(*a, **k)) or opened[-1],
|
||||||
|
)
|
||||||
|
m = SerialManager(lambda ln: None, lambda s, d: None)
|
||||||
|
m._target = mgr_mod._Target(LOOP.device, None)
|
||||||
|
ser = m._open()
|
||||||
|
# (DTR, RTS) = (False, True) resets the chip on connect.
|
||||||
|
assert (False, True) not in ser.states
|
||||||
|
assert ser.states[-1] == (False, False)
|
||||||
|
|
||||||
|
|
||||||
def test_waits_when_port_missing(monkeypatch):
|
def test_waits_when_port_missing(monkeypatch):
|
||||||
monkeypatch.setattr(mgr_mod, "find_port", lambda sn, dev: None)
|
monkeypatch.setattr(mgr_mod, "find_port", lambda sn, dev: None)
|
||||||
states, lines = [], []
|
states, lines = [], []
|
||||||
|
|||||||
Reference in new issue
Block a user