From 9ea57a8976c0dab1236d8a245e30caa2e6703dc0 Mon Sep 17 00:00:00 2001 From: Ramon Roche Date: Mon, 28 Sep 2026 12:14:28 -0700 Subject: [PATCH 1/3] Map registry PIDs to PX4 boards and check them against PX4 Add px4_board on PID entries and usb_vendor_string on manufacturers, and a check_px4.py cross-check against a PX4-Autopilot checkout that CI runs on every PR, push, and weekly. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Ramon Roche --- .github/ISSUE_TEMPLATE/pid-request.yml | 16 +++ .github/PULL_REQUEST_TEMPLATE.md | 1 + .github/workflows/validate.yml | 15 +++ README.md | 15 +++ check_px4.py | 143 +++++++++++++++++++++++++ usb-ids.yaml | 11 ++ validate.py | 63 +++++++++-- 7 files changed, 255 insertions(+), 9 deletions(-) create mode 100644 check_px4.py diff --git a/.github/ISSUE_TEMPLATE/pid-request.yml b/.github/ISSUE_TEMPLATE/pid-request.yml index 42b0eac..db59b2e 100644 --- a/.github/ISSUE_TEMPLATE/pid-request.yml +++ b/.github/ISSUE_TEMPLATE/pid-request.yml @@ -30,6 +30,22 @@ body: request claims one), one PID per board. validations: required: true + - type: input + id: px4_board + attributes: + label: PX4 board directory + description: >- + The `boards//` directory in PX4-Autopilot that will + use the PID, if you have one (e.g. `boards/acme/fc1`). Leave empty + if the board is not headed upstream yet. + - type: input + id: usb_vendor_string + attributes: + label: USB vendor string + description: >- + The exact `CONFIG_CDCACM_VENDORSTR` your boards' defconfigs set, if + it differs from the manufacturer name above. PX4 CI compares it + character for character, case included. - type: checkboxes id: membership attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 39667c9..8310275 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,4 +6,5 @@ - [ ] All PIDs are inside our claimed block(s) in `blocks` (first request: claim a free aligned 16-PID block, `0xNNN0`) - [ ] Contact email is valid and monitored +- [ ] `px4_board` is set only for boards upstream in PX4 (or in an open board PR), and `usb_vendor_string` matches their `CONFIG_CDCACM_VENDORSTR` when it differs from `name` - [ ] We are a Dronecode Foundation member (or state your affiliation below) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index bacdfa7..2273a92 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -4,6 +4,9 @@ on: push: branches: [main] pull_request: + schedule: + - cron: "0 6 * * 1" # weekly, catches drift on the PX4 side + workflow_dispatch: jobs: validate: @@ -15,3 +18,15 @@ jobs: python-version: "3.x" - run: pip install pyyaml - run: python3 validate.py usb-ids.yaml + + px4: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - run: pip install pyyaml + - run: git clone --depth 1 --filter=blob:none --sparse https://github.com/PX4/PX4-Autopilot.git px4 + - run: git -C px4 sparse-checkout set boards + - run: python3 check_px4.py px4 usb-ids.yaml diff --git a/README.md b/README.md index 92be6cf..5559264 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ PR, use the [PID request issue form](../../issues/new/choose). | `date` | Assignment date, `YYYY-MM-DD` | | `contact` | Email address for the manufacturer | | `px4_vendor` | Your vendor directory name in the PX4 `boards/` tree. Optional until you upstream a board; **required before your first PX4-Autopilot board PR**, otherwise PX4 CI will reject it. | +| `usb_vendor_string` | The exact `CONFIG_CDCACM_VENDORSTR` your PX4 boards ship. Optional; defaults to `name`, so set it only when the two differ. | +| `px4_board` | On a PID: the `/` directory under PX4's `boards/` that uses it (e.g. `siyi/n7`), inside your `px4_vendor`. Leave it off until the board is upstream in PX4, or add it in the same window as your board PR. | | `blocks` | List of claimed block starts, `"0x"` + 4 uppercase hex digits ending in `0`; each covers 16 PIDs (`0xNNN0`-`0xNNNF`), globally unique. Required before any non-legacy PID can be assigned. | | `legacy` | `true` on assignments that predate the block policy (before 2026-09). Maintainer-set, not for new requests. | @@ -60,6 +62,19 @@ python3 validate.py usb-ids.yaml Runs automatically on every PR and push to `main`. +`check_px4.py` checks the registry against a PX4-Autopilot checkout in both +directions: each `px4_board` must exist with matching +`CONFIG_CDCACM_VENDORID`, `CONFIG_CDCACM_PRODUCTID` and +`CONFIG_CDCACM_VENDORSTR` (the registry `vendor_string`, or the +manufacturer's `usb_vendor_string`), and every PX4 board using VID `0x3643` +must be claimed by some `px4_board`. A `px4_board` whose directory is not in +PX4 yet is only a notice. CI runs it against PX4 `main` on every PR, push, +and weekly. + +```sh +python3 check_px4.py path/to/PX4-Autopilot usb-ids.yaml +``` + Note for maintainers: PX4 CI (including release branches carrying older copies of `check_usb_ids.py`) fetches this file from `main` at check time, so schema changes must stay backward compatible: adding fields is fine, diff --git a/check_px4.py b/check_px4.py new file mode 100644 index 0000000..74f9575 --- /dev/null +++ b/check_px4.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Check usb-ids.yaml against a PX4-Autopilot checkout. + +Usage: check_px4.py [usb-ids.yaml] + +A PX4 board defconfig matches its registry entry when it lives at +boards//nuttx-config/*/defconfig and its CONFIG_CDCACM_VENDORID, +CONFIG_CDCACM_PRODUCTID (hex-int compares) and CONFIG_CDCACM_VENDORSTR +(exact: the registry vendor_string, or the manufacturer's +usb_vendor_string, which defaults to its name) agree with the registry. + +Both directions are checked: every entry with px4_board must match the +defconfigs in that directory, and every PX4 defconfig using the registry VID +must belong to some entry's px4_board. A px4_board whose directory does not +exist in PX4 yet is a notice, not an error, so a registry entry can land +before its board PR. Prints one line per finding and exits 1 on any error. + +Only dependency: PyYAML. +""" + +import sys +from pathlib import Path + +from validate import load, validate + +DEFCONFIG_GLOB = "nuttx-config/*/defconfig" +KEYS = ("CONFIG_CDCACM_VENDORID", "CONFIG_CDCACM_PRODUCTID", "CONFIG_CDCACM_VENDORSTR") + + +def read_defconfig(path): + """Return the CDCACM settings of a defconfig, strings unquoted.""" + values = {} + for line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + key, sep, value = line.partition("=") + if sep and key in KEYS: + if len(value) >= 2 and value[0] == value[-1] == '"': + value = value[1:-1] + values[key] = value + return values + + +def hex_int(value): + try: + return int(value, 16) + except (TypeError, ValueError): + return None + + +def check(doc, px4_dir): + """Return (errors, notices) comparing the registry to PX4's boards/.""" + errors, notices = [], [] + boards_dir = px4_dir / "boards" + vid = int(doc["vid"], 16) + + # px4_board -> (pid, accepted vendor strings) + entries = {} + for mfr in doc["manufacturers"]: + strings = {doc["vendor_string"], mfr.get("usb_vendor_string", mfr["name"])} + for entry in mfr["pids"]: + if "px4_board" in entry: + entries[entry["px4_board"]] = (entry["pid"], strings) + + # board -> [(defconfig path relative to PX4, settings)] for our VID only + px4_boards = {} + for path in sorted(boards_dir.glob(f"*/*/{DEFCONFIG_GLOB}")): + values = read_defconfig(path) + if hex_int(values.get("CONFIG_CDCACM_VENDORID")) != vid: + continue + board = "/".join(path.relative_to(boards_dir).parts[:2]) + rel = path.relative_to(px4_dir).as_posix() + px4_boards.setdefault(board, []).append((rel, values)) + + for board, (pid, strings) in sorted(entries.items()): + if not (boards_dir / board).is_dir(): + notices.append( + f"pid {pid}: boards/{board}/ not in PX4 yet; checked once " + "the board is upstream" + ) + continue + defconfigs = px4_boards.get(board) + if not defconfigs: + errors.append( + f"pid {pid}: no defconfig under boards/{board}/ sets " + f"CONFIG_CDCACM_VENDORID={doc['vid']}" + ) + continue + for rel, values in defconfigs: + product = values.get("CONFIG_CDCACM_PRODUCTID") + if hex_int(product) != int(pid, 16): + errors.append( + f"{rel}: CONFIG_CDCACM_PRODUCTID={product} but the " + f"registry assigns {pid} to {board}" + ) + vendor_str = values.get("CONFIG_CDCACM_VENDORSTR") + if vendor_str not in strings: + expected = " or ".join(f'"{s}"' for s in sorted(strings)) + found = "unset" if vendor_str is None else f'"{vendor_str}"' + errors.append( + f"{rel}: CONFIG_CDCACM_VENDORSTR is {found}, " + f"expected {expected}" + ) + + for board, defconfigs in sorted(px4_boards.items()): + if board not in entries: + for rel, _ in defconfigs: + errors.append( + f"{rel}: uses VID {doc['vid']} but no registry entry " + f"has px4_board '{board}'" + ) + + return errors, notices + + +def main(): + if len(sys.argv) not in (2, 3): + print(__doc__.splitlines()[2], file=sys.stderr) + return 2 + px4_dir = Path(sys.argv[1]) + path = sys.argv[2] if len(sys.argv) > 2 else "usb-ids.yaml" + if not (px4_dir / "boards").is_dir(): + print(f"error: {px4_dir}/boards is not a directory", file=sys.stderr) + return 1 + + ok, doc = load(path) + if not ok: + return 1 + # The cross-check trusts the registry's structure; stop if it is invalid. + errors = validate(doc) + if not errors: + errors, notices = check(doc, px4_dir) + for n in notices: + print(f"notice: {n}") + for e in errors: + print(f"error: {e}", file=sys.stderr) + if errors: + print(f"{path}: {len(errors)} error(s) against {px4_dir}", file=sys.stderr) + return 1 + print(f"{path}: matches {px4_dir}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/usb-ids.yaml b/usb-ids.yaml index 384eb78..b522791 100644 --- a/usb-ids.yaml +++ b/usb-ids.yaml @@ -10,6 +10,11 @@ # # PID format: "0x" + 4 uppercase hex digits, quoted (YAML would otherwise # parse some values as integers). +# +# `px4_board` on a PID names the PX4 boards// directory that +# ships it; absent means the board is not upstream yet. `usb_vendor_string` +# on a manufacturer is the CONFIG_CDCACM_VENDORSTR its PX4 boards use, set +# only when it differs from `name`. check_px4.py enforces both against PX4. vid: "0x3643" vendor_string: "Dronecode Project, Inc." @@ -23,6 +28,7 @@ manufacturers: - pid: "0x001D" board: FMU-v6XRT date: 2023-11-01 + px4_board: px4/fmu-v6xrt - name: ZeroOne px4_vendor: zeroone @@ -32,6 +38,7 @@ manufacturers: - pid: "0x15E0" board: X6 date: 2024-08-01 + px4_board: zeroone/x6 - name: NewBeeDrone # Not upstream in PX4 yet; slug matches boards/newbeedrone/ from PR #26966. @@ -60,12 +67,14 @@ manufacturers: board: Agam Autopilot v6X-RT date: 2026-08-18 legacy: true + px4_board: agam/fmu-v6xrt - pid: "0x0003" board: Agam MegH7 date: 2026-08-22 legacy: true - name: SIYI + usb_vendor_string: "Siyi" px4_vendor: siyi contact: support@siyi.biz blocks: ["0x04B0"] # 0x04B0-0x04BF @@ -73,9 +82,11 @@ manufacturers: - pid: "0x04B0" board: SIYI N7 date: 2026-09-11 + px4_board: siyi/n7 - pid: "0x04BF" board: SIYI UniFC 6 PICO date: 2026-09-11 + px4_board: siyi/unifc-6-pico - name: Droneer px4_vendor: droneer diff --git a/validate.py b/validate.py index 5652292..59b6fa3 100644 --- a/validate.py +++ b/validate.py @@ -2,10 +2,11 @@ """Validate usb-ids.yaml, the Dronecode USB ID registry. Checks structure, field formats, PID uniqueness (case-insensitive), -px4_vendor slug uniqueness, and block allocation: every non-legacy PID sits -inside one of its manufacturer's claimed 16-PID blocks, and no block holds -another manufacturer's PID. Prints one error per line and exits 1 on any -violation, 0 when the registry is valid. +px4_vendor slug uniqueness, px4_board format and uniqueness, and block +allocation: every non-legacy PID sits inside one of its manufacturer's +claimed 16-PID blocks, and no block holds another manufacturer's PID. +Prints one error per line and exits 1 on any violation, 0 when the registry +is valid. check_px4.py compares the registry against a PX4 checkout. Only dependency: PyYAML. """ @@ -19,14 +20,17 @@ VID_RE = re.compile(r"^0x[0-9A-F]{4}$") BLOCK_RE = re.compile(r"^0x[0-9A-F]{3}0$") PX4_VENDOR_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$") +# Board directory names in PX4 include uppercase and underscores +# (radiolink/PIX6, saam/saampixv1_1). +PX4_BOARD_RE = re.compile(r"^[a-z0-9][a-z0-9-]*/[A-Za-z0-9][A-Za-z0-9_.-]*$") DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") TOP_KEYS = {"vid", "vendor_string", "manufacturers"} MFR_REQUIRED = {"name", "contact", "pids"} -MFR_OPTIONAL = {"px4_vendor", "blocks"} +MFR_OPTIONAL = {"px4_vendor", "blocks", "usb_vendor_string"} PID_REQUIRED = {"pid", "board", "date"} -PID_OPTIONAL = {"legacy"} +PID_OPTIONAL = {"legacy", "px4_board"} # Assignments predating the block policy. Maintainer-set: extending this set # is a deliberate edit here, not something a PID request can grant itself. @@ -68,6 +72,7 @@ def err(msg): seen_pids = {} # normalized pid -> manufacturer name seen_vendors = {} # px4_vendor -> manufacturer name seen_blocks = {} # block start -> manufacturer name + seen_boards = {} # px4_board -> pid assigned = [] # (pid as int, pid as written, manufacturer name) for i, mfr in enumerate(manufacturers): @@ -107,6 +112,12 @@ def err(msg): else: seen_vendors[px4_vendor] = name + usb_vendor_string = mfr.get("usb_vendor_string") + if "usb_vendor_string" in mfr and ( + not isinstance(usb_vendor_string, str) or not usb_vendor_string.strip() + ): + err(f"{where}: 'usb_vendor_string' must be a non-empty string") + # Blocks are all 16 wide and 16-aligned, so two of them overlap only # if they share a start value: unique starts means no overlap. blocks = mfr.get("blocks") @@ -200,6 +211,33 @@ def err(msg): f"the block policy (date before {LEGACY_CUTOFF})" ) + px4_board = entry.get("px4_board") + if "px4_board" in entry: + if not isinstance(px4_board, str) or not PX4_BOARD_RE.match( + px4_board + ): + err( + f"{pwhere}: px4_board '{px4_board}' must be a " + "/ path under PX4's boards/" + ) + elif not isinstance(px4_vendor, str): + err( + f"{pwhere}: px4_board '{px4_board}' requires " + f"{name}'s 'px4_vendor'" + ) + elif not px4_board.startswith(px4_vendor + "/"): + err( + f"{pwhere}: px4_board '{px4_board}' is outside " + f"px4_vendor '{px4_vendor}'" + ) + elif px4_board in seen_boards: + err( + f"{pwhere}: px4_board '{px4_board}' already used by " + f"pid {seen_boards[px4_board]}" + ) + else: + seen_boards[px4_board] = pid + # Skip entries whose pid already failed the format check. if pid_ok and legacy is not True: start = f"0x{int(pid, 16) & ~0xF:04X}" @@ -224,13 +262,20 @@ def err(msg): return errors -def main(): - path = sys.argv[1] if len(sys.argv) > 1 else "usb-ids.yaml" +def load(path): + """Return (ok, parsed registry); prints the error when loading fails.""" try: with open(path, encoding="utf-8") as f: - doc = yaml.safe_load(f) + return True, yaml.safe_load(f) except (OSError, yaml.YAMLError) as e: print(f"error: cannot load {path}: {e}", file=sys.stderr) + return False, None + + +def main(): + path = sys.argv[1] if len(sys.argv) > 1 else "usb-ids.yaml" + ok, doc = load(path) + if not ok: return 1 errors = validate(doc) From b36fa86fd9ac2597060518364675d9bbe25ef1fb Mon Sep 17 00:00:00 2001 From: Ramon Roche Date: Mon, 28 Sep 2026 12:21:36 -0700 Subject: [PATCH 2/3] Drop vendor string from the PX4 contract The registry maps PIDs to PX4 boards so members can ship under the Dronecode VID. It does not govern the USB vendor string, so remove the usb_vendor_string field and stop comparing CONFIG_CDCACM_VENDORSTR in check_px4.py. The contract is now only that a board's defconfigs using VID 0x3643 set the PID of the entry whose px4_board names that board, and that every such board is mapped. check_px4.py also fails when the PX4 path yields no defconfig on the registry VID, instead of passing with nothing checked, and reports how many defconfigs and mapped boards it checked. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Ramon Roche --- .github/ISSUE_TEMPLATE/pid-request.yml | 8 ---- .github/PULL_REQUEST_TEMPLATE.md | 2 +- README.md | 13 +++--- check_px4.py | 56 ++++++++++++++------------ usb-ids.yaml | 6 +-- validate.py | 8 +--- 6 files changed, 40 insertions(+), 53 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/pid-request.yml b/.github/ISSUE_TEMPLATE/pid-request.yml index db59b2e..c66367e 100644 --- a/.github/ISSUE_TEMPLATE/pid-request.yml +++ b/.github/ISSUE_TEMPLATE/pid-request.yml @@ -38,14 +38,6 @@ body: The `boards//` directory in PX4-Autopilot that will use the PID, if you have one (e.g. `boards/acme/fc1`). Leave empty if the board is not headed upstream yet. - - type: input - id: usb_vendor_string - attributes: - label: USB vendor string - description: >- - The exact `CONFIG_CDCACM_VENDORSTR` your boards' defconfigs set, if - it differs from the manufacturer name above. PX4 CI compares it - character for character, case included. - type: checkboxes id: membership attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8310275..8104fcf 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,5 +6,5 @@ - [ ] All PIDs are inside our claimed block(s) in `blocks` (first request: claim a free aligned 16-PID block, `0xNNN0`) - [ ] Contact email is valid and monitored -- [ ] `px4_board` is set only for boards upstream in PX4 (or in an open board PR), and `usb_vendor_string` matches their `CONFIG_CDCACM_VENDORSTR` when it differs from `name` +- [ ] `px4_board` is set only for boards upstream in PX4 (or in an open board PR) - [ ] We are a Dronecode Foundation member (or state your affiliation below) diff --git a/README.md b/README.md index 5559264..29a5377 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,6 @@ PR, use the [PID request issue form](../../issues/new/choose). | `date` | Assignment date, `YYYY-MM-DD` | | `contact` | Email address for the manufacturer | | `px4_vendor` | Your vendor directory name in the PX4 `boards/` tree. Optional until you upstream a board; **required before your first PX4-Autopilot board PR**, otherwise PX4 CI will reject it. | -| `usb_vendor_string` | The exact `CONFIG_CDCACM_VENDORSTR` your PX4 boards ship. Optional; defaults to `name`, so set it only when the two differ. | | `px4_board` | On a PID: the `/` directory under PX4's `boards/` that uses it (e.g. `siyi/n7`), inside your `px4_vendor`. Leave it off until the board is upstream in PX4, or add it in the same window as your board PR. | | `blocks` | List of claimed block starts, `"0x"` + 4 uppercase hex digits ending in `0`; each covers 16 PIDs (`0xNNN0`-`0xNNNF`), globally unique. Required before any non-legacy PID can be assigned. | | `legacy` | `true` on assignments that predate the block policy (before 2026-09). Maintainer-set, not for new requests. | @@ -63,12 +62,12 @@ python3 validate.py usb-ids.yaml Runs automatically on every PR and push to `main`. `check_px4.py` checks the registry against a PX4-Autopilot checkout in both -directions: each `px4_board` must exist with matching -`CONFIG_CDCACM_VENDORID`, `CONFIG_CDCACM_PRODUCTID` and -`CONFIG_CDCACM_VENDORSTR` (the registry `vendor_string`, or the -manufacturer's `usb_vendor_string`), and every PX4 board using VID `0x3643` -must be claimed by some `px4_board`. A `px4_board` whose directory is not in -PX4 yet is only a notice. CI runs it against PX4 `main` on every PR, push, +directions: every defconfig under a `px4_board` directory that sets +`CONFIG_CDCACM_VENDORID` to `0x3643` must set `CONFIG_CDCACM_PRODUCTID` to +that entry's PID, and every PX4 board using VID `0x3643` must be claimed by +some `px4_board`. A `px4_board` whose directory is not in PX4 yet is only a +notice. A PX4 path with no board using VID `0x3643` fails rather than +passing with nothing checked. CI runs it against PX4 `main` on every PR, push, and weekly. ```sh diff --git a/check_px4.py b/check_px4.py index 74f9575..dd07f88 100644 --- a/check_px4.py +++ b/check_px4.py @@ -3,17 +3,17 @@ Usage: check_px4.py [usb-ids.yaml] -A PX4 board defconfig matches its registry entry when it lives at -boards//nuttx-config/*/defconfig and its CONFIG_CDCACM_VENDORID, -CONFIG_CDCACM_PRODUCTID (hex-int compares) and CONFIG_CDCACM_VENDORSTR -(exact: the registry vendor_string, or the manufacturer's -usb_vendor_string, which defaults to its name) agree with the registry. +A PX4 board defconfig at boards//nuttx-config/*/defconfig whose +CONFIG_CDCACM_VENDORID is the registry VID must set CONFIG_CDCACM_PRODUCTID +to that entry's PID (hex-int compares). Both directions are checked: every entry with px4_board must match the defconfigs in that directory, and every PX4 defconfig using the registry VID must belong to some entry's px4_board. A px4_board whose directory does not exist in PX4 yet is a notice, not an error, so a registry entry can land -before its board PR. Prints one line per finding and exits 1 on any error. +before its board PR. A PX4 path with no boards/ directory, or with no +defconfig using the registry VID, is an error, so an empty checkout cannot +pass. Prints one line per finding and exits 1 on any error. Only dependency: PyYAML. """ @@ -24,17 +24,15 @@ from validate import load, validate DEFCONFIG_GLOB = "nuttx-config/*/defconfig" -KEYS = ("CONFIG_CDCACM_VENDORID", "CONFIG_CDCACM_PRODUCTID", "CONFIG_CDCACM_VENDORSTR") +KEYS = ("CONFIG_CDCACM_VENDORID", "CONFIG_CDCACM_PRODUCTID") def read_defconfig(path): - """Return the CDCACM settings of a defconfig, strings unquoted.""" + """Return the CDCACM VID and PID settings of a defconfig.""" values = {} for line in path.read_text(encoding="utf-8", errors="replace").splitlines(): key, sep, value = line.partition("=") if sep and key in KEYS: - if len(value) >= 2 and value[0] == value[-1] == '"': - value = value[1:-1] values[key] = value return values @@ -47,18 +45,17 @@ def hex_int(value): def check(doc, px4_dir): - """Return (errors, notices) comparing the registry to PX4's boards/.""" + """Return (errors, notices, defconfigs checked, boards checked).""" errors, notices = [], [] boards_dir = px4_dir / "boards" vid = int(doc["vid"], 16) - # px4_board -> (pid, accepted vendor strings) + # px4_board -> pid entries = {} for mfr in doc["manufacturers"]: - strings = {doc["vendor_string"], mfr.get("usb_vendor_string", mfr["name"])} for entry in mfr["pids"]: if "px4_board" in entry: - entries[entry["px4_board"]] = (entry["pid"], strings) + entries[entry["px4_board"]] = entry["pid"] # board -> [(defconfig path relative to PX4, settings)] for our VID only px4_boards = {} @@ -70,7 +67,17 @@ def check(doc, px4_dir): rel = path.relative_to(px4_dir).as_posix() px4_boards.setdefault(board, []).append((rel, values)) - for board, (pid, strings) in sorted(entries.items()): + # PX4 ships boards on this VID, so finding none means a wrong or empty + # checkout, not a clean result. + if not px4_boards: + errors.append( + f"no defconfig under {boards_dir} sets " + f"CONFIG_CDCACM_VENDORID={doc['vid']}; not a PX4 checkout?" + ) + return errors, notices, 0, 0 + + checked_boards = 0 + for board, pid in sorted(entries.items()): if not (boards_dir / board).is_dir(): notices.append( f"pid {pid}: boards/{board}/ not in PX4 yet; checked once " @@ -84,6 +91,7 @@ def check(doc, px4_dir): f"CONFIG_CDCACM_VENDORID={doc['vid']}" ) continue + checked_boards += 1 for rel, values in defconfigs: product = values.get("CONFIG_CDCACM_PRODUCTID") if hex_int(product) != int(pid, 16): @@ -91,14 +99,6 @@ def check(doc, px4_dir): f"{rel}: CONFIG_CDCACM_PRODUCTID={product} but the " f"registry assigns {pid} to {board}" ) - vendor_str = values.get("CONFIG_CDCACM_VENDORSTR") - if vendor_str not in strings: - expected = " or ".join(f'"{s}"' for s in sorted(strings)) - found = "unset" if vendor_str is None else f'"{vendor_str}"' - errors.append( - f"{rel}: CONFIG_CDCACM_VENDORSTR is {found}, " - f"expected {expected}" - ) for board, defconfigs in sorted(px4_boards.items()): if board not in entries: @@ -108,7 +108,8 @@ def check(doc, px4_dir): f"has px4_board '{board}'" ) - return errors, notices + checked = sum(len(d) for d in px4_boards.values()) + return errors, notices, checked, checked_boards def main(): @@ -127,7 +128,7 @@ def main(): # The cross-check trusts the registry's structure; stop if it is invalid. errors = validate(doc) if not errors: - errors, notices = check(doc, px4_dir) + errors, notices, checked, boards = check(doc, px4_dir) for n in notices: print(f"notice: {n}") for e in errors: @@ -135,7 +136,10 @@ def main(): if errors: print(f"{path}: {len(errors)} error(s) against {px4_dir}", file=sys.stderr) return 1 - print(f"{path}: matches {px4_dir}") + print( + f"{path}: matches {px4_dir}, checked {checked} defconfig(s) " + f"across {boards} mapped board(s)" + ) return 0 diff --git a/usb-ids.yaml b/usb-ids.yaml index b522791..65df23e 100644 --- a/usb-ids.yaml +++ b/usb-ids.yaml @@ -12,9 +12,8 @@ # parse some values as integers). # # `px4_board` on a PID names the PX4 boards// directory that -# ships it; absent means the board is not upstream yet. `usb_vendor_string` -# on a manufacturer is the CONFIG_CDCACM_VENDORSTR its PX4 boards use, set -# only when it differs from `name`. check_px4.py enforces both against PX4. +# ships it; absent means the board is not upstream yet. check_px4.py checks +# it against PX4. vid: "0x3643" vendor_string: "Dronecode Project, Inc." @@ -74,7 +73,6 @@ manufacturers: legacy: true - name: SIYI - usb_vendor_string: "Siyi" px4_vendor: siyi contact: support@siyi.biz blocks: ["0x04B0"] # 0x04B0-0x04BF diff --git a/validate.py b/validate.py index 59b6fa3..5413d8b 100644 --- a/validate.py +++ b/validate.py @@ -28,7 +28,7 @@ TOP_KEYS = {"vid", "vendor_string", "manufacturers"} MFR_REQUIRED = {"name", "contact", "pids"} -MFR_OPTIONAL = {"px4_vendor", "blocks", "usb_vendor_string"} +MFR_OPTIONAL = {"px4_vendor", "blocks"} PID_REQUIRED = {"pid", "board", "date"} PID_OPTIONAL = {"legacy", "px4_board"} @@ -112,12 +112,6 @@ def err(msg): else: seen_vendors[px4_vendor] = name - usb_vendor_string = mfr.get("usb_vendor_string") - if "usb_vendor_string" in mfr and ( - not isinstance(usb_vendor_string, str) or not usb_vendor_string.strip() - ): - err(f"{where}: 'usb_vendor_string' must be a non-empty string") - # Blocks are all 16 wide and 16-aligned, so two of them overlap only # if they share a start value: unique starts means no overlap. blocks = mfr.get("blocks") From cec5ec3bd7e8369f89ba3d30a7cf94ab0fc2155e Mon Sep 17 00:00:00 2001 From: Ramon Roche Date: Mon, 28 Sep 2026 14:42:35 -0700 Subject: [PATCH 3/3] Document registering px4_board before the PX4 board PR PX4 CI fails a board PR using VID 0x3643 unless its PID is already mapped to the board's directory with px4_board on this repo's main, so the registry entry has to merge first. The README and PR template said the opposite (add px4_board once the board is upstream, or alongside the board PR), and pointed at px4_vendor as what PX4 CI checks. Add the step to the request process, map the example entry, and fix both field descriptions and the PR template checklist. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Ramon Roche --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- README.md | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8104fcf..a0bc974 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,5 +6,5 @@ - [ ] All PIDs are inside our claimed block(s) in `blocks` (first request: claim a free aligned 16-PID block, `0xNNN0`) - [ ] Contact email is valid and monitored -- [ ] `px4_board` is set only for boards upstream in PX4 (or in an open board PR) +- [ ] `px4_board` is set for every board we are upstreaming to PX4-Autopilot (merge this before opening the PX4 board PR) - [ ] We are a Dronecode Foundation member (or state your affiliation below) diff --git a/README.md b/README.md index 29a5377..03bb22f 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Pixhawk/PX4-compatible hardware. This repository is the single source of truth for those assignments: [`usb-ids.yaml`](usb-ids.yaml). PX4-Autopilot CI checks board definitions against this registry, so a board -using VID `0x3643` cannot merge upstream with an unregistered PID or a PID -belonging to another manufacturer. +using VID `0x3643` cannot merge upstream unless its PID is registered here +and mapped to that board's directory with `px4_board`. PIDs are assigned in blocks of 16: your first request claims an aligned block (`0xNNN0`-`0xNNNF`) and every PID you are assigned comes from inside @@ -27,6 +27,7 @@ block fills up, claim another. pids: - pid: "0x0070" board: Acme FC1 + px4_board: acme/fc1 # boards/acme/fc1 in PX4-Autopilot date: 2026-07-10 ``` @@ -34,6 +35,12 @@ block fills up, claim another. Dronecode Foundation membership and merges. Assignments are at maintainer discretion. +3. Once your entry is merged here, open your board pull request in + PX4-Autopilot. Its USB ID check reads this registry and fails until your + PID is mapped to your board's directory with `px4_board`; if you opened + the PX4 pull request first, re-run the check after the registry pull + request merges, no changes needed. + Pick the lowest free block unless you have a reason not to; any free aligned block is fine. One entry per PID. PID values are hexadecimal: after `"0x0039"` comes `"0x003A"`, not `"0x0040"`. If you can't open a @@ -47,8 +54,8 @@ PR, use the [PID request issue form](../../issues/new/choose). | `board` | Board name | | `date` | Assignment date, `YYYY-MM-DD` | | `contact` | Email address for the manufacturer | -| `px4_vendor` | Your vendor directory name in the PX4 `boards/` tree. Optional until you upstream a board; **required before your first PX4-Autopilot board PR**, otherwise PX4 CI will reject it. | -| `px4_board` | On a PID: the `/` directory under PX4's `boards/` that uses it (e.g. `siyi/n7`), inside your `px4_vendor`. Leave it off until the board is upstream in PX4, or add it in the same window as your board PR. | +| `px4_vendor` | Your vendor directory name in the PX4 `boards/` tree. Optional until you upstream a board; needed for `px4_board`, which must sit inside it. | +| `px4_board` | On a PID: the `/` directory under PX4's `boards/` that uses it (e.g. `siyi/n7`). **Required, and merged here, before you open the PX4-Autopilot board PR**: PX4 CI rejects a board using VID `0x3643` whose PID is not mapped to its directory. Leave it off for boards not headed upstream. | | `blocks` | List of claimed block starts, `"0x"` + 4 uppercase hex digits ending in `0`; each covers 16 PIDs (`0xNNN0`-`0xNNNF`), globally unique. Required before any non-legacy PID can be assigned. | | `legacy` | `true` on assignments that predate the block policy (before 2026-09). Maintainer-set, not for new requests. |