From b37867dd998dd33163aaf096efa782007b086d84 Mon Sep 17 00:00:00 2001 From: Ramon Roche Date: Thu, 27 Aug 2026 09:12:31 -0700 Subject: [PATCH 1/2] Assign PIDs in aligned blocks of 16 Vendors were taking sequential single PIDs (0x0001, 0x0002, 0x0003), interleaving unrelated manufacturers across the low range. The allocation unit is now an aligned block of 16, 0xNNN0-0xNNNF, claimed in a new per-manufacturer `blocks` list; every PID a vendor is assigned has to fall inside one of their blocks. Existing assignments do not move. PX4, ZeroOne and NewBeeDrone simply declare the block that already contains their PID. Syro's 0x0001 and Agam's 0x0002 and 0x0003 sit in the interleaved low range where no clean block exists, so they carry a new `legacy: true` flag: exempt from containment, frozen in place, still globally unique. No manufacturer may claim a block holding another vendor's PID, which permanently freezes 0x0000-0x000F. `legacy` is maintainer-set only. It is valid solely for the PIDs named in LEGACY_PIDS, and only on entries dated before 2026-09-01, so a request cannot grant itself the exemption without editing this validator. Both fields are additive, so deployed PX4-Autopilot checkers that fetch usb-ids.yaml from main keep working unchanged. --- usb-ids.yaml | 11 ++++++ validate.py | 100 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 100 insertions(+), 11 deletions(-) diff --git a/usb-ids.yaml b/usb-ids.yaml index 1f5b5ad..9578c72 100644 --- a/usb-ids.yaml +++ b/usb-ids.yaml @@ -3,6 +3,11 @@ # Single source of truth for PID assignments under VID 0x3643. # See README.md for the assignment process. # +# PIDs are assigned in blocks of 16: each manufacturer claims an +# aligned block (0xNNN0-0xNNNF), listed in `blocks`, and assigns +# PIDs from inside it. Values are hexadecimal: after "0x0039" +# comes "0x003A", not "0x0040". +# # PID format: "0x" + 4 uppercase hex digits, quoted (YAML would otherwise # parse some values as integers). @@ -13,6 +18,7 @@ manufacturers: - name: PX4/Dronecode px4_vendor: px4 contact: rroche@linuxfoundation.org + blocks: ["0x0010"] # 0x0010-0x001F pids: - pid: "0x001D" board: FMU-v6XRT @@ -21,6 +27,7 @@ manufacturers: - name: ZeroOne px4_vendor: zeroone contact: menghua@01aero.com + blocks: ["0x15E0"] # 0x15E0-0x15EF pids: - pid: "0x15E0" board: X6 @@ -30,6 +37,7 @@ manufacturers: # Not upstream in PX4 yet; slug matches boards/newbeedrone/ from PR #26966. px4_vendor: newbeedrone contact: kelvin@newbeedrone.com + blocks: ["0x0050"] # 0x0050-0x005F pids: - pid: "0x0050" board: PixNova @@ -42,6 +50,7 @@ manufacturers: - pid: "0x0001" board: Syro V6X date: 2026-08-03 + legacy: true - name: Agam Robotics px4_vendor: agam-robotics @@ -50,6 +59,8 @@ manufacturers: - pid: "0x0002" board: Agam Autopilot v6X-RT date: 2026-08-18 + legacy: true - pid: "0x0003" board: Agam MegH7 date: 2026-08-22 + legacy: true diff --git a/validate.py b/validate.py index 79fd21a..5652292 100644 --- a/validate.py +++ b/validate.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 """Validate usb-ids.yaml, the Dronecode USB ID registry. -Checks structure, field formats, PID uniqueness (case-insensitive), and -px4_vendor slug uniqueness. Prints one error per line and exits 1 on any +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. Only dependency: PyYAML. @@ -15,14 +17,21 @@ PID_RE = re.compile(r"^0x[0-9A-F]{4}$") 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-]*$") 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"} -PID_KEYS = {"pid", "board", "date"} +MFR_OPTIONAL = {"px4_vendor", "blocks"} +PID_REQUIRED = {"pid", "board", "date"} +PID_OPTIONAL = {"legacy"} + +# Assignments predating the block policy. Maintainer-set: extending this set +# is a deliberate edit here, not something a PID request can grant itself. +LEGACY_PIDS = {"0x0001", "0x0002", "0x0003"} +LEGACY_CUTOFF = "2026-09-01" def validate(doc): @@ -58,6 +67,8 @@ def err(msg): seen_pids = {} # normalized pid -> manufacturer name seen_vendors = {} # px4_vendor -> manufacturer name + seen_blocks = {} # block start -> manufacturer name + assigned = [] # (pid as int, pid as written, manufacturer name) for i, mfr in enumerate(manufacturers): where = f"manufacturers[{i}]" @@ -96,6 +107,34 @@ def err(msg): else: seen_vendors[px4_vendor] = name + # 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") + mfr_blocks = set() + if "blocks" in mfr: + if not isinstance(blocks, list) or not blocks: + err(f"{where}: 'blocks' must be a non-empty list") + else: + for block in blocks: + if not isinstance(block, str) or not BLOCK_RE.match(block): + err( + f"{where}: block '{block}' is not a quoted 0xXXX0 " + "uppercase hex string; a block start must end in 0 " + "(16-aligned)" + ) + continue + if block in mfr_blocks: + err(f"{where}: block '{block}' listed twice") + continue + mfr_blocks.add(block) + if block in seen_blocks: + err( + f"{where}: block '{block}' already claimed by " + f"'{seen_blocks[block]}'" + ) + else: + seen_blocks[block] = name + pids = mfr.get("pids") if "pids" not in mfr: continue @@ -109,12 +148,13 @@ def err(msg): err(f"{pwhere}: expected a mapping") continue - for key in sorted(set(entry) - PID_KEYS): + for key in sorted(set(entry) - PID_REQUIRED - PID_OPTIONAL): err(f"{pwhere}: unknown field '{key}'") - for key in sorted(PID_KEYS - set(entry)): + for key in sorted(PID_REQUIRED - set(entry)): err(f"{pwhere}: missing field '{key}'") pid = entry.get("pid") + pid_ok = False if "pid" in entry: if not isinstance(pid, str) or not PID_RE.match(pid): err( @@ -122,6 +162,7 @@ def err(msg): "uppercase hex string" ) else: + pid_ok = True pwhere = f"{where} pid {pid}" norm = pid.lower() if norm in seen_pids: @@ -131,17 +172,54 @@ def err(msg): ) else: seen_pids[norm] = name + assigned.append((int(pid, 16), pid, name)) board = entry.get("board") if "board" in entry and (not isinstance(board, str) or not board.strip()): err(f"{pwhere}: 'board' must be a non-empty string") date = entry.get("date") - if "date" in entry: - # PyYAML may parse unquoted dates as datetime.date - date_str = date.isoformat() if hasattr(date, "isoformat") else date - if not isinstance(date_str, str) or not DATE_RE.match(date_str): - err(f"{pwhere}: date '{date}' must be YYYY-MM-DD") + # PyYAML may parse unquoted dates as datetime.date + date_str = date.isoformat() if hasattr(date, "isoformat") else date + date_ok = isinstance(date_str, str) and bool(DATE_RE.match(date_str)) + if "date" in entry and not date_ok: + err(f"{pwhere}: date '{date}' must be YYYY-MM-DD") + + legacy = entry.get("legacy") + if "legacy" in entry: + if legacy is not True: + err(f"{pwhere}: 'legacy' must be true when present") + elif not (pid_ok and pid in LEGACY_PIDS): + err( + f"{pwhere}: 'legacy' is maintainer-set only; this pid " + "is not in the frozen legacy set" + ) + elif date_ok and date_str >= LEGACY_CUTOFF: + err( + f"{pwhere}: 'legacy' is only for assignments predating " + f"the block policy (date before {LEGACY_CUTOFF})" + ) + + # 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}" + if start not in mfr_blocks: + err( + f"{pwhere}: outside {name}'s claimed blocks; claim " + f"block '{start}' in 'blocks' or move the pid into a " + "claimed block" + ) + + # A claimed block must not hold another manufacturer's pid, legacy ones + # included; this is what keeps the pre-policy 0x0000-0x000F range frozen. + for pid_int, pid, owner in assigned: + start = f"0x{pid_int & ~0xF:04X}" + holder = seen_blocks.get(start) + if holder is not None and holder != owner: + err( + f"manufacturer '{holder}': block '{start}' contains pid " + f"{pid} assigned to '{owner}'" + ) return errors From f27c802f8bc3bdd37575e0c21778e5e2dba1e4cb Mon Sep 17 00:00:00 2001 From: Ramon Roche Date: Thu, 27 Aug 2026 09:12:32 -0700 Subject: [PATCH 2/2] Document block allocation and put the registry under CODEOWNERS The README, the PR checklist and the issue form all still told requesters to pick the lowest free PID. They now describe claiming an aligned 16-PID block, with a reminder that PID values are hexadecimal: after "0x0039" comes "0x003A", not "0x0040". The sequential-single habit came partly from reading the list as decimal. CODEOWNERS puts the registry and its validator under maintainer review. It stays advisory until main has branch protection with "Require review from Code Owners" enabled. --- .github/CODEOWNERS | 4 ++++ .github/ISSUE_TEMPLATE/pid-request.yml | 4 +++- .github/PULL_REQUEST_TEMPLATE.md | 2 +- README.md | 15 +++++++++++---- 4 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..4793d87 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# The registry and the validator that guards it are maintainer-owned. +# `legacy:` in particular is maintainer-set; see LEGACY_PIDS in validate.py. +/usb-ids.yaml @mrpollo +/validate.py @mrpollo diff --git a/.github/ISSUE_TEMPLATE/pid-request.yml b/.github/ISSUE_TEMPLATE/pid-request.yml index d5dcef4..42b0eac 100644 --- a/.github/ISSUE_TEMPLATE/pid-request.yml +++ b/.github/ISSUE_TEMPLATE/pid-request.yml @@ -25,7 +25,9 @@ body: id: boards attributes: label: Board(s) the PIDs are for - description: One PID is assigned per board. + description: >- + PIDs are assigned from your manufacturer's block of 16 (your first + request claims one), one PID per board. validations: required: true - type: checkboxes diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 804b531..39667c9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,6 @@ **Board(s):** -- [ ] Entries added to `usb-ids.yaml` (one per PID, lowest free PIDs) +- [ ] 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 - [ ] We are a Dronecode Foundation member (or state your affiliation below) diff --git a/README.md b/README.md index e2cf073..92be6cf 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,10 @@ 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. -There are no reservations: PIDs are assigned to real boards. Request one -when you have hardware to name. +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 +it. Claim a block when you have hardware to name, not in advance; when a +block fills up, claim another. ## Requesting a PID @@ -21,6 +23,7 @@ when you have hardware to name. - name: Acme Robotics px4_vendor: acme # your directory under boards/ in PX4-Autopilot contact: usb@acme.example + blocks: ["0x0070"] # 16 PIDs, 0x0070-0x007F pids: - pid: "0x0070" board: Acme FC1 @@ -31,8 +34,10 @@ when you have hardware to name. Dronecode Foundation membership and merges. Assignments are at maintainer discretion. -Pick the lowest free PID; one entry per PID. If you can't open a PR, use -the [PID request issue form](../../issues/new/choose). +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 +PR, use the [PID request issue form](../../issues/new/choose). ## Field reference @@ -43,6 +48,8 @@ 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. | +| `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. | ## Validation