Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/pid-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ body:
request claims one), one PID per board.
validations:
required: true
- type: input
id: px4_board
attributes:
label: PX4 board directory
description: >-
The `boards/<vendor>/<board>` 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: checkboxes
id: membership
attributes:
Expand Down
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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)
15 changes: 15 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,13 +27,20 @@ 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
```

2. CI validates the file (format, uniqueness). A maintainer confirms your
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
Expand All @@ -47,7 +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_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 `<vendor>/<board>` 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. |

Expand All @@ -60,6 +68,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: 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
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,
Expand Down
147 changes: 147 additions & 0 deletions check_px4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Check usb-ids.yaml against a PX4-Autopilot checkout.

Usage: check_px4.py <px4-autopilot-dir> [usb-ids.yaml]

A PX4 board defconfig at boards/<px4_board>/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. 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.
"""

import sys
from pathlib import Path

from validate import load, validate

DEFCONFIG_GLOB = "nuttx-config/*/defconfig"
KEYS = ("CONFIG_CDCACM_VENDORID", "CONFIG_CDCACM_PRODUCTID")


def read_defconfig(path):
"""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:
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, defconfigs checked, boards checked)."""
errors, notices = [], []
boards_dir = px4_dir / "boards"
vid = int(doc["vid"], 16)

# px4_board -> pid
entries = {}
for mfr in doc["manufacturers"]:
for entry in mfr["pids"]:
if "px4_board" in entry:
entries[entry["px4_board"]] = entry["pid"]

# 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))

# 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 "
"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
checked_boards += 1
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}"
)

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}'"
)

checked = sum(len(d) for d in px4_boards.values())
return errors, notices, checked, checked_boards


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, checked, boards = 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}, checked {checked} defconfig(s) "
f"across {boards} mapped board(s)"
)
return 0


if __name__ == "__main__":
sys.exit(main())
9 changes: 9 additions & 0 deletions usb-ids.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
#
# 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/<vendor>/<board> directory that
# ships it; absent means the board is not upstream yet. check_px4.py checks
# it against PX4.

vid: "0x3643"
vendor_string: "Dronecode Project, Inc."
Expand All @@ -23,6 +27,7 @@ manufacturers:
- pid: "0x001D"
board: FMU-v6XRT
date: 2023-11-01
px4_board: px4/fmu-v6xrt

- name: ZeroOne
px4_vendor: zeroone
Expand All @@ -32,6 +37,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.
Expand Down Expand Up @@ -60,6 +66,7 @@ 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
Expand All @@ -73,9 +80,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
Expand Down
Loading
Loading