diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index af82cc938..b91b619f9 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -45,6 +45,9 @@ commands normally. A detected USB disconnect/reset also ends the session. UART-to-USB adapters without a disconnect signal may require RETURN. If the radio2 settings change mid-session, USB stays quarantined until RETURN or disconnect, so leftover binary bytes cannot become CLI commands. +For Espressif USB-Serial/JTAG (`303A:1001`), open the host port with DTR and +RTS deasserted; those lines control boot/reset selection. Other USB CDC +devices, including nRF52/TinyUSB, retain their normal DTR handling. ESP32 also supports `set kiss.transport tcp`. Enable station WiFi separately and connect to its address on TCP **5004**. The service accepts one client, diff --git a/tools/hil/profile_switch.py b/tools/hil/profile_switch.py index 4b66a6912..240f4db78 100644 --- a/tools/hil/profile_switch.py +++ b/tools/hil/profile_switch.py @@ -16,11 +16,13 @@ _run_sequences = itertools.count(secrets.randbelow(0x3fffffff) + 1) def configure_session(port): - # Native Espressif/Seeed/Adafruit USB and CP2102/CH340 bridges differ in DTR semantics. - # DTR keeps the native session active; on a bridge it can drive BOOT/GPIO0. + # ESP32-C3/S3 USB Serial/JTAG (303A:1001) maps asserted DTR to IO0. + # Leave it released across a firmware reboot. TinyUSB/nRF52 CDC still + # needs DTR; UART bridges can also drive BOOT/GPIO0 and keep it released. port.rts = False device = os.path.realpath(str(port.port)).casefold() port.dtr = any(info.vid in (0x303A, 0x2886, 0x239A) + and (info.vid, getattr(info, "pid", None)) != (0x303A, 0x1001) and os.path.realpath(str(info.device)).casefold() == device for info in list_ports.comports()) diff --git a/tools/hil/test_profile_switch_host.py b/tools/hil/test_profile_switch_host.py index 3451769c0..68138b6e9 100644 --- a/tools/hil/test_profile_switch_host.py +++ b/tools/hil/test_profile_switch_host.py @@ -98,14 +98,15 @@ class ResultReaderTests(unittest.TestCase): master, slave = os.openpty() try: device = os.ttyname(slave) - for vid, expected in ((0x10C4, False), (0x1A86, False), - (0x303A, True), (0x2886, True), (0x239A, True)): - with self.subTest(vid=vid): + for vid, pid, expected in ((0x10C4, 0xEA60, False), (0x1A86, 0x7523, False), + (0x303A, 0x1001, False), (0x303A, 0x4001, True), + (0x2886, 0x8045, True), (0x239A, 0x8029, True)): + with self.subTest(vid=vid, pid=pid): port = PosixModemSerial() port.port = device try: with patch("profile_switch.list_ports.comports", return_value=[ - SimpleNamespace(device=device, vid=vid)]): + SimpleNamespace(device=device, vid=vid, pid=pid)]): open_session(port) self.assertFalse(port.has_reset_transition(), port.modem_edges) self.assertEqual(port.modem_lines, {"dtr": expected, "rts": False}) @@ -170,11 +171,12 @@ class ResultReaderTests(unittest.TestCase): self.assertTrue(result["transport_recovered"]) def test_native_usb_and_bridge_have_distinct_control_lines(self): - for vid, expected in ((0x303A, True), (0x2886, True), (0x239A, True), - (0x1A86, False), (0x10C4, False), (None, False)): + for vid, pid, expected in ((0x303A, 0x1001, False), (0x303A, 0x4001, True), + (0x2886, 0x8045, True), (0x239A, 0x8029, True), + (0x1A86, 0x7523, False), (0x10C4, 0xEA60, False), (None, None, False)): port = SessionPort("test") with patch("profile_switch.list_ports.comports", return_value=[ - SimpleNamespace(device="TEST", vid=vid), + SimpleNamespace(device="TEST", vid=vid, pid=pid), SimpleNamespace(device="different", vid=0x303A)]): configure_session(port) self.assertFalse(port.is_open) @@ -196,9 +198,9 @@ class ResultReaderTests(unittest.TestCase): bridge_id = by_id / "usb-Silicon_Labs_CP2102-if00-port0" native_id.symlink_to(native) bridge_id.symlink_to(bridge) - inventory = [SimpleNamespace(device=str(native), vid=0x303A), + inventory = [SimpleNamespace(device=str(native), vid=0x303A, pid=0x1001), SimpleNamespace(device=str(bridge), vid=0x10C4)] - for device, expected in ((native_id, True), (bridge_id, False)): + for device, expected in ((native_id, False), (bridge_id, False)): with self.subTest(device=device.name): port = SessionPort(str(device)) with patch("profile_switch.list_ports.comports", return_value=inventory):