From 3a19df702726f14c649af8c903f8d2146c7a61b5 Mon Sep 17 00:00:00 2001 From: hisayya <128133846+hisayya@users.noreply.github.com> Date: Sat, 15 Aug 2026 16:36:52 +0000 Subject: [PATCH] fix: detect Linux serial ports in deploy.sh deploy.sh only auto-detected the macOS Arduino path (/dev/cu.usbmodem*), so Linux users (native USB CDC at /dev/ttyACM*, UART bridges at /dev/ttyUSB*) had to set PORT manually or the script reported no port. Scan both OS families with a find_port helper, keep the macOS paths for regressions, and make the error message name every pattern checked. Also document the hf CLI dependency that fetch_model.sh needs, which the README previously omitted. Tests: SerialPortIsAutoDetected stubs ls and verifies each family's port is picked; all 29 deploy tests pass. --- README.md | 12 ++++++++++++ scripts/deploy.sh | 19 +++++++++++++++---- tests/test_deploy.py | 42 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3458ede..95c65b5 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,18 @@ scripts/deploy.sh barista # generate headers, run gates, compile, flash the model to be named, because the board holds one at a time and deploying replaces it. +Before the first fetch, install the Hugging Face CLI (`hf`), which +`fetch_model.sh` requires to download the released assets: + +```bash +uv tool install huggingface_hub # provides `hf` +``` + +`deploy.sh` auto-detects the board's serial port on Linux +(`/dev/ttyACM*` for native USB CDC, `/dev/ttyUSB*` for a UART bridge) and on +macOS (`/dev/cu.usbmodem*`). If several ports match, or detection does not fit +your wiring, pin it explicitly: `PORT=/dev/ttyACM0 scripts/deploy.sh barista`. + `fetch_model.sh` checks the inference assets against a SHA-256 and byte size pinned in the script, and cross-checks the release's own `metadata.json` against those same pins. It installs nothing unless every check passes, so a failed diff --git a/scripts/deploy.sh b/scripts/deploy.sh index c06ab89..d406fe4 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -167,10 +167,21 @@ find_esptool() { } ESPTOOL=$(find_esptool) -# `|| true`: a failing glob would otherwise abort the assignment under `set -e`, -# before the message below can print. -PORT=${PORT:-$(ls /dev/cu.usbmodem* 2>/dev/null | head -1 || true)} -[ -n "$PORT" ] || { echo "no /dev/cu.usbmodem* found; plug the board in, or set PORT=..." >&2; exit 1; } +# Both OS families name the same hardware differently: macOS calls the Arduino +# bridge /dev/cu.usbmodem* (and older FTDI/CP210x boards /dev/cu.usbserial-*), +# while Linux calls the native USB CDC device /dev/ttyACM* and a UART bridge +# /dev/ttyUSB*. Scan them all so the same command works on either platform. +# With more than one board attached the first match is arbitrary, so the +# message tells the user to set PORT to the exact path. +find_port() { + local pat + for pat in /dev/ttyACM* /dev/ttyUSB* /dev/cu.usbmodem* /dev/cu.usbserial-*; do + ls $pat 2>/dev/null && return 0 + done + return 0 +} +PORT=${PORT:-$(find_port | head -1)} +[ -n "$PORT" ] || { echo "no serial port found (looked for /dev/ttyACM*, /dev/ttyUSB*, /dev/cu.usbmodem*); plug the board in, or set PORT=/dev/ttyACM0 (Linux) or PORT=/dev/cu.usbmodemNNNN (macOS)" >&2; exit 1; } # Every required artifact is checked before anything is generated or built, so a # missing file is reported as a list rather than discovered halfway through. diff --git a/tests/test_deploy.py b/tests/test_deploy.py index 1f379a6..a84f66d 100644 --- a/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -351,9 +351,49 @@ def test_a_missing_port_stops_before_generating(self): "vocab.json", "layout.json"]) r = self.run_deploy("barista", PORT="") self.assertNotEqual(r.returncode, 0) - self.assertIn("no /dev/cu.usbmodem*", r.stderr) + self.assertIn("no serial port found", r.stderr) self.assertEqual(self.calls(), []) +class SerialPortIsAutoDetected(DeployHarness): + """deploy.sh picks a port when PORT is unset. The scan covers both OS + families: Linux names the board /dev/ttyACM* or /dev/ttyUSB*, macOS + /dev/cu.usbmodem*. These tests stub `ls` so they are independent of the + hardware attached to the runner.""" + + def setUp(self): + super().setUp() + self.artifacts("barista", ["model.bin", "tokenizer.json", + "vocab.json", "layout.json"]) + + def run_with_ls(self, ls_body): + stub_ls = self.bin / "ls" + stub_ls.write_text(ls_body) + os.chmod(stub_ls, 0o755) + return self.run_deploy("barista", PORT="") + + def test_linux_ttyACM_is_detected(self): + r = self.run_with_ls("""#!/bin/sh +case "$1" in + /dev/ttyACM*) echo /dev/ttyACM0; exit 0 ;; + *) exit 1 ;; +esac +""") + self.assertEqual(r.returncode, 0, r.stderr) + flash = [c for c in self.calls() if "write_flash" in c] + self.assertIn("/dev/ttyACM0", flash[0]) + + def test_macos_cu_usbmodem_is_detected(self): + r = self.run_with_ls("""#!/bin/sh +case "$1" in + /dev/cu.usbmodem*) echo /dev/cu.usbmodem1410; exit 0 ;; + *) exit 1 ;; +esac +""") + self.assertEqual(r.returncode, 0, r.stderr) + flash = [c for c in self.calls() if "write_flash" in c] + self.assertIn("/dev/cu.usbmodem1410", flash[0]) + + if __name__ == "__main__": unittest.main()