Skip to content

fix(autoresearch): correct port auto-detect for teensy* + data-driven USB vendor names - #3610

Closed
zackees wants to merge 1 commit into
masterfrom
fix/teensy-port-autodetect
Closed

fix(autoresearch): correct port auto-detect for teensy* + data-driven USB vendor names#3610
zackees wants to merge 1 commit into
masterfrom
fix/teensy-port-autodetect

Conversation

@zackees

@zackees zackees commented Jul 6, 2026

Copy link
Copy Markdown
Member

Problem

bash autoresearch teensy41 auto-detected the wrong port — an ESP32's CP2102 on COM11 instead of the Teensy on COM20. ENVIRONMENT_TO_VCOM_VID_PIDS had no teensy* entry, so selection fell through to the generic USB heuristic that prefers CP2102/CH340/FTDI adapters.

Fix

  • Add PJRC Teensy fingerprints (16C0:0483 / 16C0:0489) for teensy40/41/lc/31/35/36/mm.
  • Match VID:PIDs in declared preference order (not a set) so an entry lists its unambiguous fingerprint first. LPC8xx now keys only on LPC-Link2 1FC9:013216C0:0483 is dropped there because on Windows the legacy LPCXpresso-VCOM firmware is byte-identical to a Teensy (same VID:PID, generic "USB Serial Device"), so keeping it made lpc* grab a co-attached Teensy.
  • New ci/util/usb_vendors.py: lazily decompresses the usb-vendors.tar.zst VID→vendor table into memory on first use (mirrors fbuild's embedded resolver) to annotate port diagnostics — e.g. COM11=10C4:EA60 (Silicon Labs).

Verified

teensy41 → COM20, lpc845 → 1FC9:0132 against live attached ports; ci/tests/test_usb_vendors.py (3) pass; ruff + KBI clean.

Companion to FastLED/fbuild#982 (fbuild embeds the same VID:PID resolution from the FastLED/boards pipeline) and FastLED/fbuild#962 (Teensy shown in fbuild port scan).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • USB device detection now shows vendor names alongside VID:PID details in diagnostic output.
    • Added support for looking up USB vendor names from a bundled vendor list.
  • Bug Fixes

    • Port detection now prefers matches in a consistent order when multiple VID:PID options are available.
    • Some LPC environment detections were tightened to avoid matching older, incorrect USB fingerprints.
    • Missing or unreadable vendor data now fails gracefully instead of breaking detection.

…USB vendor names

`bash autoresearch teensy41` auto-detected the WRONG port (an ESP32's
CP2102 on COM11) instead of the Teensy: `ENVIRONMENT_TO_VCOM_VID_PIDS` had
no teensy entry, so port selection fell through to the generic USB
heuristic that prefers CP2102/CH340/FTDI adapters.

- Add PJRC Teensy fingerprints (16C0:0483 / 16C0:0489) for teensy40/41/lc/
  31/35/36/mm so teensy* selects the Teensy CDC port precisely.
- Match VID:PIDs in DECLARED PREFERENCE ORDER (not a set), so an entry can
  list its unambiguous fingerprint first — LPC8xx now keys only on the
  LPC-Link2 1FC9:0132 (16C0:0483 is dropped there: on Windows the legacy
  LPCXpresso-VCOM firmware is byte-identical to a Teensy — same VID:PID,
  generic "USB Serial Device" — so keeping it made lpc* grab a co-attached
  Teensy).
- New `ci/util/usb_vendors.py`: lazy-decompress the `usb-vendors.tar.zst`
  VID→vendor table into memory on first use (mirrors fbuild's embedded
  resolver), used to annotate port-detection diagnostics
  ("COM11=10C4:EA60 (Silicon Labs)").

Tests: ci/tests/test_usb_vendors.py (3) pass; teensy41→COM20, lpc845→
1FC9:0132 verified against live attached ports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a lazy-loading USB vendor ID-to-name resolver (ci/util/usb_vendors.py) that decompresses a bundled vendor archive on first use. ci/util/port_utils.py is updated to use this resolver for port diagnostics, tightens VID:PID fingerprints for lpc environments, and switches to deterministic ordered VID:PID matching. New unit tests cover the vendor resolver.

Changes

USB vendor lookup and port matching

Layer / File(s) Summary
Vendor table loader and public API
ci/util/usb_vendors.py
New module lazily decompresses a bundled vendor archive, parses VID:vendor pairs with a size cap, caches results, and exposes vendor_name(vid) and vendor_count() with safe fallbacks on failure.
Port matching and diagnostics wiring
ci/util/port_utils.py
Imports vendor_name, tightens ENVIRONMENT_TO_VCOM_VID_PIDS for lpc environments to drop the legacy VID:PID, adds _describe_port() for vendor-annotated diagnostics, and replaces set-based VID:PID matching with deterministic ordered iteration in auto_detect_upload_port().
Vendor module tests
ci/tests/test_usb_vendors.py
New tests verify lazy table loading, VID resolution, None-safety of vendor_name, and _describe_port formatting with and without VID/PID data.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant auto_detect_upload_port
  participant Ports
  participant _describe_port
  participant vendor_name
  participant _vendor_map

  auto_detect_upload_port->>Ports: enumerate connected ports
  auto_detect_upload_port->>auto_detect_upload_port: iterate declared VID:PID pairs in order
  alt matching port found
    auto_detect_upload_port-->>auto_detect_upload_port: return first matching port
  else no match
    auto_detect_upload_port->>_describe_port: describe each detected port
    _describe_port->>vendor_name: resolve VID to vendor name
    vendor_name->>_vendor_map: load/get cached vendor table
    _vendor_map-->>vendor_name: vendor dict
    vendor_name-->>_describe_port: vendor name or None
    _describe_port-->>auto_detect_upload_port: "COMx=VID:PID (Vendor)"
    auto_detect_upload_port-->>auto_detect_upload_port: raise error with formatted port list
  end
Loading

Related PRs: None specified.

Suggested labels: ci, testing

Suggested reviewers: None specified.

🐰 A vendor's name, once locked in a tar,
Now whispers to ports, near and far,
LPC-Link finds its lone true friend,
While sets give way to order, end to end,
Tests confirm the magic's not bizarre.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: fixing autoresearch port auto-detection for Teensy boards and adding data-driven USB vendor names.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/teensy-port-autodetect

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
ci/tests/test_usb_vendors.py (1)

26-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Mock classes may fail static type checking against _describe_port.

_describe_port is typed to accept ListPortInfo (a concrete pyserial class per ci/util/port_utils.py). _P and _NoIds duck-type the needed attributes but don't inherit from or structurally satisfy that type, which mypy/pyright would flag as an incompatible argument. Runtime behavior is fine; only static analysis is at risk.

🔧 Suggested fix using cast or a minimal subclass
+from typing import cast
+from serial.tools.list_ports_common import ListPortInfo
+
 def test_describe_port_appends_vendor() -> None:
     class _P:
         device = "COM7"
         vid = 0x303A
         pid = 0x1001

     class _NoIds:
         device = "COM8"
         vid = None
         pid = None

-    assert _describe_port(_P()) == "COM7=303A:1001 (Espressif Systems)"
-    assert _describe_port(_NoIds()) == "COM8=----:----"
+    assert _describe_port(cast(ListPortInfo, _P())) == "COM7=303A:1001 (Espressif Systems)"
+    assert _describe_port(cast(ListPortInfo, _NoIds())) == "COM8=----:----"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ci/tests/test_usb_vendors.py` around lines 26 - 37, The test uses mock helper
classes that don’t match the concrete ListPortInfo type expected by
_describe_port, which can trigger static type-checking errors. Update the
assertions in test_usb_vendors.py to either cast the mock instances to the
expected type or make the mocks inherit from a minimal ListPortInfo-compatible
base so mypy/pyright accepts the call while preserving the same runtime
behavior. Keep the fix localized around _describe_port and the _P/_NoIds test
doubles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ci/util/port_utils.py`:
- Around line 85-89: The fingerprint mapping for lpc845brk no longer matches the
expectations in the tests. Either keep 16C0:0483 in the lpc845brk entry in
port_utils, or update the related tests to the new fingerprint set. Make the
change consistent with test_lpc845brk_lpcxpresso_vcom_fingerprint_matches and
test_lpc845brk_neither_fingerprint_matches_reports_both so the expected VID:PID
values and error text align.

In `@ci/util/usb_vendors.py`:
- Around line 60-75: The lazy import in usb_vendors.py can hide a missing
dependency, causing fresh installs to silently fall back to an empty vendor map.
Make zstandard an explicit dependency in pyproject.toml or change the decompress
path in the usb_vendors helper to clearly treat zstandard as optional and
log/handle the fallback intentionally, keeping the behavior around
_MAX_DECOMPRESSED_BYTES and the usb-vendors.txt extraction unchanged.

---

Nitpick comments:
In `@ci/tests/test_usb_vendors.py`:
- Around line 26-37: The test uses mock helper classes that don’t match the
concrete ListPortInfo type expected by _describe_port, which can trigger static
type-checking errors. Update the assertions in test_usb_vendors.py to either
cast the mock instances to the expected type or make the mocks inherit from a
minimal ListPortInfo-compatible base so mypy/pyright accepts the call while
preserving the same runtime behavior. Keep the fix localized around
_describe_port and the _P/_NoIds test doubles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e24e5117-e796-40cc-96d1-221e896a3398

📥 Commits

Reviewing files that changed from the base of the PR and between 61b9d62 and c0de22a.

⛔ Files ignored due to path filters (1)
  • ci/data/usb-vendors.tar.zst is excluded by !**/*.zst
📒 Files selected for processing (3)
  • ci/tests/test_usb_vendors.py
  • ci/util/port_utils.py
  • ci/util/usb_vendors.py

Comment thread ci/util/port_utils.py
Comment on lines +85 to +89
"lpc845brk": ((0x1FC9, 0x0132),),
"lpc845": ((0x1FC9, 0x0132),),
"lpc804": ((0x1FC9, 0x0132),),
"lpcxpresso845max": ((0x1FC9, 0x0132),),
"lpcxpresso804": ((0x1FC9, 0x0132),),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C2 '16C0:0483|lpc845brk_lpcxpresso_vcom_fingerprint_matches|neither_fingerprint_matches_reports_both' ci/tests/test_autoresearch_phases.py

Repository: FastLED/FastLED

Length of output: 1039


Keep 16C0:0483 for lpc845brk, or update the matching tests in this PR.
test_lpc845brk_lpcxpresso_vcom_fingerprint_matches still expects 16C0:0483, and test_lpc845brk_neither_fingerprint_matches_reports_both still asserts the error message includes both 16C0:0483 and 1FC9:0132. Removing the old VID:PID from lpc845brk will break those checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ci/util/port_utils.py` around lines 85 - 89, The fingerprint mapping for
lpc845brk no longer matches the expectations in the tests. Either keep 16C0:0483
in the lpc845brk entry in port_utils, or update the related tests to the new
fingerprint set. Make the change consistent with
test_lpc845brk_lpcxpresso_vcom_fingerprint_matches and
test_lpc845brk_neither_fingerprint_matches_reports_both so the expected VID:PID
values and error text align.

Comment thread ci/util/usb_vendors.py
Comment on lines +60 to +75
try:
import zstandard

decompressed = zstandard.ZstdDecompressor().decompress(
raw, max_output_size=_MAX_DECOMPRESSED_BYTES
)
with tarfile.open(fileobj=io.BytesIO(decompressed)) as tf:
member = tf.extractfile("usb-vendors.txt")
if member is None:
return {}
text = member.read().decode("utf-8", "replace")
except KeyboardInterrupt as ki:
handle_keyboard_interrupt(ki)
raise
except Exception: # noqa: BLE001 — any decode failure → empty map fallback
return {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether zstandard is a declared project dependency
fd -t f 'pyproject.toml|requirements.*\.txt|setup.cfg|setup.py' --exec rg -n -i 'zstandard' {} +

Repository: FastLED/FastLED

Length of output: 195


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Dependency/manifests in repo:\n'
git ls-files | rg '(^|/)(pyproject\.toml|requirements[^/]*\.txt|setup\.cfg|setup\.py|poetry\.lock|Pipfile|Pipfile\.lock|uv\.lock|requirements\.in)$' || true

printf '\nSearch for zstandard references:\n'
git ls-files | xargs rg -n -i --no-heading '\bzstandard\b' || true

printf '\nTarget file existence:\n'
git ls-files 'ci/util/usb_vendors.py' || true

Repository: FastLED/FastLED

Length of output: 420


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== manifests containing zstandard =='
for f in $(git ls-files | rg '(^|/)(pyproject\.toml|requirements[^/]*\.txt|setup\.cfg|setup\.py|poetry\.lock|Pipfile|Pipfile\.lock|uv\.lock|requirements\.in)$'); do
  if rg -n -i '\bzstandard\b' "$f"; then
    echo
  fi
done

echo '== ci/util/usb_vendors.py (relevant lines) =='
nl -ba ci/util/usb_vendors.py | sed -n '1,130p'

Repository: FastLED/FastLED

Length of output: 277


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== all tracked files mentioning zstandard =='
git ls-files | xargs rg -n -i --no-heading '\bzstandard\b' || true

echo
echo '== dependency-related tracked files =='
git ls-files | rg '(^|/)(pyproject\.toml|requirements[^/]*\.txt|setup\.cfg|setup\.py|tox\.ini|noxfile\.py|Pipfile|Pipfile\.lock|uv\.lock)$' || true

echo
echo '== target source excerpt =='
nl -ba ci/util/usb_vendors.py | sed -n '50,90p'

Repository: FastLED/FastLED

Length of output: 464


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== pyproject.toml references to zstandard =='
rg -n -i '\bzstandard\b' pyproject.toml || true

echo
echo '== ci/util/usb_vendors.py excerpt =='
sed -n '1,120p' ci/util/usb_vendors.py | cat -n

Repository: FastLED/FastLED

Length of output: 4647


Declare zstandard or make the fallback explicit. ci/util/usb_vendors.py imports it lazily, but it isn’t listed in pyproject.toml, so fresh installs will silently drop to {} and lose USB vendor resolution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ci/util/usb_vendors.py` around lines 60 - 75, The lazy import in
usb_vendors.py can hide a missing dependency, causing fresh installs to silently
fall back to an empty vendor map. Make zstandard an explicit dependency in
pyproject.toml or change the decompress path in the usb_vendors helper to
clearly treat zstandard as optional and log/handle the fallback intentionally,
keeping the behavior around _MAX_DECOMPRESSED_BYTES and the usb-vendors.txt
extraction unchanged.

Source: Coding guidelines

@zackees zackees closed this Jul 6, 2026
@fastled-project-sync fastled-project-sync Bot moved this from Triage to Done in FastLED Tracker Jul 6, 2026
@zackees
zackees deleted the fix/teensy-port-autodetect branch August 30, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant