4"""USB CDC ACM round-trip echo verifier for the EK-RA8D2 USB demos.
7 Open the target tty in raw mode using a non-blocking POSIX file descriptor
8 (NOT pyserial's Serial wrapper -- pyserial toggles DTR which can hang on
9 the USB bridge during enumeration), write a fixed payload, then read back
10 for ``--timeout`` seconds. The round-trip succeeds iff the bytes read
11 exactly equal the bytes written.
14 0 -- payload round-tripped intact
15 1 -- no data received within --timeout seconds
16 2 -- data received but does not match the payload
17 3 -- usage / configuration / I/O error (no device, etc.)
20 python3 scripts/hil/usb/cdc_echo_test.py --tty /dev/cu.usbmodem000000011
22Auto-detect example (FS demo):
23 python3 scripts/hil/usb/cdc_echo_test.py --auto fs
26from __future__
import annotations
37from pathlib
import Path
41DEFAULT_PAYLOAD = b
"PING-RA8D2"
42DEFAULT_TIMEOUT_S = 4.0
49PRODUCT_SUBSTR_FS =
"EK-RA8D2 CDC Echo"
50PRODUCT_SUBSTR_HS =
"EK-RA8D2 HS CDC Echo"
53def _ioreg_lookup(product_substr: str) -> str |
None:
54 """Return the /dev/cu.usbmodem* path whose USB Product string matches.
56 Uses macOS ioreg to map the USB product description back to the BSD
57 device node. Returns None if no matching device is found or ioreg is
58 unavailable (e.g. on Linux).
61 out = subprocess.check_output(
62 [
"ioreg",
"-p",
"IOUSB",
"-l",
"-w",
"0"],
63 stderr=subprocess.DEVNULL,
65 ).decode(
"utf-8", errors=
"replace")
66 except (FileNotFoundError, subprocess.SubprocessError):
72 lines = out.splitlines()
73 for idx, line
in enumerate(lines):
74 if product_substr
in line:
75 window = lines[max(0, idx - 40) : idx + 40]
77 if "IOCalloutDevice" in w
and "/dev/cu.usbmodem" in w:
79 start = w.find(
'"/dev/cu.')
82 end = w.find(
'"', start + 1)
85 return w[start + 1 : end]
89def auto_detect(speed: str) -> str |
None:
90 """Resolve the EK-RA8D2 tty for the given speed ('fs' or 'hs').
93 1. ioreg lookup by USB Product string (most reliable).
94 2. Fallback: scan /dev/cu.usbmodem* and pick the one whose serial
95 suffix matches the speed-specific iSerial pattern.
98 prod, serial = PRODUCT_SUBSTR_FS, SERIAL_FS
100 prod, serial = PRODUCT_SUBSTR_HS, SERIAL_HS
104 path = _ioreg_lookup(prod)
111 for cand
in sorted(Path(
"/dev").glob(
"cu.usbmodem*")):
112 if serial
in cand.name:
117def _open_raw(tty_path: str) -> int:
118 """Open the tty as a raw, non-blocking fd suitable for binary I/O."""
119 fd = os.open(tty_path, os.O_RDWR | os.O_NONBLOCK | os.O_NOCTTY)
122 attrs = termios.tcgetattr(fd)
123 iflag, oflag, cflag, lflag, ispeed, ospeed, cc = attrs
127 cflag = (cflag & ~termios.CSIZE) | termios.CS8 | termios.CREAD | termios.CLOCAL
129 cc[termios.VTIME] = 0
130 termios.tcsetattr(fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
134def _drain_stale(fd: int, window_s: float = 0.2) ->
None:
135 """Discard anything already in the RX buffer.
137 Leftover boot chatter would otherwise contaminate the echo comparison.
139 deadline = time.monotonic() + window_s
140 while time.monotonic() < deadline:
141 r, _, _ = select.select([fd], [], [], 0.05)
145 _ = os.read(fd, 4096)
146 except OSError
as exc:
147 if exc.errno
not in (errno.EAGAIN, errno.EWOULDBLOCK):
151def _write_all(fd: int, payload: bytes) ->
None:
152 """Write the whole payload, retrying on a would-block."""
154 while written < len(payload):
156 n = os.write(fd, payload[written:])
160 except OSError
as exc:
161 if exc.errno
in (errno.EAGAIN, errno.EWOULDBLOCK):
167def _read_until(fd: int, want: int, timeout_s: float) -> bytes:
168 """Read until ``want`` bytes have arrived or ``timeout_s`` expires."""
170 deadline = time.monotonic() + timeout_s
171 while len(rx) < want
and time.monotonic() < deadline:
172 remaining = deadline - time.monotonic()
173 r, _, _ = select.select([fd], [], [],
min(0.25, max(0.0, remaining)))
177 chunk = os.read(fd, 4096)
178 except OSError
as exc:
179 if exc.errno
in (errno.EAGAIN, errno.EWOULDBLOCK):
187def round_trip(tty_path: str, payload: bytes, timeout_s: float) -> tuple[int, bytes]:
188 """Write payload, read back for timeout_s, return (exit_code, received).
190 The function does not raise on I/O errors -- it converts them into the
191 documented exit codes so the CLI surface stays predictable.
193 fd = _open_raw(tty_path)
196 _write_all(fd, payload)
197 received = _read_until(fd, len(payload), timeout_s)
199 with contextlib.suppress(OSError):
204 if received[: len(payload)] != payload:
209def _build_parser() -> argparse.ArgumentParser:
210 """Build the command-line parser for this HIL probe."""
211 parser = argparse.ArgumentParser(
212 description=
"Round-trip echo test for the EK-RA8D2 USB CDC demos.",
216 help=
"Path to the CDC tty (e.g. /dev/cu.usbmodem000000011).",
220 choices=(
"fs",
"hs"),
221 help=
"Auto-detect the EK-RA8D2 tty for the given USB speed.",
225 default=DEFAULT_PAYLOAD.decode(
"ascii"),
226 help=
"ASCII payload to send (default: %(default)s).",
231 default=DEFAULT_TIMEOUT_S,
232 help=
"Seconds to wait for the echo (default: %(default)s).",
237def _resolve_tty(args: argparse.Namespace) -> str |
None:
238 """Resolve the tty to test, or None (having said why) if it cannot be.
240 Every failure here is a RIG problem, not a firmware one, which is why the
241 caller maps them all to exit 3 rather than to a test failure.
244 if not tty_path
and args.auto:
245 tty_path = auto_detect(args.auto)
247 print(f
"ERROR: could not auto-detect EK-RA8D2 {args.auto.upper()} tty", file=sys.stderr)
249 print(f
"auto-detected: {tty_path}")
251 print(
"ERROR: must pass --tty or --auto {fs,hs}", file=sys.stderr)
253 if not Path(tty_path).exists():
254 print(f
"ERROR: tty does not exist: {tty_path}", file=sys.stderr)
259def main(argv: list[str]) -> int:
260 """Echo-test one CDC tty and map the outcome onto a three-way exit code.
262 The exit codes are the interface the HIL suite consumes, and they separate
263 the two failures that get confused: 3 means the test could not RUN (no
264 tty given, auto-detect failed, path absent, non-ASCII payload, I/O error)
265 while 1 and 2 mean it ran and the device failed it -- 1 for silence within
266 the timeout, 2 for data that came back different. A rig problem therefore
267 never reads as a firmware problem.
269 Returns 0 on an exact round-trip, 1 on timeout, 2 on mismatch, 3 on any
272 args = _build_parser().parse_args(argv)
274 tty_path = _resolve_tty(args)
279 payload_bytes = args.payload.encode(
"ascii")
280 except UnicodeEncodeError:
281 print(
"ERROR: payload must be ASCII-only", file=sys.stderr)
285 code, received = round_trip(tty_path, payload_bytes, args.timeout)
286 except OSError
as exc:
287 print(f
"ERROR: I/O failure on {tty_path}: {exc}", file=sys.stderr)
291 print(f
"OK: round-tripped {len(payload_bytes)} bytes")
295 f
"FAIL: no data received within {args.timeout:.1f}s on {tty_path}",
300 f
"FAIL: data mismatch on {tty_path}\n"
301 f
" expected: {payload_bytes!r}\n"
302 f
" received: {received!r}",
308if __name__ ==
"__main__":
309 sys.exit(
main(sys.argv[1:]))
void main(void)
The application entry point Reset_Handler hands control to.
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.