From 9fc332b7910659f924d01bb23aa7129e1508cbcb Mon Sep 17 00:00:00 2001 From: Ramon Roche Date: Fri, 10 Jul 2026 10:24:31 -0700 Subject: [PATCH 1/3] tooling: check board USB IDs against the Dronecode registry Boards declaring the Dronecode USB VID (0x3643) must use a product ID registered to their manufacturer in https://github.com/Dronecode/usb-ids. CI checks only the defconfigs changed by a PR; other vendor IDs are ignored. --- .github/workflows/usb_ids.yml | 45 +++++++++ Tools/check_usb_ids.py | 171 ++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 .github/workflows/usb_ids.yml create mode 100644 Tools/check_usb_ids.py diff --git a/.github/workflows/usb_ids.yml b/.github/workflows/usb_ids.yml new file mode 100644 index 000000000000..20b6fddc7704 --- /dev/null +++ b/.github/workflows/usb_ids.yml @@ -0,0 +1,45 @@ +name: USB ID Check + +on: + push: + branches: [main, 'release/**'] + paths: ['boards/**'] + pull_request: + paths: ['boards/**'] + +permissions: + contents: read + pull-requests: read + +jobs: + check: + name: Check changed board USB IDs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Find changed defconfigs + id: changed + env: + GH_TOKEN: ${{ github.token }} + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \ + --paginate --jq '.[] | select(.status != "removed") | .filename' > changed.txt + elif [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then + git show --pretty=format: --name-only "${{ github.sha }}" > changed.txt + else + gh api "repos/${{ github.repository }}/compare/${{ github.event.before }}...${{ github.sha }}" \ + --jq '.files[] | select(.status != "removed") | .filename' > changed.txt + fi + grep -E '^boards/[^/]+/[^/]+/nuttx-config/.+/defconfig$' changed.txt > defconfigs.txt || true + echo "count=$(wc -l < defconfigs.txt | tr -d ' ')" >> "$GITHUB_OUTPUT" + cat defconfigs.txt + + - name: Install PyYAML + if: steps.changed.outputs.count != '0' + run: pip install --user pyyaml + + - name: Check USB IDs against the Dronecode registry + if: steps.changed.outputs.count != '0' + run: xargs -a defconfigs.txt python3 Tools/check_usb_ids.py check diff --git a/Tools/check_usb_ids.py b/Tools/check_usb_ids.py new file mode 100644 index 000000000000..62d7270e9611 --- /dev/null +++ b/Tools/check_usb_ids.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Check board USB IDs against the Dronecode USB ID registry. + +Boards declaring the Dronecode USB vendor ID (0x3643) in their NuttX +defconfigs must use a product ID registered to their manufacturer in +https://github.com/Dronecode/usb-ids. Boards using other vendor IDs are +ignored. + +Usage: + check_usb_ids.py check [ ...] + check_usb_ids.py lookup <0xNNNN | boards/[/...]> + +The registry is fetched from the usb-ids repo main branch by default; +use --registry to check against a local copy. + +Only dependency: PyYAML. +""" + +import argparse +import re +import sys +import urllib.request + +import yaml + +REGISTRY_URL = "https://raw.githubusercontent.com/Dronecode/usb-ids/main/usb-ids.yaml" + +CONFIG_RE = re.compile( + r'^CONFIG_CDCACM_(VENDORID|PRODUCTID|VENDORSTR|PRODUCTSTR)=("?)(.*?)\2\s*$' +) + + +def load_registry(args): + if args.registry: + with open(args.registry, encoding="utf-8") as f: + doc = yaml.safe_load(f) + else: + with urllib.request.urlopen(args.registry_url, timeout=30) as r: + doc = yaml.safe_load(r.read()) + + registry = { + "vid": int(doc["vid"], 16), + "vendor_string": doc["vendor_string"], + "pids": {}, # pid int -> {"manufacturer", "board", "px4_vendor"} + "vendors": {}, # px4_vendor slug -> manufacturer name + } + for mfr in doc["manufacturers"]: + slug = mfr.get("px4_vendor") + if slug: + registry["vendors"][slug] = mfr["name"] + for entry in mfr["pids"]: + registry["pids"][int(entry["pid"], 16)] = { + "manufacturer": mfr["name"], + "board": entry["board"], + "px4_vendor": slug, + } + return registry + + +def parse_defconfig(path): + values = {} + with open(path, encoding="utf-8") as f: + for line in f: + m = CONFIG_RE.match(line) + if m: + values[m.group(1)] = m.group(3) + return values + + +def board_vendor(path): + """Vendor directory name from a path like boards///...""" + parts = path.replace("\\", "/").split("/") + try: + return parts[parts.index("boards") + 1] + except (ValueError, IndexError): + return None + + +def cmd_check(registry, paths): + violations = [] + checked = 0 + for path in paths: + values = parse_defconfig(path) + vid = values.get("VENDORID") + if vid is None or int(vid, 16) != registry["vid"]: + continue + checked += 1 + + pid_raw = values.get("PRODUCTID") + if pid_raw is None: + violations.append(f"{path}: VID 0x3643 set but no CONFIG_CDCACM_PRODUCTID") + continue + pid = int(pid_raw, 16) + + entry = registry["pids"].get(pid) + if entry is None: + violations.append( + f"{path}: PID {pid_raw} is not registered in the Dronecode USB ID " + "registry (https://github.com/Dronecode/usb-ids)" + ) + continue + + vendor = board_vendor(path) + if entry["px4_vendor"] is None: + violations.append( + f"{path}: PID {pid_raw} is registered to '{entry['manufacturer']}' " + "but the registry has no px4_vendor mapping for them; add it to " + "usb-ids.yaml first" + ) + elif vendor != entry["px4_vendor"]: + violations.append( + f"{path}: PID {pid_raw} is registered to '{entry['manufacturer']}' " + f"(boards/{entry['px4_vendor']}/), not to boards/{vendor}/" + ) + + vendorstr = values.get("VENDORSTR") + allowed = {registry["vendor_string"], entry["manufacturer"]} + if vendorstr not in allowed: + violations.append( + f"{path}: CONFIG_CDCACM_VENDORSTR \"{vendorstr}\" must be one of: " + + ", ".join(f'"{s}"' for s in sorted(allowed)) + ) + + for v in violations: + print(f"error: {v}", file=sys.stderr) + print( + f"checked {checked} defconfig(s) with VID 0x3643, " + f"{len(violations)} violation(s)" + ) + return 1 if violations else 0 + + +def cmd_lookup(registry, query): + if query.lower().startswith("0x"): + entry = registry["pids"].get(int(query, 16)) + if entry is None: + print(f"{query}: not registered") + return 1 + print(f"{query}: {entry['manufacturer']}, board: {entry['board']}") + return 0 + + vendor = board_vendor(query) or query + name = registry["vendors"].get(vendor) + if name is None: + print(f"boards/{vendor}/: no manufacturer registered for this vendor directory") + return 1 + pids = sorted(p for p, e in registry["pids"].items() if e["manufacturer"] == name) + pid_list = ", ".join(f"0x{p:04X}" for p in pids) + print(f"boards/{vendor}/: {name}, PIDs: {pid_list}") + return 0 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--registry", help="path to a local usb-ids.yaml") + parser.add_argument("--registry-url", default=REGISTRY_URL) + sub = parser.add_subparsers(dest="command", required=True) + p_check = sub.add_parser("check", help="check defconfig files") + p_check.add_argument("paths", nargs="+") + p_lookup = sub.add_parser("lookup", help="look up a PID or board path") + p_lookup.add_argument("query") + args = parser.parse_args() + + registry = load_registry(args) + if args.command == "check": + return cmd_check(registry, args.paths) + return cmd_lookup(registry, args.query) + + +if __name__ == "__main__": + sys.exit(main()) From 14ee22689ce3eae25f41d4b2ae8b7b361fe3da66 Mon Sep 17 00:00:00 2001 From: Ramon Roche Date: Fri, 10 Jul 2026 10:30:17 -0700 Subject: [PATCH 2/3] tooling: move USB ID CI logic into Tools/ci/usb_ids_runner.sh Keep the workflow file trivial; the changed-file detection and checker invocation live in a script named after the workflow, following the build_all_runner.sh convention. --- .github/workflows/usb_ids.yml | 27 ++++--------------------- Tools/ci/usb_ids_runner.sh | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 23 deletions(-) create mode 100755 Tools/ci/usb_ids_runner.sh diff --git a/.github/workflows/usb_ids.yml b/.github/workflows/usb_ids.yml index 20b6fddc7704..c425a562a839 100644 --- a/.github/workflows/usb_ids.yml +++ b/.github/workflows/usb_ids.yml @@ -18,28 +18,9 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Find changed defconfigs - id: changed + - name: Check USB IDs against the Dronecode registry env: GH_TOKEN: ${{ github.token }} - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \ - --paginate --jq '.[] | select(.status != "removed") | .filename' > changed.txt - elif [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then - git show --pretty=format: --name-only "${{ github.sha }}" > changed.txt - else - gh api "repos/${{ github.repository }}/compare/${{ github.event.before }}...${{ github.sha }}" \ - --jq '.files[] | select(.status != "removed") | .filename' > changed.txt - fi - grep -E '^boards/[^/]+/[^/]+/nuttx-config/.+/defconfig$' changed.txt > defconfigs.txt || true - echo "count=$(wc -l < defconfigs.txt | tr -d ' ')" >> "$GITHUB_OUTPUT" - cat defconfigs.txt - - - name: Install PyYAML - if: steps.changed.outputs.count != '0' - run: pip install --user pyyaml - - - name: Check USB IDs against the Dronecode registry - if: steps.changed.outputs.count != '0' - run: xargs -a defconfigs.txt python3 Tools/check_usb_ids.py check + PR_NUMBER: ${{ github.event.pull_request.number }} + BEFORE_SHA: ${{ github.event.before }} + run: Tools/ci/usb_ids_runner.sh diff --git a/Tools/ci/usb_ids_runner.sh b/Tools/ci/usb_ids_runner.sh new file mode 100755 index 000000000000..df2fd3bff4db --- /dev/null +++ b/Tools/ci/usb_ids_runner.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# This script is meant to be used by the usb_ids.yml workflow in a github runner +# It finds the board defconfigs changed by the triggering PR or push and checks +# them against the Dronecode USB ID registry (https://github.com/Dronecode/usb-ids) +# using Tools/check_usb_ids.py +set -euo pipefail + +# Provided by the workflow / runner environment: +# GITHUB_EVENT_NAME, GITHUB_REPOSITORY, GITHUB_SHA (default runner env) +# GH_TOKEN - token for the gh api calls +# PR_NUMBER - pull request number (pull_request events only) +# BEFORE_SHA - github.event.before (push events only) + +changed=$(mktemp) + +if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then + gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --paginate \ + --jq '.[] | select(.status != "removed") | .filename' > "${changed}" +elif [ "${BEFORE_SHA}" = "0000000000000000000000000000000000000000" ]; then + # push to a new branch: no before-sha to diff against, check the head commit + git show --pretty=format: --name-only "${GITHUB_SHA}" > "${changed}" +else + gh api "repos/${GITHUB_REPOSITORY}/compare/${BEFORE_SHA}...${GITHUB_SHA}" \ + --jq '.files[] | select(.status != "removed") | .filename' > "${changed}" +fi + +defconfigs=$(grep -E '^boards/[^/]+/[^/]+/nuttx-config/.+/defconfig$' "${changed}" || true) + +if [ -z "${defconfigs}" ]; then + echo "No board defconfig changes, nothing to check." + exit 0 +fi + +echo "Changed defconfigs:" +echo "${defconfigs}" + +pip install --user --quiet pyyaml +echo "${defconfigs}" | xargs python3 Tools/check_usb_ids.py check From a12809a2f5cb72415213af10fb14b215f7150dc7 Mon Sep 17 00:00:00 2001 From: Ramon Roche Date: Fri, 10 Jul 2026 12:52:40 -0700 Subject: [PATCH 3/3] tooling: name the USB ID workflow after the Dronecode registry Make the status check self-explanatory in the PR checks list so maintainers treat failures as registry violations, not routine lint. --- .github/workflows/usb_ids.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/usb_ids.yml b/.github/workflows/usb_ids.yml index c425a562a839..e66858fdf45f 100644 --- a/.github/workflows/usb_ids.yml +++ b/.github/workflows/usb_ids.yml @@ -1,4 +1,4 @@ -name: USB ID Check +name: Dronecode USB ID Registry on: push: @@ -13,7 +13,7 @@ permissions: jobs: check: - name: Check changed board USB IDs + name: Board USB VID/PID must be registered runs-on: ubuntu-latest steps: - uses: actions/checkout@v4