release prep: rc.1 baseline and gating updates

This commit is contained in:
2026-03-02 15:54:55 -05:00
parent 97a42c8802
commit f43b2b24b6
168 changed files with 14708 additions and 982 deletions

View File

@@ -0,0 +1,59 @@
name: Dirty-Room Evidence (Sanitized)
description: Submit sanitized protocol evidence for unconfirmed devices/operations.
title: "[dirtyroom] <vid:pid> <operation-group>"
labels:
- protocol
body:
- type: input
id: vidpid
attributes:
label: VID/PID
placeholder: 0x2dc8:0x5200
validations:
required: true
- type: input
id: firmware
attributes:
label: Firmware version
placeholder: 1.00
validations:
required: true
- type: input
id: operation_group
attributes:
label: Operation group
placeholder: CoreDiag / Firmware / Mapping
validations:
required: true
- type: textarea
id: request_shape
attributes:
label: Sanitized request shape
description: Structural description only. No copied vendor/decompiled snippets.
validations:
required: true
- type: textarea
id: response_shape
attributes:
label: Sanitized response shape
description: Structural description and validator cues.
validations:
required: true
- type: textarea
id: failure_signatures
attributes:
label: Failure signatures
description: Timeout/malformed/invalid signatures observed.
- type: textarea
id: reproducibility
attributes:
label: Reproducibility notes
description: Steps, environment, and confidence.
validations:
required: true
- type: markdown
attributes:
value: |
**Clean-room reminder**
- Do not include raw decompiled code.
- Submit sanitized protocol structure only.

View File

@@ -0,0 +1,54 @@
name: Hardware Report
description: Report tested device behavior for OpenBitdo compatibility.
title: "[hardware] <device> <vid:pid>"
labels:
- hardware
body:
- type: input
id: device_name
attributes:
label: Device name
placeholder: Ultimate2 / JP108 / etc.
validations:
required: true
- type: input
id: vidpid
attributes:
label: VID/PID
description: Use hex format `0xVVVV:0xPPPP`
placeholder: 0x2dc8:0x6012
validations:
required: true
- type: input
id: firmware
attributes:
label: Firmware version
placeholder: 1.02
- type: textarea
id: operations
attributes:
label: Operations tested
description: identify, diag probe, mode read, profile read/write, firmware preflight, etc.
validations:
required: true
- type: textarea
id: sanitized_structure
attributes:
label: Sanitized request/response structure
description: Structural shape only. Do not paste vendor/decompiled snippets.
validations:
required: true
- type: textarea
id: behavior
attributes:
label: Observed behavior
description: Include expected vs actual.
validations:
required: true
- type: textarea
id: env
attributes:
label: Environment
description: OS, transport (USB/BT), and reproducibility notes.
validations:
required: true

View File

@@ -0,0 +1,73 @@
name: Release Blocker
description: Report a problem that must be resolved before public RC release.
title: "[release-blocker] <short summary>"
labels:
- release-blocker
- severity:p1
body:
- type: dropdown
id: severity
attributes:
label: Severity
description: Select the impact level for this blocker.
options:
- severity:p0 (critical)
- severity:p1 (high)
- severity:p2 (medium)
validations:
required: true
- type: input
id: vidpid
attributes:
label: VID/PID
description: Required for triage. Use format `0xVVVV:0xPPPP`.
placeholder: 0x2dc8:0x6012
validations:
required: true
- type: input
id: firmware_version
attributes:
label: Firmware version
description: Required for triage.
placeholder: 1.02
validations:
required: true
- type: input
id: openbitdo_version
attributes:
label: OpenBitdo version
placeholder: v0.0.1-rc.1
validations:
required: true
- type: textarea
id: summary
attributes:
label: What is broken?
description: One concise paragraph describing the blocker.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction steps
description: Provide deterministic steps from app launch to failure.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
validations:
required: true
- type: textarea
id: attachments
attributes:
label: Attachments (optional but strongly recommended)
description: Attach TOML report/logs/screenshots if available to speed up triage.
placeholder: "Attach report TOML and logs if possible."

130
.github/workflows/aur-publish.yml vendored Normal file
View File

@@ -0,0 +1,130 @@
name: AUR Publish
on:
workflow_call:
inputs:
tag:
description: "Release tag to publish (for example: v0.0.1-rc.1)"
required: true
type: string
workflow_dispatch:
inputs:
tag:
description: "Release tag to publish (for example: v0.0.1-rc.1)"
required: true
type: string
permissions:
contents: read
jobs:
publish-aur:
if: vars.AUR_PUBLISH_ENABLED == '1'
runs-on: ubuntu-latest
container: archlinux:base
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ inputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Install AUR packaging dependencies
run: |
pacman -Sy --noconfirm --needed base-devel git openssh curl github-cli
- name: Wait for release assets
run: |
set -euo pipefail
for attempt in $(seq 1 30); do
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "release ${TAG} is available"
exit 0
fi
sleep 10
done
echo "release ${TAG} was not found after waiting" >&2
exit 1
- name: Render AUR metadata from released assets
run: |
set -euo pipefail
mkdir -p /tmp/release-input /tmp/release-metadata
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
--pattern "openbitdo-${TAG}-linux-x86_64.tar.gz" \
--pattern "openbitdo-${TAG}-linux-aarch64.tar.gz" \
--pattern "openbitdo-${TAG}-macos-arm64.tar.gz" \
--dir /tmp/release-input
gh api -H "Accept: application/octet-stream" "repos/${GITHUB_REPOSITORY}/tarball/${TAG}" \
> "/tmp/release-input/openbitdo-${TAG}-source.tar.gz"
bash packaging/scripts/render_release_metadata.sh \
"$TAG" \
"$GITHUB_REPOSITORY" \
/tmp/release-input \
/tmp/release-metadata
useradd -m builder
chown -R builder:builder /tmp/release-metadata
su builder -s /bin/bash -c "set -euo pipefail; \
cd /tmp/release-metadata/aur/openbitdo; \
makepkg --printsrcinfo > .SRCINFO; \
cd /tmp/release-metadata/aur/openbitdo-bin; \
makepkg --printsrcinfo > .SRCINFO"
- name: Upload rendered metadata (audit)
uses: actions/upload-artifact@v4
with:
name: aur-rendered-metadata-${{ inputs.tag }}
path: |
/tmp/release-metadata/aur/openbitdo/PKGBUILD
/tmp/release-metadata/aur/openbitdo/.SRCINFO
/tmp/release-metadata/aur/openbitdo-bin/PKGBUILD
/tmp/release-metadata/aur/openbitdo-bin/.SRCINFO
/tmp/release-metadata/checksums.env
- name: Configure SSH for AUR
run: |
if [[ -z "${{ secrets.AUR_USERNAME }}" ]]; then
echo "missing required secret: AUR_USERNAME" >&2
exit 1
fi
if [[ -z "${{ secrets.AUR_SSH_PRIVATE_KEY }}" ]]; then
echo "missing required secret: AUR_SSH_PRIVATE_KEY" >&2
exit 1
fi
mkdir -p ~/.ssh
echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
- name: Publish openbitdo
env:
GIT_SSH_COMMAND: ssh -i ~/.ssh/aur
AUR_USER: ${{ secrets.AUR_USERNAME }}
run: |
set -euo pipefail
TMP="$(mktemp -d)"
git clone "ssh://${AUR_USER}@aur.archlinux.org/openbitdo.git" "$TMP/openbitdo"
cp /tmp/release-metadata/aur/openbitdo/PKGBUILD "$TMP/openbitdo/PKGBUILD"
cp /tmp/release-metadata/aur/openbitdo/.SRCINFO "$TMP/openbitdo/.SRCINFO"
cd "$TMP/openbitdo"
git config user.name "openbitdo-ci"
git config user.email "actions@users.noreply.github.com"
git add PKGBUILD .SRCINFO
git commit -m "Update openbitdo package for ${TAG}" || exit 0
git push
- name: Publish openbitdo-bin
env:
GIT_SSH_COMMAND: ssh -i ~/.ssh/aur
AUR_USER: ${{ secrets.AUR_USERNAME }}
run: |
set -euo pipefail
TMP="$(mktemp -d)"
git clone "ssh://${AUR_USER}@aur.archlinux.org/openbitdo-bin.git" "$TMP/openbitdo-bin"
cp /tmp/release-metadata/aur/openbitdo-bin/PKGBUILD "$TMP/openbitdo-bin/PKGBUILD"
cp /tmp/release-metadata/aur/openbitdo-bin/.SRCINFO "$TMP/openbitdo-bin/.SRCINFO"
cd "$TMP/openbitdo-bin"
git config user.name "openbitdo-ci"
git config user.email "actions@users.noreply.github.com"
git add PKGBUILD .SRCINFO
git commit -m "Update openbitdo-bin package for ${TAG}" || exit 0
git push

View File

@@ -7,6 +7,10 @@ on:
pull_request:
paths:
- '**'
schedule:
# 07:30 UTC is 02:30 America/New_York during standard time.
# (GitHub cron does not support time zones/DST directly.)
- cron: '30 7 * * *'
workflow_dispatch:
jobs:
@@ -20,7 +24,42 @@ jobs:
- name: Clean-room guard
run: ./scripts/cleanroom_guard.sh
test:
aur-validate:
runs-on: ubuntu-latest
needs: guard
container: archlinux:base
steps:
- uses: actions/checkout@v4
- name: Install AUR packaging toolchain
run: |
pacman -Sy --noconfirm --needed base-devel git
- name: Ensure package metadata has pinned checksum fields
run: |
set -euo pipefail
if grep -nE 'SKIP|:no_check' \
packaging/aur/openbitdo/PKGBUILD \
packaging/aur/openbitdo-bin/PKGBUILD \
packaging/homebrew/Formula/openbitdo.rb; then
echo "Found placeholder checksum markers; release metadata must be pinned." >&2
exit 1
fi
test -f packaging/scripts/render_release_metadata.sh
test -f packaging/aur/openbitdo/PKGBUILD.tmpl
test -f packaging/aur/openbitdo-bin/PKGBUILD.tmpl
test -f packaging/homebrew/Formula/openbitdo.rb.tmpl
- name: Validate PKGBUILD and .SRCINFO
run: |
useradd -m builder
chown -R builder:builder "$GITHUB_WORKSPACE"
su builder -s /bin/bash -c "set -euo pipefail; \
cd '$GITHUB_WORKSPACE/packaging/aur/openbitdo'; \
makepkg --printsrcinfo > /tmp/openbitdo.srcinfo; \
diff -u .SRCINFO /tmp/openbitdo.srcinfo; \
cd '$GITHUB_WORKSPACE/packaging/aur/openbitdo-bin'; \
makepkg --printsrcinfo > /tmp/openbitdo-bin.srcinfo; \
diff -u .SRCINFO /tmp/openbitdo-bin.srcinfo"
tui-smoke-test:
runs-on: ubuntu-latest
needs: guard
defaults:
@@ -34,14 +73,80 @@ jobs:
sudo apt-get install -y libudev-dev pkg-config
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Run TUI smoke tests
run: cargo test -p bitdo_tui --all-targets
build-macos-arm64:
runs-on: macos-14
needs: guard
defaults:
run:
working-directory: sdk
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Build/package macOS arm64 artifacts
run: ./scripts/package-macos.sh v0.0.0-ci arm64 aarch64-apple-darwin
- name: Upload macOS arm64 artifact sample
if: always()
uses: actions/upload-artifact@v4
with:
name: ci-macos-arm64-package
path: sdk/dist/openbitdo-v0.0.0-ci-macos-arm64*
hardware-paths:
runs-on: ubuntu-latest
outputs:
hardware: ${{ steps.filter.outputs.hardware }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
hardware:
- 'sdk/crates/bitdo_proto/**'
- 'sdk/crates/bitdo_app_core/**'
- 'sdk/crates/bitdo_tui/**'
- 'sdk/crates/openbitdo/**'
- 'sdk/tests/**'
- 'sdk/scripts/run_hardware_smoke.sh'
- 'spec/**'
- 'harness/lab/device_lab.yaml'
- '.github/workflows/ci.yml'
test:
runs-on: ubuntu-latest
needs:
- guard
- tui-smoke-test
- aur-validate
- build-macos-arm64
defaults:
run:
working-directory: sdk
steps:
- uses: actions/checkout@v4
- name: Install system deps
run: |
sudo apt-get update
sudo apt-get install -y libudev-dev pkg-config
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Clippy (workspace strict)
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Run tests
run: cargo test --workspace --all-targets
hardware-smoke:
hardware-dinput:
if: vars.BITDO_ENABLE_EXTRA_HARDWARE == '1'
runs-on: [self-hosted, linux, hid-lab]
if: (github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch') && vars.HARDWARE_SMOKE_ENABLED == '1'
continue-on-error: true
needs: test
env:
BITDO_REQUIRED_FAMILIES: DInput
defaults:
run:
working-directory: sdk
@@ -55,5 +160,93 @@ jobs:
if: always()
uses: actions/upload-artifact@v4
with:
name: hardware-smoke-report
name: hardware-smoke-dinput
path: harness/reports/*.json
hardware-standard64:
if: vars.BITDO_ENABLE_EXTRA_HARDWARE == '1'
runs-on: [self-hosted, linux, hid-lab]
needs: test
env:
BITDO_REQUIRED_FAMILIES: Standard64
defaults:
run:
working-directory: sdk
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Hardware smoke tests
run: ./scripts/run_hardware_smoke.sh
- name: Upload hardware smoke report
if: always()
uses: actions/upload-artifact@v4
with:
name: hardware-smoke-standard64
path: harness/reports/*.json
hardware-ultimate2:
if: github.event_name == 'schedule' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || needs.hardware-paths.outputs.hardware == 'true'
runs-on: [self-hosted, linux, hid-lab]
needs: [test, hardware-paths]
env:
BITDO_REQUIRED_SUITE: ultimate2
defaults:
run:
working-directory: sdk
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Hardware smoke tests (Ultimate2)
run: ./scripts/run_hardware_smoke.sh
- name: Upload hardware smoke report
if: always()
uses: actions/upload-artifact@v4
with:
name: hardware-smoke-ultimate2
path: harness/reports/*.json
hardware-108jp:
if: github.event_name == 'schedule' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || needs.hardware-paths.outputs.hardware == 'true'
runs-on: [self-hosted, linux, hid-lab]
needs: [test, hardware-paths]
env:
BITDO_REQUIRED_SUITE: 108jp
defaults:
run:
working-directory: sdk
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Hardware smoke tests (JP108)
run: ./scripts/run_hardware_smoke.sh
- name: Upload hardware smoke report
if: always()
uses: actions/upload-artifact@v4
with:
name: hardware-smoke-108jp
path: harness/reports/*.json
hardware-jphandshake:
if: vars.BITDO_ENABLE_JP_HARDWARE == '1'
runs-on: [self-hosted, linux, hid-lab]
needs: test
env:
BITDO_REQUIRED_FAMILIES: JpHandshake
defaults:
run:
working-directory: sdk
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Hardware smoke tests
run: ./scripts/run_hardware_smoke.sh
- name: Upload hardware smoke report
if: always()
uses: actions/upload-artifact@v4
with:
name: hardware-smoke-jphandshake
path: harness/reports/*.json

287
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,287 @@
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
checks: read
issues: read
jobs:
preflight:
runs-on: ubuntu-latest
defaults:
run:
working-directory: sdk
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Ensure tag commit is on main
working-directory: ${{ github.workspace }}
run: |
set -euo pipefail
git fetch origin main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "Tag commit is not reachable from origin/main" >&2
exit 1
fi
- name: Ensure no open release blockers
working-directory: ${{ github.workspace }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
blocker_count="$(gh api "repos/${GITHUB_REPOSITORY}/issues?state=open&labels=release-blocker&per_page=100" \
--jq '[.[] | select(.pull_request == null)] | length')"
if [[ "$blocker_count" != "0" ]]; then
echo "Open release-blocker issues detected: ${blocker_count}" >&2
gh issue list --repo "$GITHUB_REPOSITORY" --state open --label release-blocker
exit 1
fi
- name: Preflight required publish secrets
working-directory: ${{ github.workspace }}
env:
AUR_USERNAME: ${{ secrets.AUR_USERNAME }}
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
set -euo pipefail
[[ -n "${AUR_USERNAME:-}" ]] || { echo "missing required secret: AUR_USERNAME" >&2; exit 1; }
[[ -n "${AUR_SSH_PRIVATE_KEY:-}" ]] || { echo "missing required secret: AUR_SSH_PRIVATE_KEY" >&2; exit 1; }
[[ -n "${HOMEBREW_TAP_TOKEN:-}" ]] || { echo "missing required secret: HOMEBREW_TAP_TOKEN" >&2; exit 1; }
- name: Require successful CI and hardware checks on tagged commit
working-directory: ${{ github.workspace }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
required_checks=(
guard
aur-validate
tui-smoke-test
build-macos-arm64
test
hardware-ultimate2
hardware-108jp
)
check_runs_json="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/check-runs")"
failed=0
for check in "${required_checks[@]}"; do
conclusion="$(jq -r --arg name "$check" '[.check_runs[] | select(.name == $name) | .conclusion] | first // "missing"' <<<"$check_runs_json")"
if [[ "$conclusion" != "success" ]]; then
echo "required check '${check}' is not successful on ${GITHUB_SHA} (got: ${conclusion})" >&2
failed=1
fi
done
if [[ "$failed" -ne 0 ]]; then
exit 1
fi
- name: Install system deps
run: |
sudo apt-get update
sudo apt-get install -y libudev-dev pkg-config
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Guard
run: ./scripts/cleanroom_guard.sh
- name: Tests
run: cargo test --workspace --all-targets
build-linux-x86_64:
runs-on: ubuntu-latest
needs: preflight
defaults:
run:
working-directory: sdk
steps:
- uses: actions/checkout@v4
- name: Install system deps
run: |
sudo apt-get update
sudo apt-get install -y libudev-dev pkg-config
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Package linux x86_64
run: ./scripts/package-linux.sh "${GITHUB_REF_NAME}" x86_64
- name: Upload linux x86_64 assets
uses: actions/upload-artifact@v4
with:
name: release-linux-x86_64
path: sdk/dist/openbitdo-${{ github.ref_name }}-linux-x86_64*
build-linux-aarch64:
runs-on: ubuntu-24.04-arm
needs: preflight
defaults:
run:
working-directory: sdk
steps:
- uses: actions/checkout@v4
- name: Install system deps
run: |
sudo apt-get update
sudo apt-get install -y libudev-dev pkg-config
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Package linux aarch64
run: ./scripts/package-linux.sh "${GITHUB_REF_NAME}" aarch64
- name: Upload linux aarch64 assets
uses: actions/upload-artifact@v4
with:
name: release-linux-aarch64
path: sdk/dist/openbitdo-${{ github.ref_name }}-linux-aarch64*
build-macos-arm64:
runs-on: macos-14
needs: preflight
defaults:
run:
working-directory: sdk
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Package macOS arm64
run: ./scripts/package-macos.sh "${GITHUB_REF_NAME}" arm64 aarch64-apple-darwin
- name: Upload macOS arm64 assets
uses: actions/upload-artifact@v4
with:
name: release-macos-arm64
path: sdk/dist/openbitdo-${{ github.ref_name }}-macos-arm64*
publish:
runs-on: ubuntu-latest
needs:
- build-linux-x86_64
- build-linux-aarch64
- build-macos-arm64
steps:
- uses: actions/checkout@v4
- name: Download packaged assets
uses: actions/download-artifact@v4
with:
path: release-assets
- name: Determine prerelease flag
id: prerelease
run: |
if [[ "${GITHUB_REF_NAME}" == *"-rc."* ]]; then
echo "value=true" >> "$GITHUB_OUTPUT"
else
echo "value=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish GitHub release
uses: softprops/action-gh-release@v2
with:
prerelease: ${{ steps.prerelease.outputs.value }}
body_path: CHANGELOG.md
files: |
release-assets/**/openbitdo-${{ github.ref_name }}-linux-x86_64
release-assets/**/openbitdo-${{ github.ref_name }}-linux-x86_64.sha256
release-assets/**/openbitdo-${{ github.ref_name }}-linux-x86_64.tar.gz
release-assets/**/openbitdo-${{ github.ref_name }}-linux-x86_64.tar.gz.sha256
release-assets/**/openbitdo-${{ github.ref_name }}-linux-aarch64
release-assets/**/openbitdo-${{ github.ref_name }}-linux-aarch64.sha256
release-assets/**/openbitdo-${{ github.ref_name }}-linux-aarch64.tar.gz
release-assets/**/openbitdo-${{ github.ref_name }}-linux-aarch64.tar.gz.sha256
release-assets/**/openbitdo-${{ github.ref_name }}-macos-arm64
release-assets/**/openbitdo-${{ github.ref_name }}-macos-arm64.sha256
release-assets/**/openbitdo-${{ github.ref_name }}-macos-arm64.tar.gz
release-assets/**/openbitdo-${{ github.ref_name }}-macos-arm64.tar.gz.sha256
release-assets/**/openbitdo-${{ github.ref_name }}-macos-arm64.pkg
release-assets/**/openbitdo-${{ github.ref_name }}-macos-arm64.pkg.sha256
publish-homebrew:
if: vars.HOMEBREW_PUBLISH_ENABLED == '1'
runs-on: ubuntu-latest
needs: publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Wait for release assets
run: |
set -euo pipefail
for attempt in $(seq 1 30); do
if gh release view "${GITHUB_REF_NAME}" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
exit 0
fi
sleep 10
done
echo "release ${GITHUB_REF_NAME} was not found after waiting" >&2
exit 1
- name: Render Homebrew formula with release checksums
run: |
set -euo pipefail
mkdir -p /tmp/release-input /tmp/release-metadata
gh release download "${GITHUB_REF_NAME}" --repo "$GITHUB_REPOSITORY" \
--pattern "openbitdo-${GITHUB_REF_NAME}-linux-x86_64.tar.gz" \
--pattern "openbitdo-${GITHUB_REF_NAME}-linux-aarch64.tar.gz" \
--pattern "openbitdo-${GITHUB_REF_NAME}-macos-arm64.tar.gz" \
--dir /tmp/release-input
gh api -H "Accept: application/octet-stream" "repos/${GITHUB_REPOSITORY}/tarball/${GITHUB_REF_NAME}" \
> "/tmp/release-input/openbitdo-${GITHUB_REF_NAME}-source.tar.gz"
bash packaging/scripts/render_release_metadata.sh \
"${GITHUB_REF_NAME}" \
"$GITHUB_REPOSITORY" \
/tmp/release-input \
/tmp/release-metadata
- name: Upload rendered Homebrew formula (audit)
uses: actions/upload-artifact@v4
with:
name: homebrew-rendered-formula-${{ github.ref_name }}
path: |
/tmp/release-metadata/homebrew/Formula/openbitdo.rb
/tmp/release-metadata/checksums.env
- name: Sync Homebrew tap
env:
HOMEBREW_PUBLISH_ENABLED: "1"
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
HOMEBREW_TAP_REPO: ${{ vars.HOMEBREW_TAP_REPO }}
FORMULA_SOURCE: /tmp/release-metadata/homebrew/Formula/openbitdo.rb
run: |
set -euo pipefail
if [[ -z "${HOMEBREW_TAP_TOKEN:-}" ]]; then
echo "missing required secret: HOMEBREW_TAP_TOKEN" >&2
exit 1
fi
bash packaging/homebrew/sync_tap.sh
publish-aur:
if: vars.AUR_PUBLISH_ENABLED == '1'
needs: publish
uses: ./.github/workflows/aur-publish.yml
with:
tag: ${{ github.ref_name }}
secrets: inherit

12
.gitignore vendored
View File

@@ -1,9 +1,21 @@
# Rust
sdk/target/
sdk/dist/
target/
dist/
# Reports
harness/reports/*.json
# Release/build artifacts
*.tar.gz
*.sha256
*.pkg
release-assets/
packaging/tmp/
packaging/**/tmp/
packaging/**/.cache/
# OS/editor
.DS_Store
Thumbs.db

24
CHANGELOG.md Normal file
View File

@@ -0,0 +1,24 @@
# Changelog
All notable changes to this project will be documented in this file.
## v0.0.1-rc.1
### Added
- Beginner-first `openbitdo` TUI flow with device-specific JP108 and Ultimate2 guided mapping paths.
- About screen showing app version, git commit, build date, and runtime/target platform.
- Release packaging scripts for Linux (`x86_64`, `aarch64`) and macOS arm64 outputs.
- macOS arm64 unsigned/ad-hoc `.pkg` packaging to `/opt/homebrew/bin/openbitdo`.
- AUR packaging sources for `openbitdo` and `openbitdo-bin`.
- Homebrew formula scaffolding and deferred tap sync script.
- Release workflow for tag-triggered GitHub pre-releases using changelog content.
- Release metadata rendering that computes authoritative checksums from published assets for AUR/Homebrew updates.
### Changed
- Project license transitioned to BSD 3-Clause.
- CI expanded to include macOS arm64 package build validation and AUR package metadata validation.
- Release process documentation updated for clean-tree requirements and RC gating policy.
### Notes
- Homebrew and AUR publication paths are automated and remain controlled by repo variables (`HOMEBREW_PUBLISH_ENABLED`, `AUR_PUBLISH_ENABLED`).
- Hardware CI gates remain required as configured in `ci.yml`.

29
LICENSE Normal file
View File

@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2026 OpenBitDo contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

54
MIGRATION.md Normal file
View File

@@ -0,0 +1,54 @@
# OpenBitdo Migration Notes
## What changed
- `bitdoctl` was removed.
- `openbitdo cmd ...` was removed.
- JSON report/output flags were removed from user-facing flows.
- OpenBitdo now focuses on a single beginner entrypoint: `openbitdo`.
## New usage
From `/Users/brooklyn/data/8bitdo/cleanroom/sdk`:
```bash
cargo run -p openbitdo --
```
Optional mock mode:
```bash
cargo run -p openbitdo -- --mock
```
## Beginner flow
1. Launch `openbitdo`.
2. Select a detected device.
3. Click or choose an action:
- `Update` (guided firmware flow)
- `Diagnose` (quick readiness checks)
- `Refresh`
- `Quit`
4. Confirm with a simple `y`/`yes` prompt before firmware transfer.
## Firmware behavior
- OpenBitdo first attempts a recommended firmware download.
- If download or verification fails, it immediately asks for a local firmware file (`.bin`/`.fw`).
- Detect-only devices remain blocked from firmware write operations with a clear reason.
## New device-specific wizards
- JP108 (`0x5209`/`0x520a`):
- Dedicated button mapping for `A`, `B`, and `K1-K8`
- Auto-backup before write
- One-click restore if needed
- Guided button test text after apply
- Ultimate2 (`0x6012`/`0x6013`):
- Slot + mode + core map editing
- Auto-backup and rollback path
- Guided button test text after apply
## CI changes
- Hardware CI split into per-family jobs:
- `hardware-dinput` (required)
- `hardware-standard64` (required)
- `hardware-ultimate2` (required)
- `hardware-108jp` (required)
- `hardware-jphandshake` (gated until fixture availability)

139
RC_CHECKLIST.md Normal file
View File

@@ -0,0 +1,139 @@
# OpenBitdo RC Checklist (`v0.0.1-rc.1`)
This checklist defines release-candidate readiness for the first public RC tag.
## Candidate Policy
- Tag format: `v*` (for this RC: `v0.0.1-rc.1`)
- Tag source: `main` only
- Release trigger: tag push
- RC gate: all required CI checks + manual smoke validation
## Release-Blocker Policy
Use GitHub issue labels:
- `release-blocker`
- `severity:p0`
- `severity:p1`
- `severity:p2`
Public RC gate rule:
- zero open issues labeled `release-blocker`
Daily review cadence:
- run once per day until RC tag:
- `gh issue list -R bybrooklyn/openbitdo --label release-blocker --state open --limit 200`
- release remains blocked while this list is non-empty.
## Scope-Completeness Gate ("Good Point")
Before tagging `v0.0.1-rc.1`, RC scope must match the locked contract:
- JP108 mapping supports dedicated keys only (`A/B/K1-K8`) for RC.
- Ultimate2 expanded mapping supports RC-required fields only:
- remappable slots `A/B/K1-K8`
- known controller-button targets
- profile slot read/write/readback
- firmware version in diagnostics/reports
- L2/R2 analog read (and write where capability allows)
Release gate is checklist-driven for RC (no separate scorecard artifact).
## Clean-Tree Requirement (Before Tagging)
Run from `/Users/brooklyn/data/8bitdo/cleanroom`:
```bash
git status --porcelain
git clean -ndX
```
Expected:
- `git status --porcelain` prints nothing (no modified, staged, or untracked files)
- `git clean -ndX` output reviewed for ignored-build artifacts only
Tracked-path audit:
```bash
git ls-files | rg '(^sdk/dist/|^sdk/target/|^harness/reports/)'
```
Expected:
- no tracked artifact/build-output paths matched
## Required CI Checks
- `guard`
- `aur-validate`
- `tui-smoke-test`
- `build-macos-arm64`
- `test`
- `hardware-ultimate2`
- `hardware-108jp`
Gated/non-required:
- `hardware-jphandshake` (enabled only when `BITDO_ENABLE_JP_HARDWARE=1`)
Hardware execution policy:
- Pull requests run required hardware jobs when surgical runtime/spec paths are touched.
- `main`, nightly, and tag workflows run full required hardware coverage.
- Nightly full hardware run is scheduled for `02:30 America/New_York` policy time (implemented as GitHub cron UTC).
## Release Secret Preflight (Tag Workflow)
Tag preflight must fail early if any required secret is missing:
- `AUR_USERNAME`
- `AUR_SSH_PRIVATE_KEY`
- `HOMEBREW_TAP_TOKEN`
## Artifact Expectations
Release assets must include:
- `openbitdo-v0.0.1-rc.1-linux-x86_64.tar.gz`
- `openbitdo-v0.0.1-rc.1-linux-x86_64`
- `openbitdo-v0.0.1-rc.1-linux-aarch64.tar.gz`
- `openbitdo-v0.0.1-rc.1-linux-aarch64`
- `openbitdo-v0.0.1-rc.1-macos-arm64.tar.gz`
- `openbitdo-v0.0.1-rc.1-macos-arm64`
- `openbitdo-v0.0.1-rc.1-macos-arm64.pkg`
- corresponding `.sha256` files for every artifact above
## Verify Checksums
Run from release asset directory:
```bash
shasum -a 256 -c openbitdo-v0.0.1-rc.1-linux-x86_64.tar.gz.sha256
shasum -a 256 -c openbitdo-v0.0.1-rc.1-linux-x86_64.sha256
shasum -a 256 -c openbitdo-v0.0.1-rc.1-linux-aarch64.tar.gz.sha256
shasum -a 256 -c openbitdo-v0.0.1-rc.1-linux-aarch64.sha256
shasum -a 256 -c openbitdo-v0.0.1-rc.1-macos-arm64.tar.gz.sha256
shasum -a 256 -c openbitdo-v0.0.1-rc.1-macos-arm64.sha256
shasum -a 256 -c openbitdo-v0.0.1-rc.1-macos-arm64.pkg.sha256
```
## Manual Smoke Matrix
1. Linux `x86_64`
- Extract tarball, run `./bin/openbitdo --mock`
- Confirm waiting/home flow renders
- Confirm About page opens (`a` and mouse click)
2. Linux `aarch64`
- Extract tarball, run `./bin/openbitdo --mock`
- Confirm main navigation and update preflight render
3. macOS arm64
- Run standalone binary `openbitdo --mock`
- Install `.pkg`, then run `/opt/homebrew/bin/openbitdo --mock`
- Confirm launch and About page behavior
## Distribution Readiness Notes
- Homebrew publication runs after release asset publish when `HOMEBREW_PUBLISH_ENABLED=1`.
- AUR publication runs after release asset publish when `AUR_PUBLISH_ENABLED=1`.
- Both package paths use release-derived SHA256 values (no `SKIP`, no `:no_check` in published metadata).
## RC Gate Snapshot (Local)
| Gate | Status | Notes |
| --- | --- | --- |
| Clean tree | Pending | Will pass after local baseline commit if `git status --porcelain` is empty. |
| Secrets present | Pass | `AUR_USERNAME`, `AUR_SSH_PRIVATE_KEY`, `HOMEBREW_TAP_TOKEN` exist in repo secrets. |
| Required checks configured | Pass | `guard`, `test`, `tui-smoke-test`, `aur-validate`, `build-macos-arm64`, `hardware-108jp`, `hardware-ultimate2`. |
| Open release-blocker issues | Fail | `1` open (`Wave 2 Dirty-Room Expansion (+12 Popularity Set)`). |
| RC release allowed | Fail | `No` while release-blocker count is non-zero. |
## RC Execution Log
- 2026-03-02T20:54:31Z: governance preflight complete; release blocker remains open by policy.
- Local baseline commit snapshot:
- commit hash: `PENDING_AFTER_COMMIT` (retrieve with `git rev-parse --short HEAD`)
- clean-tree check: run `git status --porcelain` and expect empty output.

169
README.md
View File

@@ -1,8 +1,167 @@
# OpenBitdo
OpenBitdo is a clean-room implementation workspace for 8BitDo protocol tooling.
OpenBitdo is a clean-room implementation workspace for beginner-friendly 8BitDo tooling.
OpenBitDo is an unofficial tool and should be used at your own risk. The authors are not responsible for device damage, firmware corruption, or data loss. This project is not affiliated with or endorsed by 8BitDo. See the [LICENSE](/Users/brooklyn/data/8bitdo/cleanroom/LICENSE) file for details.
- `spec/`: sanitized protocol, requirements, PID matrix, command matrix
- `process/`: clean-room workflow and branch policy documents
- `harness/`: golden fixtures, lab config, and hardware smoke reports
- `sdk/`: Rust workspace (`bitdo_proto` library + `bitdoctl` CLI)
## Beginner Quickstart
From `/Users/brooklyn/data/8bitdo/cleanroom/sdk`:
```bash
cargo run -p openbitdo --
```
Optional mock mode:
```bash
cargo run -p openbitdo -- --mock
```
Beginner flow is always `openbitdo` only. No extra command surface is required.
## UI Language Support
| Language | Status | Notes |
| --- | --- | --- |
| English | Supported | Default and primary beginner flow language. |
| Spanish | In Progress | Roadmap item for core wizard screens. |
| Japanese | Planned | Planned after Spanish stabilization. |
| German | Planned | Planned after Spanish stabilization. |
| French | Planned | Planned after Spanish stabilization. |
## Support Status Model
| Runtime Tier | README Status | Meaning |
| --- | --- | --- |
| `full` | Supported | Mapping/update paths allowed when safety gates are satisfied. |
| `candidate-readonly` | In Progress | Detect/identify/diagnostics enabled, writes and firmware transfer blocked. |
| `detect-only` | Planned | Detect/identify baseline now, broader functionality planned. |
Beginner UI note: candidate devices are shown with blocked-action messaging in the TUI so new users always see the safe path first.
## Protocol Family Overview
| Family | PID Rows | Notes |
| --- | --- | --- |
| Standard64 | 36 | Largest family, mixed status from Planned to Supported. |
| DInput | 11 | Includes current full-support Ultimate2 and Pro3 lines. |
| JpHandshake | 9 | Includes JP108 full support and keyboard-related candidate lines. |
| DS4Boot | 0 | No standalone canonical PID rows in current catalog. |
| Unknown | 1 | Sentinel/internal row only. |
DS4Boot explicit note: there are currently no active canonical PID rows in this catalog.
## Compatibility Verification Dates (Manual, ISO)
| Family | Last verified date | Notes |
| --- | --- | --- |
| Standard64 | 2026-03-02 | Manual RC verification date. |
| DInput | 2026-03-02 | Manual RC verification date. |
| JpHandshake | 2026-03-02 | Manual RC verification date. |
| DS4Boot | 2026-03-02 | No active canonical rows. |
| Unknown | 2026-03-02 | Sentinel/internal row only. |
## Device Compatibility (Canonical, No Duplicate PIDs)
Device naming references are documented in [device_name_catalog.md](/Users/brooklyn/data/8bitdo/cleanroom/spec/device_name_catalog.md) and [device_name_sources.md](/Users/brooklyn/data/8bitdo/cleanroom/process/device_name_sources.md).
### Unknown
| Canonical ID | Display Name | PID (hex) | Family | Status | Current User Actions | Firmware Path | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `PID_None` | No Device (Sentinel) | `0x0000` | Unknown | Planned | None | N/A | Internal sentinel row only. |
### Standard64
| Canonical ID | Display Name | PID (hex) | Family | Status | Current User Actions | Firmware Path | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `PID_IDLE` | Unconfirmed Internal Device (PID_IDLE) | `0x3109` | Standard64 | Planned | Detect, identify | Blocked | Internal interface row. |
| `PID_SN30Plus` | SN30 Pro+ | `0x6002` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_USB_Ultimate` | Unconfirmed Internal Device (PID_USB_Ultimate) | `0x3100` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_USB_Ultimate2` | Unconfirmed Internal Device (PID_USB_Ultimate2) | `0x3105` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_USB_UltimateClasses` | Unconfirmed Internal Device (PID_USB_UltimateClasses) | `0x3104` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_Xcloud` | Unconfirmed Internal Device (PID_Xcloud) | `0x2100` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_Xcloud2` | Unconfirmed Internal Device (PID_Xcloud2) | `0x2101` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_ArcadeStick` | Arcade Stick | `0x901a` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_Pro2` | Pro 2 Bluetooth Controller | `0x6003` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_Pro2_CY` | Unconfirmed Variant Name (PID_Pro2_CY) | `0x6006` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_Pro2_Wired` | Pro 2 Wired Controller | `0x3010` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_Ultimate_PC` | Ultimate PC Controller | `0x3011` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_Ultimate2_4` | Ultimate 2.4G Controller | `0x3012` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_Ultimate2_4RR` | Ultimate 2.4G Adapter | `0x3013` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_UltimateBT` | Ultimate Wireless Controller | `0x6007` | Standard64 | Supported | Detect, diagnose, recommended update | Stable download + local fallback | Full-support runtime. |
| `PID_UltimateBTRR` | Ultimate Wireless Adapter | `0x3106` | Standard64 | Supported | Detect, diagnose, recommended update | Stable download + local fallback | Full-support runtime. |
| `PID_NUMPAD` | Retro 18 Mechanical Numpad | `0x5203` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_NUMPADRR` | Retro 18 Adapter | `0x5204` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_Pro3DOCK` | Charging Dock for Pro 3S Gamepad | `0x600d` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_NGCDIY` | Mod Kit for NGC Controller | `0x5750` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_NGCRR` | Retro Receiver for NGC | `0x902a` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_Mouse` | Retro R8 Mouse | `0x5205` | Standard64 | Planned | Detect, identify | Blocked | Canonical row for `0x5205`. |
| `PID_MouseRR` | Retro R8 Adapter | `0x5206` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_SaturnRR` | Retro Receiver for Saturn | `0x902b` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_UltimateBT2C` | Ultimate 2C Bluetooth Controller | `0x301a` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_Lashen` | Ultimate Mobile Gaming Controller | `0x301e` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_N64BT` | 64 Bluetooth Controller | `0x3019` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_N64` | 64 2.4G Wireless Controller | `0x3004` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_N64RR` | Retro Receiver for N64 | `0x9028` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_XBOXUK` | Retro 87 Mechanical Keyboard - Xbox (UK) | `0x3026` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_XBOXUKUSB` | Retro 87 Mechanical Keyboard Adapter - Xbox (UK) | `0x3027` | Standard64 | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_LashenX` | Ultimate Mobile Gaming Controller For Xbox | `0x200b` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_N64JoySticks` | Joystick v2 for N64 Controller | `0x3021` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_DoubleSuper` | Wireless Dual Super Button | `0x203e` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_Cube2RR` | Retro Cube 2 Adapter - N Edition | `0x2056` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_Cube2` | Retro Cube 2 Speaker - N Edition | `0x2039` | Standard64 | Planned | Detect, identify | Blocked | Planned support only. |
### JpHandshake
| Canonical ID | Display Name | PID (hex) | Family | Status | Current User Actions | Firmware Path | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `PID_JP` | Retro Mechanical Keyboard | `0x5200` | JpHandshake | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_JPUSB` | Retro Mechanical Keyboard Receiver | `0x5201` | JpHandshake | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_108JP` | Retro 108 Mechanical Keyboard | `0x5209` | JpHandshake | Supported | Detect, diagnose, dedicated mapping (`A/B/K1-K8`), recommended update | Stable download + local fallback | Full-support JP108 flow. |
| `PID_108JPUSB` | Retro 108 Mechanical Adapter | `0x520a` | JpHandshake | Supported | Detect, diagnose, dedicated mapping (`A/B/K1-K8`), recommended update | Stable download + local fallback | Full-support JP108 receiver flow. |
| `PID_XBOXJP` | Retro 87 Mechanical Keyboard - Xbox Edition | `0x2028` | JpHandshake | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_XBOXJPUSB` | Retro 87 Mechanical Keyboard Adapter - Xbox Edition | `0x202e` | JpHandshake | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_68JP` | Retro 68 Keyboard - N40 | `0x203a` | JpHandshake | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_68JPUSB` | Retro 68 Keyboard Adapter - N40 | `0x2049` | JpHandshake | In Progress | Detect, identify, diagnose | Blocked (`NotHardwareConfirmed`) | Candidate read-only. |
| `PID_ASLGJP` | Riviera Keyboard | `0x205a` | JpHandshake | Planned | Detect, identify | Blocked | Planned support only. |
### DInput
| Canonical ID | Display Name | PID (hex) | Family | Status | Current User Actions | Firmware Path | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `PID_QINGCHUN2` | Ultimate 2C Controller | `0x310a` | DInput | Supported | Detect, diagnose, recommended update | Stable download + local fallback | Full-support runtime. |
| `PID_QINGCHUN2RR` | Ultimate 2C Wireless Adapter | `0x301c` | DInput | Supported | Detect, diagnose, recommended update | Stable download + local fallback | Full-support runtime. |
| `PID_Xinput` | Unconfirmed Interface Name (PID_Xinput) | `0x310b` | DInput | Planned | Detect, identify | Blocked | Planned support only. |
| `PID_Pro3` | Pro 3 Bluetooth Gamepad | `0x6009` | DInput | Supported | Detect, diagnose, recommended update | Stable download + local fallback | Full-support runtime. |
| `PID_Pro3USB` | Pro 3 Bluetooth Adapter | `0x600a` | DInput | Supported | Detect, diagnose, recommended update | Stable download + local fallback | Full-support runtime. |
| `PID_Ultimate2` | Ultimate 2 Wireless Controller | `0x6012` | DInput | Supported | Detect, diagnose, slot mapping (`A/B/K1-K8`), analog L2/R2, recommended update | Stable download + local fallback | Full-support Ultimate2 RC scope (known controller-button targets). |
| `PID_Ultimate2RR` | Ultimate 2 Wireless Adapter | `0x6013` | DInput | Supported | Detect, diagnose, slot mapping (`A/B/K1-K8`), analog L2/R2, recommended update | Stable download + local fallback | Full-support Ultimate2 receiver RC scope (known controller-button targets). |
| `PID_UltimateBT2` | Ultimate 2 Bluetooth Controller | `0x600f` | DInput | Supported | Detect, diagnose, recommended update | Stable download + local fallback | Full-support runtime. |
| `PID_UltimateBT2RR` | Ultimate 2 Bluetooth Adapter | `0x6011` | DInput | Supported | Detect, diagnose, recommended update | Stable download + local fallback | Full-support runtime. |
| `PID_HitBox` | Arcade Controller | `0x600b` | DInput | Supported | Detect, diagnose, recommended update | Stable download + local fallback | Full-support runtime. |
| `PID_HitBoxRR` | Arcade Controller Adapter | `0x600c` | DInput | Supported | Detect, diagnose, recommended update | Stable download + local fallback | Full-support runtime. |
## Alias Appendix (Non-Canonical Names)
These aliases are intentionally excluded from canonical PID rows to guarantee uniqueness.
| Alias PID Name | Canonical PID Name | PID (hex) |
| --- | --- | --- |
| `PID_Pro2_OLD` | `PID_Pro2` | `0x6003` |
| `PID_ASLGMouse` | `PID_Mouse` | `0x5205` |
See [alias_index.md](/Users/brooklyn/data/8bitdo/cleanroom/spec/alias_index.md) for details.
## Hardware Support Confidence
Support is implemented to our best current knowledge. Coverage and confidence are expanded and confirmed over time through community testing and hardware reports.
## Dirty-Room Evidence Backlog
- [dirtyroom_evidence_backlog.md](/Users/brooklyn/data/8bitdo/cleanroom/process/dirtyroom_evidence_backlog.md)
- [dirtyroom_collection_playbook.md](/Users/brooklyn/data/8bitdo/cleanroom/process/dirtyroom_collection_playbook.md)
- [dirtyroom_dossier_schema.md](/Users/brooklyn/data/8bitdo/cleanroom/process/dirtyroom_dossier_schema.md)
- [community_evidence_intake.md](/Users/brooklyn/data/8bitdo/cleanroom/process/community_evidence_intake.md)
## Source Notes
- Canonical clean-room naming catalog: [device_name_catalog.md](/Users/brooklyn/data/8bitdo/cleanroom/spec/device_name_catalog.md)
- Dirty-room source index + official web cross-check URLs: [device_name_sources.md](/Users/brooklyn/data/8bitdo/cleanroom/process/device_name_sources.md)
## Public RC Gate
`v0.0.1-rc.1` remains blocked until release-blocker issues are zero and all required checks are green.
RC gating is checklist-based (see [RC_CHECKLIST.md](/Users/brooklyn/data/8bitdo/cleanroom/RC_CHECKLIST.md)).

1
VERSION Normal file
View File

@@ -0,0 +1 @@
v0.0.1-rc.1

View File

@@ -0,0 +1,4 @@
operation = "jp108_read_dedicated_mapping"
pid = "0x5209"
status = "sanitized-placeholder"
notes = "Trace placeholder for clean-room replay harness"

View File

@@ -0,0 +1,4 @@
operation = "jp108_write_dedicated_mapping"
pid = "0x520a"
status = "sanitized-placeholder"
notes = "Trace placeholder for clean-room replay harness"

View File

@@ -0,0 +1,4 @@
operation = "u2_read_core_profile"
pid = "0x6012"
status = "sanitized-placeholder"
notes = "Trace placeholder for clean-room replay harness"

View File

@@ -0,0 +1,4 @@
operation = "u2_write_core_profile"
pid = "0x6013"
status = "sanitized-placeholder"
notes = "Trace placeholder for clean-room replay harness"

View File

@@ -1,8 +1,9 @@
version: 1
devices:
- name: Pro3
- name: Ultimate2
fixture_id: ultimate2
vid: 0x2dc8
pid: 0x6009
pid: 0x6012
capability:
supports_mode: true
supports_profile_rw: true
@@ -10,17 +11,30 @@ devices:
supports_firmware: true
protocol_family: DInput
evidence: confirmed
- name: UltimateBT2
- name: JP108
fixture_id: 108jp
vid: 0x2dc8
pid: 0x600f
pid: 0x5209
capability:
supports_mode: true
supports_profile_rw: true
supports_mode: false
supports_profile_rw: false
supports_boot: true
supports_firmware: true
protocol_family: DInput
protocol_family: JpHandshake
evidence: confirmed
- name: JPFixture
fixture_id: jphandshake
vid: 0x2dc8
pid: 0x5200
capability:
supports_mode: false
supports_profile_rw: false
supports_boot: false
supports_firmware: false
protocol_family: JpHandshake
evidence: untested
policies:
require_mock_suite: true
hardware_jobs_non_blocking: true
hardware_jobs_non_blocking: false
jphandshake_gated_until_fixture_ready: true
promote_after_stable_weeks: 2

19
packaging/aur/README.md Normal file
View File

@@ -0,0 +1,19 @@
# AUR Packaging
This directory contains AUR package sources for:
- `openbitdo` (source build)
- `openbitdo-bin` (prebuilt release assets)
Publishing is automated by `.github/workflows/aur-publish.yml` and remains gated:
- requires repository variable `AUR_PUBLISH_ENABLED=1`
- requires secrets `AUR_SSH_PRIVATE_KEY` and `AUR_USERNAME`
Publish flow:
1. wait for release assets from a `v*` tag
2. compute authoritative SHA256 values from released artifacts
3. render `PKGBUILD`/`.SRCINFO` with pinned hashes
4. push updates to AUR repos
Template files used for release rendering:
- `openbitdo/PKGBUILD.tmpl`
- `openbitdo-bin/PKGBUILD.tmpl`

View File

@@ -0,0 +1,15 @@
pkgbase = openbitdo-bin
pkgdesc = Prebuilt beginner-first clean-room 8BitDo utility
pkgver = 0.0.1rc1
pkgrel = 1
url = https://github.com/bybrooklyn/openbitdo
arch = x86_64
arch = aarch64
license = BSD-3-Clause
depends = hidapi
source_x86_64 = openbitdo-v0.0.1-rc.1-linux-x86_64.tar.gz::https://github.com/bybrooklyn/openbitdo/releases/download/v0.0.1-rc.1/openbitdo-v0.0.1-rc.1-linux-x86_64.tar.gz
sha256sums_x86_64 = 0000000000000000000000000000000000000000000000000000000000000000
source_aarch64 = openbitdo-v0.0.1-rc.1-linux-aarch64.tar.gz::https://github.com/bybrooklyn/openbitdo/releases/download/v0.0.1-rc.1/openbitdo-v0.0.1-rc.1-linux-aarch64.tar.gz
sha256sums_aarch64 = 0000000000000000000000000000000000000000000000000000000000000000
pkgname = openbitdo-bin

View File

@@ -0,0 +1,26 @@
pkgname=openbitdo-bin
pkgver=0.0.1rc1
_upstream_tag=v0.0.1-rc.1
pkgrel=1
pkgdesc="Prebuilt beginner-first clean-room 8BitDo utility"
arch=('x86_64' 'aarch64')
url="https://github.com/bybrooklyn/openbitdo"
license=('BSD-3-Clause')
depends=('hidapi')
source_x86_64=("openbitdo-${_upstream_tag}-linux-x86_64.tar.gz::${url}/releases/download/${_upstream_tag}/openbitdo-${_upstream_tag}-linux-x86_64.tar.gz")
source_aarch64=("openbitdo-${_upstream_tag}-linux-aarch64.tar.gz::${url}/releases/download/${_upstream_tag}/openbitdo-${_upstream_tag}-linux-aarch64.tar.gz")
sha256sums_x86_64=('0000000000000000000000000000000000000000000000000000000000000000')
sha256sums_aarch64=('0000000000000000000000000000000000000000000000000000000000000000')
package() {
local extracted_dir
extracted_dir="$(find "${srcdir}" -maxdepth 1 -type d -name "openbitdo-${_upstream_tag}-linux-${CARCH}" | head -n 1)"
if [[ -z "${extracted_dir}" ]]; then
echo "unable to locate extracted release payload for ${CARCH}" >&2
return 1
fi
install -Dm755 "${extracted_dir}/bin/openbitdo" "${pkgdir}/usr/bin/openbitdo"
install -Dm644 "${extracted_dir}/README.md" "${pkgdir}/usr/share/doc/openbitdo/README.md"
install -Dm644 "${extracted_dir}/LICENSE" "${pkgdir}/usr/share/licenses/openbitdo/LICENSE"
}

View File

@@ -0,0 +1,26 @@
pkgname=openbitdo-bin
pkgver=@AUR_PKGVER@
_upstream_tag=@UPSTREAM_TAG@
pkgrel=1
pkgdesc="Prebuilt beginner-first clean-room 8BitDo utility"
arch=('x86_64' 'aarch64')
url="https://github.com/@REPOSITORY@"
license=('BSD-3-Clause')
depends=('hidapi')
source_x86_64=("openbitdo-${_upstream_tag}-linux-x86_64.tar.gz::${url}/releases/download/${_upstream_tag}/openbitdo-${_upstream_tag}-linux-x86_64.tar.gz")
source_aarch64=("openbitdo-${_upstream_tag}-linux-aarch64.tar.gz::${url}/releases/download/${_upstream_tag}/openbitdo-${_upstream_tag}-linux-aarch64.tar.gz")
sha256sums_x86_64=('@LINUX_X86_64_SHA256@')
sha256sums_aarch64=('@LINUX_AARCH64_SHA256@')
package() {
local extracted_dir
extracted_dir="$(find "${srcdir}" -maxdepth 1 -type d -name "openbitdo-${_upstream_tag}-linux-${CARCH}" | head -n 1)"
if [[ -z "${extracted_dir}" ]]; then
echo "unable to locate extracted release payload for ${CARCH}" >&2
return 1
fi
install -Dm755 "${extracted_dir}/bin/openbitdo" "${pkgdir}/usr/bin/openbitdo"
install -Dm644 "${extracted_dir}/README.md" "${pkgdir}/usr/share/doc/openbitdo/README.md"
install -Dm644 "${extracted_dir}/LICENSE" "${pkgdir}/usr/share/licenses/openbitdo/LICENSE"
}

View File

@@ -0,0 +1,14 @@
pkgbase = openbitdo
pkgdesc = Beginner-first clean-room 8BitDo utility
pkgver = 0.0.1rc1
pkgrel = 1
url = https://github.com/bybrooklyn/openbitdo
arch = x86_64
arch = aarch64
license = BSD-3-Clause
makedepends = cargo
depends = hidapi
source = openbitdo-v0.0.1-rc.1.tar.gz::https://github.com/bybrooklyn/openbitdo/archive/refs/tags/v0.0.1-rc.1.tar.gz
sha256sums = 0000000000000000000000000000000000000000000000000000000000000000
pkgname = openbitdo

View File

@@ -0,0 +1,24 @@
pkgname=openbitdo
pkgver=0.0.1rc1
_upstream_tag=v0.0.1-rc.1
pkgrel=1
pkgdesc="Beginner-first clean-room 8BitDo utility"
arch=('x86_64' 'aarch64')
url="https://github.com/bybrooklyn/openbitdo"
license=('BSD-3-Clause')
depends=('hidapi')
makedepends=('cargo')
source=("${pkgname}-${_upstream_tag}.tar.gz::${url}/archive/refs/tags/${_upstream_tag}.tar.gz")
sha256sums=('0000000000000000000000000000000000000000000000000000000000000000')
build() {
cd "${srcdir}/openbitdo-${_upstream_tag#v}/sdk"
cargo build --release -p openbitdo
}
package() {
cd "${srcdir}/openbitdo-${_upstream_tag#v}"
install -Dm755 "sdk/target/release/openbitdo" "${pkgdir}/usr/bin/openbitdo"
install -Dm644 "README.md" "${pkgdir}/usr/share/doc/openbitdo/README.md"
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/openbitdo/LICENSE"
}

View File

@@ -0,0 +1,24 @@
pkgname=openbitdo
pkgver=@AUR_PKGVER@
_upstream_tag=@UPSTREAM_TAG@
pkgrel=1
pkgdesc="Beginner-first clean-room 8BitDo utility"
arch=('x86_64' 'aarch64')
url="https://github.com/@REPOSITORY@"
license=('BSD-3-Clause')
depends=('hidapi')
makedepends=('cargo')
source=("${pkgname}-${_upstream_tag}.tar.gz::${url}/archive/refs/tags/${_upstream_tag}.tar.gz")
sha256sums=('@SOURCE_SHA256@')
build() {
cd "${srcdir}/openbitdo-${_upstream_tag#v}/sdk"
cargo build --release -p openbitdo
}
package() {
cd "${srcdir}/openbitdo-${_upstream_tag#v}"
install -Dm755 "sdk/target/release/openbitdo" "${pkgdir}/usr/bin/openbitdo"
install -Dm644 "README.md" "${pkgdir}/usr/share/doc/openbitdo/README.md"
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/openbitdo/LICENSE"
}

View File

@@ -0,0 +1,33 @@
class Openbitdo < Formula
desc "Beginner-first clean-room 8BitDo TUI utility"
homepage "https://github.com/bybrooklyn/openbitdo"
license "BSD-3-Clause"
version "0.0.1-rc.1"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/bybrooklyn/openbitdo/releases/download/v0.0.1-rc.1/openbitdo-v0.0.1-rc.1-macos-arm64.tar.gz"
sha256 "0000000000000000000000000000000000000000000000000000000000000000"
end
end
on_linux do
if Hardware::CPU.intel?
url "https://github.com/bybrooklyn/openbitdo/releases/download/v0.0.1-rc.1/openbitdo-v0.0.1-rc.1-linux-x86_64.tar.gz"
sha256 "0000000000000000000000000000000000000000000000000000000000000000"
elsif Hardware::CPU.arm?
url "https://github.com/bybrooklyn/openbitdo/releases/download/v0.0.1-rc.1/openbitdo-v0.0.1-rc.1-linux-aarch64.tar.gz"
sha256 "0000000000000000000000000000000000000000000000000000000000000000"
end
end
# Release automation rewrites checksums in the tap with authoritative values
# from published assets.
def install
bin.install "bin/openbitdo"
end
test do
assert_match "openbitdo", shell_output("#{bin}/openbitdo --help")
end
end

View File

@@ -0,0 +1,31 @@
class Openbitdo < Formula
desc "Beginner-first clean-room 8BitDo TUI utility"
homepage "https://github.com/@REPOSITORY@"
license "BSD-3-Clause"
version "@VERSION@"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/@REPOSITORY@/releases/download/@UPSTREAM_TAG@/openbitdo-@UPSTREAM_TAG@-macos-arm64.tar.gz"
sha256 "@MACOS_ARM64_SHA256@"
end
end
on_linux do
if Hardware::CPU.intel?
url "https://github.com/@REPOSITORY@/releases/download/@UPSTREAM_TAG@/openbitdo-@UPSTREAM_TAG@-linux-x86_64.tar.gz"
sha256 "@LINUX_X86_64_SHA256@"
elsif Hardware::CPU.arm?
url "https://github.com/@REPOSITORY@/releases/download/@UPSTREAM_TAG@/openbitdo-@UPSTREAM_TAG@-linux-aarch64.tar.gz"
sha256 "@LINUX_AARCH64_SHA256@"
end
end
def install
bin.install "bin/openbitdo"
end
test do
assert_match "openbitdo", shell_output("#{bin}/openbitdo --help")
end
end

View File

@@ -0,0 +1,14 @@
# Homebrew Packaging
Formula source lives in `Formula/openbitdo.rb`.
Template source used for release rendering: `Formula/openbitdo.rb.tmpl`.
Planned tap:
- `bybrooklyn/homebrew-openbitdo`
Current status:
- release workflow computes checksum-pinned formula values from published assets
- tap sync remains gated by `HOMEBREW_PUBLISH_ENABLED=1`
Optional sync helper:
- `sync_tap.sh` (disabled by default unless `HOMEBREW_PUBLISH_ENABLED=1`)

36
packaging/homebrew/sync_tap.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "${HOMEBREW_PUBLISH_ENABLED:-0}" != "1" ]]; then
echo "homebrew tap sync disabled (set HOMEBREW_PUBLISH_ENABLED=1 to enable)"
exit 0
fi
if [[ -z "${HOMEBREW_TAP_TOKEN:-}" ]]; then
echo "missing HOMEBREW_TAP_TOKEN" >&2
exit 1
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TAP_REPO="${HOMEBREW_TAP_REPO:-bybrooklyn/homebrew-openbitdo}"
FORMULA_SOURCE="${FORMULA_SOURCE:-$ROOT/packaging/homebrew/Formula/openbitdo.rb}"
TMP="$(mktemp -d)"
if [[ ! -f "$FORMULA_SOURCE" ]]; then
echo "formula source not found: $FORMULA_SOURCE" >&2
exit 1
fi
git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/${TAP_REPO}.git" "$TMP/tap"
mkdir -p "$TMP/tap/Formula"
cp "$FORMULA_SOURCE" "$TMP/tap/Formula/openbitdo.rb"
cd "$TMP/tap"
git config user.name "${GIT_AUTHOR_NAME:-openbitdo-ci}"
git config user.email "${GIT_AUTHOR_EMAIL:-actions@users.noreply.github.com}"
git add Formula/openbitdo.rb
git commit -m "Update openbitdo formula" || {
echo "no formula changes to push"
exit 0
}
git push

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
render_release_metadata.sh <tag> <repository> <input_dir> <output_dir>
Inputs expected in <input_dir>:
openbitdo-<tag>-source.tar.gz
openbitdo-<tag>-linux-x86_64.tar.gz
openbitdo-<tag>-linux-aarch64.tar.gz
openbitdo-<tag>-macos-arm64.tar.gz
EOF
}
if [[ $# -ne 4 ]]; then
usage >&2
exit 1
fi
TAG="$1"
REPOSITORY="$2"
INPUT_DIR="$3"
OUTPUT_DIR="$4"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
sha256() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
else
shasum -a 256 "$1" | awk '{print $1}'
fi
}
aur_pkgver_from_tag() {
local version
version="${1#v}"
version="${version/-rc./rc}"
version="${version/-alpha./alpha}"
version="${version/-beta./beta}"
echo "$version"
}
VERSION="${TAG#v}"
AUR_PKGVER="$(aur_pkgver_from_tag "$TAG")"
SOURCE_ARCHIVE="${INPUT_DIR}/openbitdo-${TAG}-source.tar.gz"
LINUX_X86_ARCHIVE="${INPUT_DIR}/openbitdo-${TAG}-linux-x86_64.tar.gz"
LINUX_AARCH64_ARCHIVE="${INPUT_DIR}/openbitdo-${TAG}-linux-aarch64.tar.gz"
MACOS_ARM64_ARCHIVE="${INPUT_DIR}/openbitdo-${TAG}-macos-arm64.tar.gz"
for required in \
"$SOURCE_ARCHIVE" \
"$LINUX_X86_ARCHIVE" \
"$LINUX_AARCH64_ARCHIVE" \
"$MACOS_ARM64_ARCHIVE"; do
if [[ ! -f "$required" ]]; then
echo "missing required release input: $required" >&2
exit 1
fi
done
SOURCE_SHA256="$(sha256 "$SOURCE_ARCHIVE")"
LINUX_X86_SHA256="$(sha256 "$LINUX_X86_ARCHIVE")"
LINUX_AARCH64_SHA256="$(sha256 "$LINUX_AARCH64_ARCHIVE")"
MACOS_ARM64_SHA256="$(sha256 "$MACOS_ARM64_ARCHIVE")"
mkdir -p \
"${OUTPUT_DIR}/aur/openbitdo" \
"${OUTPUT_DIR}/aur/openbitdo-bin" \
"${OUTPUT_DIR}/homebrew/Formula"
render() {
local template="$1"
local destination="$2"
sed \
-e "s|@AUR_PKGVER@|${AUR_PKGVER}|g" \
-e "s|@UPSTREAM_TAG@|${TAG}|g" \
-e "s|@VERSION@|${VERSION}|g" \
-e "s|@REPOSITORY@|${REPOSITORY}|g" \
-e "s|@SOURCE_SHA256@|${SOURCE_SHA256}|g" \
-e "s|@LINUX_X86_64_SHA256@|${LINUX_X86_SHA256}|g" \
-e "s|@LINUX_AARCH64_SHA256@|${LINUX_AARCH64_SHA256}|g" \
-e "s|@MACOS_ARM64_SHA256@|${MACOS_ARM64_SHA256}|g" \
"$template" > "$destination"
}
render \
"${ROOT}/packaging/aur/openbitdo/PKGBUILD.tmpl" \
"${OUTPUT_DIR}/aur/openbitdo/PKGBUILD"
render \
"${ROOT}/packaging/aur/openbitdo-bin/PKGBUILD.tmpl" \
"${OUTPUT_DIR}/aur/openbitdo-bin/PKGBUILD"
render \
"${ROOT}/packaging/homebrew/Formula/openbitdo.rb.tmpl" \
"${OUTPUT_DIR}/homebrew/Formula/openbitdo.rb"
cat > "${OUTPUT_DIR}/checksums.env" <<EOF
TAG=${TAG}
VERSION=${VERSION}
AUR_PKGVER=${AUR_PKGVER}
REPOSITORY=${REPOSITORY}
SOURCE_SHA256=${SOURCE_SHA256}
LINUX_X86_64_SHA256=${LINUX_X86_SHA256}
LINUX_AARCH64_SHA256=${LINUX_AARCH64_SHA256}
MACOS_ARM64_SHA256=${MACOS_ARM64_SHA256}
EOF
echo "rendered release metadata into ${OUTPUT_DIR}"

View File

@@ -0,0 +1,68 @@
# Add Device Support (Hardcoded Path)
This guide keeps device support simple and explicit: everything is added directly in Rust code.
## 1) Add/verify PID in hardcoded registry
File:
- `/Users/brooklyn/data/8bitdo/cleanroom/sdk/crates/bitdo_proto/src/pid_registry_table.rs`
Add a `PidRegistryRow` with:
- `name`
- `pid`
- `support_level`
- `support_tier`
- `protocol_family`
## 2) Update capability policy
File:
- `/Users/brooklyn/data/8bitdo/cleanroom/sdk/crates/bitdo_proto/src/registry.rs`
Update `default_capability_for(...)` and support-tier PID lists so capability flags match evidence.
## 3) Add/verify command declarations
File:
- `/Users/brooklyn/data/8bitdo/cleanroom/sdk/crates/bitdo_proto/src/command_registry_table.rs`
Add/verify command rows:
- `id`
- `safety_class`
- `confidence`
- `experimental_default`
- `report_id`
- `request`
- `expected_response`
- `applies_to`
- `operation_group`
## 4) Confirm runtime policy
Runtime policy is derived in code (not scripts):
- `confirmed` -> enabled by default
- inferred `SafeRead` -> experimental-gated
- inferred `SafeWrite`/unsafe -> blocked until confirmed
File:
- `/Users/brooklyn/data/8bitdo/cleanroom/sdk/crates/bitdo_proto/src/registry.rs`
## 5) Update candidate gating allowlists
File:
- `/Users/brooklyn/data/8bitdo/cleanroom/sdk/crates/bitdo_proto/src/session.rs`
Update `is_command_allowed_for_candidate_pid(...)` so detect/diag behavior for the new PID is explicit.
## 6) Keep spec artifacts in sync
Files:
- `/Users/brooklyn/data/8bitdo/cleanroom/spec/pid_matrix.csv`
- `/Users/brooklyn/data/8bitdo/cleanroom/spec/command_matrix.csv`
- `/Users/brooklyn/data/8bitdo/cleanroom/spec/evidence_index.csv`
- `/Users/brooklyn/data/8bitdo/cleanroom/spec/dossiers/...`
## 7) Add tests
- Extend candidate gating tests:
- `/Users/brooklyn/data/8bitdo/cleanroom/sdk/tests/candidate_readonly_gating.rs`
- Extend runtime policy tests:
- `/Users/brooklyn/data/8bitdo/cleanroom/sdk/tests/runtime_policy.rs`
## 8) Validation
From `/Users/brooklyn/data/8bitdo/cleanroom/sdk`:
- `cargo test --workspace --all-targets`
- `./scripts/cleanroom_guard.sh`

View File

@@ -0,0 +1,17 @@
# OpenBitdo Commenting Standard
This project prefers concise, high-context comments.
## Required Comment Zones
- Command gating order and rationale (`bitdo_proto::session`).
- Support-tier decisions and promotion boundaries (`candidate-readonly` vs `full`).
- Unsafe/firmware blocking rules and brick-risk protections.
- Retry/fallback behavior where multiple command paths exist.
- State-machine transitions in TUI/app-core flows when transitions are non-obvious.
## Avoid
- Trivial comments that restate code syntax.
- Comment blocks that drift from behavior and are not maintained.
## Rule of Thumb
If someone adding a new device could misread a policy or safety boundary, comment it.

View File

@@ -0,0 +1,30 @@
# Community Evidence Intake
## Purpose
Collect hardware and protocol evidence from the community in a clean-room-safe format.
## Submission Requirements
Every report must include:
- Device name
- VID/PID (`0xVVVV:0xPPPP`)
- Firmware version shown by official software/device
- Operation attempted
- Sanitized request/response shape description
- Reproducibility notes (steps, OS, transport mode)
## Prohibited Content
- Raw copied decompiled code.
- Vendor source snippets.
- Binary dumps with proprietary content not required for protocol structure.
## Acceptance Levels
- `intake`: report received, unverified.
- `triaged`: mapped to a PID/operation group and requirement IDs.
- `accepted`: converted into sanitized dossier/spec updates.
## Maintainer Processing
1. Validate report format.
2. Cross-reference PID with `spec/pid_matrix.csv`.
3. Create/update `spec/dossiers/<pid_hex>/*.toml`.
4. Update `spec/evidence_index.csv` and command/pid matrices.
5. Keep device as `candidate-readonly` until full 3-signal promotion gate is met.

View File

@@ -0,0 +1,33 @@
# Device Name Sources
This index lists sanitized naming sources used to build `/Users/brooklyn/data/8bitdo/cleanroom/spec/device_name_catalog.md`.
## Primary dirty-room references
- `DR-1`: Decompiled PID constants table (`VIDPID`) in `/Users/brooklyn/data/8bitdo/decompiled_dll/8BitDo_Ultimate_Software_V2.decompiled.cs` around line 194289.
- `DR-2`: Decompiled device name resolver (`getName()`) in `/Users/brooklyn/data/8bitdo/decompiled_dll/8BitDo_Ultimate_Software_V2.decompiled.cs` around line 12249.
- `DR-3`: Decompiled language map (`LanguageTools`) in `/Users/brooklyn/data/8bitdo/decompiled_dll/8BitDo_Ultimate_Software_V2.decompiled.cs` around line 206277.
## Official 8BitDo web cross-check references
- `WEB-1`: [8BitDo product catalog](https://www.8bitdo.com/#Products)
- `WEB-2`: [Ultimate 2 Wireless Controller](https://www.8bitdo.com/ultimate-2-wireless-controller/)
- `WEB-3`: [Ultimate 2 Bluetooth Controller](https://www.8bitdo.com/ultimate-2-bluetooth-controller/)
- `WEB-4`: [Ultimate 2C Wireless Controller](https://www.8bitdo.com/ultimate-2c-wireless-controller/)
- `WEB-5`: [Ultimate 2C Bluetooth Controller](https://www.8bitdo.com/ultimate-2c-bluetooth-controller/)
- `WEB-6`: [Pro 2 Bluetooth Controller](https://www.8bitdo.com/pro2/)
- `WEB-7`: [Retro 108 Mechanical Keyboard](https://www.8bitdo.com/retro-108-mechanical-keyboard/)
- `WEB-8`: [Retro 87 Mechanical Keyboard - Xbox Edition](https://www.8bitdo.com/retro-87-mechanical-keyboard-xbox/)
- `WEB-9`: [Retro R8 Mouse - Xbox Edition](https://www.8bitdo.com/retro-r8-mouse-xbox/)
- `WEB-10`: [Arcade Controller](https://www.8bitdo.com/arcade-controller/)
- `WEB-11`: [64 Bluetooth Controller](https://www.8bitdo.com/64-controller/)
- `WEB-12`: [Retro Mechanical Keyboard N Edition](https://www.8bitdo.com/retro-mechanical-keyboard/)
## Confidence policy
- `high`: direct match in `DR-2/DR-3` and/or product page exact-name match.
- `medium`: strong inferred match from product family naming with at least one source anchor.
- `low`: internal fallback name because no confident public marketing name was found.
## Low-confidence naming convention
- Canonical wording for low-confidence rows is:
- `Unconfirmed Internal Device (PID_*)`
- `Unconfirmed Variant Name (PID_*)`
- `Unconfirmed Interface Name (PID_*)`

View File

@@ -0,0 +1,60 @@
# Dirty-Room Collection Playbook (Decompiler-First Expansion)
## Goal
Create sanitized, requirement-linked evidence that expands device detect/diagnostics support without contaminating clean-room implementation.
## Scope of This Wave
- Evidence source: decompiler/static-only.
- Target: Wave 2 +12 popularity cohort (plus previously tracked candidate-readonly set).
- Promotion policy: no new `full` promotions in this wave.
- Output artifacts: `spec/dossiers/**`, `spec/evidence_index.csv`, updated `spec/*.csv`, updated `requirements.yaml`.
## Allowed Dirty-Room Inputs
- `/Users/brooklyn/data/8bitdo/decompiled_dll/8BitDo_Ultimate_Software_V2.decompiled.cs`
- `/Users/brooklyn/data/8bitdo/decompiled/*.cs`
- `/Users/brooklyn/data/8bitdo/decompiled_autoupdate/*.cs`
- Existing dirty-room transcript files under `/Users/brooklyn/data/8bitdo/`
## Required Sanitization Rules
- Do not copy raw vendor/decompiled code snippets into clean artifacts.
- Record only sanitized structure-level findings:
- command intent
- request/response byte-shape
- validator expectations
- gating/policy notes
- Use requirement IDs only (`REQ-DR-*`, `REQ-PROM-*`, `REQ-COMM-*`, `REQ-GH-*`).
## Dossier Workflow
1. Pick PID and operation group.
2. Collect static evidence anchors (class/function names and behavior summaries).
3. Derive sanitized command mapping and validation expectations.
4. Write TOML dossier in `spec/dossiers/<pid_hex>/<operation_group>.toml`.
5. Link dossier ID into `spec/command_matrix.csv` (`dossier_id` column).
6. Update `spec/evidence_index.csv`.
7. Ensure each Wave 2 PID has all three required dossier files:
- `core_diag.toml`
- `mode_or_profile_read.toml`
- `firmware_preflight.toml`
## Authoring Approach (No Helper Scripts)
- Dossiers and matrix updates are maintained directly in repository source files.
- `spec/evidence_index.csv` is updated manually with deterministic ordering.
- Validation is performed through normal repository review plus workspace tests.
## Confidence Rules
- `confirmed`: requires static + runtime + hardware confirmation (not achievable in this wave).
- `inferred`: static-only or partial confidence.
- For this wave, new entries should remain `inferred` unless already confirmed historically.
## Promotion Gate
A device can move from `candidate-readonly` to `full` only when all three are true:
1. static evidence complete
2. runtime trace evidence complete
3. hardware read/write/readback complete
## Review Checklist
- Dossier contains required fields from schema.
- Requirement linkage is explicit.
- No raw decompiled text/snippets are present.
- `support_tier` remains `candidate-readonly` for new no-hardware devices.
- Runtime and hardware placeholders are populated with concrete promotion evidence tasks.

View File

@@ -0,0 +1,56 @@
# Dirty-Room Dossier Schema
Each dossier file is TOML and must include these fields.
## Required Fields
- `dossier_id`: stable identifier, e.g. `DOS-5200-CORE`.
- `pid_hex`: target PID in hex (`0xNNNN`).
- `operation_group`: logical grouping (`CoreDiag`, `ModeProfileRead`, `FirmwarePreflight`, etc).
- `command_id`: array of command IDs scoped by this dossier.
- `request_shape`: sanitized request structure summary.
- `response_shape`: sanitized response structure summary.
- `validator_rules`: array of response validation constraints.
- `retry_behavior`: retry and timeout behavior summary.
- `failure_signatures`: array of known failure signatures.
- `evidence_source`: `static` for this wave.
- `confidence`: `inferred` or `confirmed`.
- `requirement_ids`: array of linked requirement IDs.
- `state_machine`: table with `pre_state`, `action`, `post_state`, and `invalid_transitions`.
- `runtime_placeholder`: table with `required` and `evidence_needed`.
- `hardware_placeholder`: table with `required` and `evidence_needed`.
## Optional Fields
- `class_family`: static class-family grouping hints.
- `notes`: additional sanitized context.
## Example
```toml
dossier_id = "DOS-5200-CORE"
pid_hex = "0x5200"
operation_group = "CoreDiag"
command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"]
request_shape = "64-byte HID report, command byte in report[1], PID-specific gating outside payload"
response_shape = "short status header plus optional payload bytes"
validator_rules = ["byte0 == 0x02", "response length >= 4"]
retry_behavior = "retry up to configured max attempts on timeout/malformed response"
failure_signatures = ["timeout", "malformed response", "unsupported command for pid"]
evidence_source = "static"
confidence = "inferred"
requirement_ids = ["REQ-DR-001", "REQ-PROM-001", "REQ-PID-002"]
class_family = "JP/Handshake path"
notes = "candidate-readonly in this wave"
[state_machine]
pre_state = "DeviceConnected"
action = "Run core diagnostics reads"
post_state = "DeviceIdentified"
invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"]
[runtime_placeholder]
required = true
evidence_needed = ["runtime request/response captures", "error signature examples"]
[hardware_placeholder]
required = true
evidence_needed = ["physical read validation", "repeatability checks"]
```

View File

@@ -0,0 +1,50 @@
# Dirty-Room Evidence Backlog
## Purpose
Track future dirty-room evidence work for protocol expansion in a structured way, so new functionality can be translated into sanitized clean-room specs without contaminating implementation code.
## Clean-Room Boundaries
- Dirty-room analysis may use approved evidence sources.
- Clean implementation must consume only sanitized artifacts in `spec/` and approved harness data.
- No raw dirty-room snippets, copied code, or direct decompiled fragments may be carried into clean implementation files.
## Prioritized Backlog
1. Wave-2 candidate-readonly expansion (decompiler-first):
- Popularity +12 PIDs:
- `0x3100`, `0x3105`, `0x2100`, `0x2101`, `0x901a`, `0x6006`
- `0x5203`, `0x5204`, `0x301a`, `0x9028`, `0x3026`, `0x3027`
- Deliverable posture: detect/diag-only (`candidate-readonly`), no firmware transfer/write promotion.
2. Wave-1 candidate-readonly follow-through:
- Primary 14 PIDs:
- `0x6002`, `0x6003`, `0x3010`, `0x3011`, `0x3012`, `0x3013`
- `0x5200`, `0x5201`, `0x203a`, `0x2049`, `0x2028`, `0x202e`
- `0x3004`, `0x3019`
- Stretch PIDs:
- `0x3021`, `0x2039`, `0x2056`, `0x5205`, `0x5206`
- Deliverable posture: stay candidate-readonly until runtime and hardware evidence is accepted.
3. JP108 deeper mapping coverage:
- Expand dedicated key mapping confirmation beyond the current A/B/K1-K8 baseline.
- Confirm feature and voice command behavior with stronger request/response confidence.
4. Ultimate2 advanced paths:
- Expand confidence for advanced slot/config interactions and additional profile behaviors.
- Confirm edge cases for mode transitions and per-slot persistence.
5. Firmware trace confidence:
- Increase confidence for bootloader enter/chunk/commit/exit behavior across supported target variants.
- Capture and sanitize additional failure and recovery traces.
## Required Sanitized Outputs
- Update `spec/protocol_spec.md` for any newly confirmed operation groups or behavior rules.
- Update `spec/requirements.yaml` with new stable requirement IDs.
- Update `spec/command_matrix.csv` and `spec/pid_matrix.csv` as evidence confidence changes.
- Add or refresh sanitized harness fixtures under `harness/golden/` for replay and regression tests.
## Review Checklist Before Clean Implementation
- Sanitized evidence is traceable to requirement IDs.
- Command confidence levels are explicit (`confirmed` vs `inferred`).
- PID capability changes are reflected in matrices.
- No raw-source text is present in clean implementation artifacts.

View File

@@ -0,0 +1,6 @@
[rc_scope]
# Set to true once full 108-key mapping coverage is implemented and validated.
jp108_full_keyboard_mapping = false
# Set to true once expanded Ultimate2 mapping coverage is implemented and validated.
ultimate2_expanded_mapping = false

23
process/wave1_baseline.md Normal file
View File

@@ -0,0 +1,23 @@
# Wave 1 Baseline Snapshot
Generated: 2026-02-28
## Hardware Access Baseline
- Available hardware lines: JP108 + Ultimate2.
- Exact attached PID variants: pending local identify run on connected hardware.
- Temporary lab fixture defaults remain:
- JP108: `0x5209`
- Ultimate2: `0x6012`
## Required Next Verification Step
Run identify flow with connected hardware and confirm fixture PIDs:
1. `cargo test --workspace --test hardware_smoke -- --ignored --exact hardware_smoke_detect_devices`
2. `./scripts/run_hardware_smoke.sh` with `BITDO_REQUIRED_SUITE=108jp`
3. `./scripts/run_hardware_smoke.sh` with `BITDO_REQUIRED_SUITE=ultimate2`
If detected variants differ, update `harness/lab/device_lab.yaml` fixture PIDs immediately.
## Support Baseline
- Existing `full` paths: JP108/Ultimate2 and previously confirmed families.
- New expansion wave devices remain `candidate-readonly` (detect/diag only).
- No new firmware/write enablement for no-hardware targets.

23
process/wave1_results.md Normal file
View File

@@ -0,0 +1,23 @@
# Wave 1 Results (Template)
Generated: 2026-02-28
## Summary
- Primary target PIDs processed: 14
- Stretch target PIDs processed: TBD
- New `full` promotions: 0 (expected in decompiler-only wave)
## Deliverables
- Dossiers created: `spec/dossiers/**`
- Evidence index updated: `spec/evidence_index.csv`
- Matrices updated: `spec/pid_matrix.csv`, `spec/command_matrix.csv`
- Requirements updated: `spec/requirements.yaml`
## Validation
- `cargo test --workspace --all-targets`: pending
- `./scripts/cleanroom_guard.sh`: pending
- Detect/diag targeted tests: pending
## Follow-Up
- Collect runtime traces for candidate-readonly devices.
- Run hardware confirmation on each candidate before promotion to `full`.

32
process/wave2_baseline.md Normal file
View File

@@ -0,0 +1,32 @@
# Wave 2 Baseline (Frozen)
## Snapshot Date
- 2026-03-01
## Pre-Wave Counts (Frozen)
- `spec/pid_matrix.csv` rows: 59
- Support tier counts (pre-wave):
- `full`: 14
- `candidate-readonly`: 15
- `detect-only`: 30
- `spec/command_matrix.csv` rows (pre-wave): 37
## Hardware Reality (Current)
- Available fixtures: JP108 line and Ultimate2 line only.
- Non-owned devices must remain `candidate-readonly` until strict promotion signals are complete.
## Required Checks Baseline
Branch protection for `main` must require:
- `guard`
- `aur-validate`
- `tui-smoke-test`
- `build-macos-arm64`
- `test`
- `hardware-108jp`
- `hardware-ultimate2`
## Promotion Policy
Promotion from `candidate-readonly` to `full` requires all 3 signals:
1. static dossier evidence,
2. runtime sanitized traces,
3. hardware read/write/readback confirmation.

View File

@@ -0,0 +1,22 @@
# Wave 2 PID Scorecard
Legend:
- Static: dossier + matrix linkage complete.
- Runtime: sanitized runtime traces accepted.
- Hardware: read/write/readback confirmed on owned fixture.
- Gate: promotion blocker status.
| PID | Device | Tier | Static | Runtime | Hardware | Gate |
|---|---|---|---|---|---|---|
| `0x3100` | PID_USB_Ultimate | candidate-readonly | yes | no | no | blocked/no_runtime |
| `0x3105` | PID_USB_Ultimate2 | candidate-readonly | yes | no | no | blocked/no_runtime |
| `0x2100` | PID_Xcloud | candidate-readonly | yes | no | no | blocked/no_runtime |
| `0x2101` | PID_Xcloud2 | candidate-readonly | yes | no | no | blocked/no_runtime |
| `0x901a` | PID_ArcadeStick | candidate-readonly | yes | no | no | blocked/no_runtime |
| `0x6006` | PID_Pro2_CY | candidate-readonly | yes | no | no | blocked/no_runtime |
| `0x5203` | PID_NUMPAD | candidate-readonly | yes | no | no | blocked/no_runtime |
| `0x5204` | PID_NUMPADRR | candidate-readonly | yes | no | no | blocked/no_runtime |
| `0x301a` | PID_UltimateBT2C | candidate-readonly | yes | no | no | blocked/no_runtime |
| `0x9028` | PID_N64RR | candidate-readonly | yes | no | no | blocked/no_runtime |
| `0x3026` | PID_XBOXUK | candidate-readonly | yes | no | no | blocked/no_runtime |
| `0x3027` | PID_XBOXUKUSB | candidate-readonly | yes | no | no | blocked/no_runtime |

23
process/wave2_results.md Normal file
View File

@@ -0,0 +1,23 @@
# Wave 2 Results
## Scope
- Expansion cohort: 12 additional popularity-first PIDs.
- Evidence mode: static/decompiler-only.
- Support result: all 12 moved to `candidate-readonly` with detect/diag-only behavior.
## Delivered
1. Manual, source-controlled dossier and matrix updates (no script-generated artifacts).
2. Wave 2 dossier set (`core_diag`, `mode_or_profile_read`, `firmware_preflight`) for each of 12 PIDs.
3. `pid_matrix.csv` updated with Wave 2 candidate-readonly tiering.
4. `command_matrix.csv` updated with Wave 2 dossier-linked rows and explicit promotion gates.
5. `evidence_index.csv` updated from current candidate-readonly set.
6. Governance verification completed for variables, labels, and required checks.
## Deferred
- Runtime trace evidence for Wave 2 cohort.
- Hardware read/write/readback confirmation for Wave 2 cohort.
- Any promotion to `full` for Wave 2 cohort.
## Exit Status
- Wave 2 static-only deliverables: complete.
- Promotion status: blocked pending runtime + hardware signals.

View File

@@ -0,0 +1,38 @@
# Wave 2 Runtime/Hardware Intake (Prepared, Deferred)
## Purpose
Define exactly what sanitized runtime/hardware evidence is needed to move Wave 2 devices beyond static-only dossiers.
## Required Submission Data
Every submission must include:
1. VID/PID in hex.
2. Firmware version.
3. Operation attempted.
4. Sanitized request structure.
5. Sanitized response structure.
6. Reproducibility notes (OS, transport, retries, success/failure rate).
## Sanitization Rules
Allowed content:
- byte-layout summaries,
- command/response shape descriptions,
- validation predicates,
- timing/retry observations.
Forbidden content:
- raw decompiled code snippets,
- copied vendor constants blocks,
- copied source fragments from official binaries/tools.
## Evidence Acceptance Checklist
1. VID/PID and firmware fields are present.
2. Request/response structure is sanitized and technically complete.
3. Failure signatures are mapped to stable categories (`timeout`, `malformed`, `unsupported`, `invalid_signature`).
4. Repro steps are clear enough for independent rerun.
5. No forbidden raw-source content appears.
## Promotion Readiness Mapping
A PID is promotion-eligible only when all are true:
1. Static dossiers complete.
2. Runtime traces accepted from at least 2 independent runs.
3. Hardware read/write/readback validation passes on owned fixture(s).

2383
sdk/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,33 @@
[workspace]
members = [
"crates/bitdo_proto",
"crates/bitdoctl",
"crates/bitdo_app_core",
"crates/bitdo_tui",
"crates/openbitdo",
]
resolver = "2"
[workspace.package]
edition = "2021"
version = "0.1.0"
license = "MIT"
license = "BSD-3-Clause"
[workspace.dependencies]
anyhow = "1.0"
thiserror = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
base64 = "0.22"
ed25519-dalek = { version = "2.1", default-features = false, features = ["std"] }
clap = { version = "4.5", features = ["derive"] }
hex = "0.4"
tokio = { version = "1.48", features = ["rt-multi-thread", "macros", "sync", "time", "fs", "net", "io-util"] }
uuid = { version = "1.18", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["clock", "serde"] }
sha2 = "0.10"
ratatui = "0.29"
crossterm = "0.29"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }

View File

@@ -1,6 +1,10 @@
# OpenBitdo SDK
`bitdo_proto` and `bitdoctl` provide the clean-room protocol core and CLI.
OpenBitdo SDK includes:
- `bitdo_proto`: protocol/transport/session library
- `bitdo_app_core`: shared firmware-first workflow and policy layer
- `bitdo_tui`: Ratatui/Crossterm terminal app
- `openbitdo`: beginner-first launcher (`openbitdo` starts guided TUI)
## Build
```bash
@@ -22,9 +26,97 @@ cargo test --workspace --all-targets
./scripts/run_hardware_smoke.sh
```
## CLI examples
## TUI app examples (`openbitdo`)
```bash
cargo run -p bitdoctl -- --mock list
cargo run -p bitdoctl -- --mock --json --pid 24585 identify
cargo run -p bitdoctl -- --mock --json --pid 24585 diag probe
cargo run -p openbitdo -- --mock
```
## Beginner-first behavior
- launch with no subcommands
- if no device is connected, OpenBitdo starts in a waiting screen with `Refresh`, `Help`, and `Quit`
- if one device is connected, it is auto-selected and ready for action
- choose `Recommended Update` or `Diagnose` from large clickable actions
- for JP108 devices (`0x5209`/`0x520a`), `Recommended Update` enters a dedicated-button wizard:
- edit `A/B/K1-K8`
- backup + apply
- guided button test
- for Ultimate2 devices (`0x6012`/`0x6013`), `Recommended Update` enters a core-profile wizard:
- choose slot (`Slot1/2/3`)
- set mode
- edit RC mapping slots (`A/B/K1-K8`) with known controller-button targets
- view/edit L2/R2 analog values when capability supports writes
- backup + apply
- guided button test
- firmware path defaults to verified recommended download; local file fallback is prompted if unavailable
- update transfer requires one plain-language `I Understand` confirmation
- detect-only PIDs stay read/diagnostic-only with a clear block reason
- mouse support:
- left click for primary actions
- right click on device rows for context menu actions
- scroll wheel to navigate device list/detail panes
- support reports are TOML only
- beginner mode: `failure_only` (default) or `always`
- advanced mode: `failure_only`, `always`, or `off` (with warning)
- advanced mode is toggled from About (`t` or click) and persisted to OS config TOML
- advanced report hotkeys after a failure report exists:
- `c` copy report path
- `o` open report file
- `f` open report folder
- open About from home (`a` key or click `About`) to view:
- app version
- git commit short and full hash
- build date (UTC)
- compile target triple
- runtime OS/arch
- firmware signing-key fingerprint (short with full-view toggle, plus next-key short)
## Packaging
```bash
./scripts/package-linux.sh v0.0.1-rc.1 x86_64
./scripts/package-linux.sh v0.0.1-rc.1 aarch64
./scripts/package-macos.sh v0.0.1-rc.1 arm64 aarch64-apple-darwin
```
Packaging outputs use:
- `openbitdo-<version>-<os>-<arch>.tar.gz`
- `openbitdo-<version>-<os>-<arch>` standalone binary
- `.sha256` checksum file for each artifact
- macOS arm64 additionally emits `.pkg` (unsigned/ad-hoc for RC)
## Release Workflow
- CI checks remain in `.github/workflows/ci.yml`.
- Tag-based release workflow is in `.github/workflows/release.yml`.
- Release tags must originate from `main`.
- `v0.0.1-rc.1` style tags publish GitHub pre-releases.
- Release notes are sourced from `/Users/brooklyn/data/8bitdo/cleanroom/CHANGELOG.md`.
- Package-manager publish runs only after release assets are published.
## Public RC Gate
- No open GitHub issues with label `release-blocker`.
- Scope-completeness gate:
- JP108 RC scope is dedicated mapping only (`A/B/K1-K8`).
- Ultimate2 RC scope is expanded mapping for required fields only.
- Clean-tree requirement from `/Users/brooklyn/data/8bitdo/cleanroom/RC_CHECKLIST.md` must be satisfied before tagging.
## Distribution Prep
- Homebrew formula scaffold: `/Users/brooklyn/data/8bitdo/cleanroom/packaging/homebrew/Formula/openbitdo.rb`
- Homebrew tap sync script (disabled by default): `/Users/brooklyn/data/8bitdo/cleanroom/packaging/homebrew/sync_tap.sh`
- Planned tap repository: `bybrooklyn/homebrew-openbitdo`
- AUR package sources:
- `/Users/brooklyn/data/8bitdo/cleanroom/packaging/aur/openbitdo`
- `/Users/brooklyn/data/8bitdo/cleanroom/packaging/aur/openbitdo-bin`
- Release metadata renderer:
- `/Users/brooklyn/data/8bitdo/cleanroom/packaging/scripts/render_release_metadata.sh`
- AUR publish workflow:
- `/Users/brooklyn/data/8bitdo/cleanroom/.github/workflows/aur-publish.yml`
- gated by `AUR_PUBLISH_ENABLED=1`
- Homebrew publish path:
- `release.yml` renders checksum-pinned formula and runs `sync_tap.sh`
- gated by `HOMEBREW_PUBLISH_ENABLED=1`
## Hardware CI gates
- required:
- `hardware-ultimate2`
- `hardware-108jp`
- gated:
- `hardware-jphandshake` (enabled only when `BITDO_ENABLE_JP_HARDWARE=1`)

View File

@@ -0,0 +1,24 @@
[package]
name = "bitdo_app_core"
version = "0.1.0"
edition = "2021"
license = "BSD-3-Clause"
[dependencies]
anyhow = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
sha2 = { workspace = true }
hex = { workspace = true }
toml = { workspace = true }
reqwest = { workspace = true }
base64 = { workspace = true }
ed25519-dalek = { workspace = true }
bitdo_proto = { path = "../bitdo_proto" }
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "time"] }

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,7 @@
name = "bitdo_proto"
version = "0.1.0"
edition = "2021"
license = "MIT"
build = "build.rs"
license = "BSD-3-Clause"
[features]
default = ["hidapi-backend"]
@@ -16,9 +15,6 @@ serde_json = { workspace = true }
hex = { workspace = true }
hidapi = { version = "2.6", optional = true }
[build-dependencies]
csv = "1.3"
[dev-dependencies]
serde_json = { workspace = true }
hex = { workspace = true }
@@ -39,10 +35,18 @@ path = "../../tests/retry_timeout.rs"
name = "pid_matrix_coverage"
path = "../../tests/pid_matrix_coverage.rs"
[[test]]
name = "command_matrix_coverage"
path = "../../tests/command_matrix_coverage.rs"
[[test]]
name = "capability_gating"
path = "../../tests/capability_gating.rs"
[[test]]
name = "candidate_readonly_gating"
path = "../../tests/candidate_readonly_gating.rs"
[[test]]
name = "profile_serialization"
path = "../../tests/profile_serialization.rs"
@@ -74,3 +78,7 @@ path = "../../tests/error_codes.rs"
[[test]]
name = "diag_probe"
path = "../../tests/diag_probe.rs"
[[test]]
name = "runtime_policy"
path = "../../tests/runtime_policy.rs"

View File

@@ -1,121 +0,0 @@
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
fn main() {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
let spec_dir = manifest_dir.join("../../../spec");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR"));
let pid_csv = spec_dir.join("pid_matrix.csv");
let command_csv = spec_dir.join("command_matrix.csv");
println!("cargo:rerun-if-changed={}", pid_csv.display());
println!("cargo:rerun-if-changed={}", command_csv.display());
generate_pid_registry(&pid_csv, &out_dir.join("generated_pid_registry.rs"));
generate_command_registry(&command_csv, &out_dir.join("generated_command_registry.rs"));
}
fn generate_pid_registry(csv_path: &Path, out_path: &Path) {
let mut rdr = csv::Reader::from_path(csv_path).expect("failed to open pid_matrix.csv");
let mut out = String::new();
out.push_str("pub const PID_REGISTRY: &[crate::registry::PidRegistryRow] = &[\n");
for rec in rdr.records() {
let rec = rec.expect("invalid pid csv record");
let name = rec.get(0).expect("pid_name");
let pid: u16 = rec
.get(1)
.expect("pid_decimal")
.parse()
.expect("invalid pid decimal");
let support_level = match rec.get(5).expect("support_level") {
"full" => "crate::types::SupportLevel::Full",
"detect-only" => "crate::types::SupportLevel::DetectOnly",
other => panic!("unknown support_level {other}"),
};
let protocol_family = match rec.get(6).expect("protocol_family") {
"Standard64" => "crate::types::ProtocolFamily::Standard64",
"JpHandshake" => "crate::types::ProtocolFamily::JpHandshake",
"DInput" => "crate::types::ProtocolFamily::DInput",
"DS4Boot" => "crate::types::ProtocolFamily::DS4Boot",
"Unknown" => "crate::types::ProtocolFamily::Unknown",
other => panic!("unknown protocol_family {other}"),
};
out.push_str(&format!(
" crate::registry::PidRegistryRow {{ name: \"{name}\", pid: {pid}, support_level: {support_level}, protocol_family: {protocol_family} }},\n"
));
}
out.push_str("]\n;");
fs::write(out_path, out).expect("failed writing generated_pid_registry.rs");
}
fn generate_command_registry(csv_path: &Path, out_path: &Path) {
let mut rdr = csv::Reader::from_path(csv_path).expect("failed to open command_matrix.csv");
let mut out = String::new();
out.push_str("pub const COMMAND_REGISTRY: &[crate::registry::CommandRegistryRow] = &[\n");
for rec in rdr.records() {
let rec = rec.expect("invalid command csv record");
let id = rec.get(0).expect("command_id");
let safety_class = match rec.get(1).expect("safety_class") {
"SafeRead" => "crate::types::SafetyClass::SafeRead",
"SafeWrite" => "crate::types::SafetyClass::SafeWrite",
"UnsafeBoot" => "crate::types::SafetyClass::UnsafeBoot",
"UnsafeFirmware" => "crate::types::SafetyClass::UnsafeFirmware",
other => panic!("unknown safety_class {other}"),
};
let confidence = match rec.get(2).expect("confidence") {
"confirmed" => "crate::types::CommandConfidence::Confirmed",
"inferred" => "crate::types::CommandConfidence::Inferred",
other => panic!("unknown confidence {other}"),
};
let experimental_default = rec
.get(3)
.expect("experimental_default")
.parse::<bool>()
.expect("invalid experimental_default");
let report_id = parse_u8(rec.get(4).expect("report_id"));
let request_hex = rec.get(6).expect("request_hex");
let request = hex_to_bytes(request_hex);
let expected_response = rec.get(7).expect("expected_response");
out.push_str(&format!(
" crate::registry::CommandRegistryRow {{ id: crate::command::CommandId::{id}, safety_class: {safety_class}, confidence: {confidence}, experimental_default: {experimental_default}, report_id: {report_id}, request: &{request:?}, expected_response: \"{expected_response}\" }},\n"
));
}
out.push_str("]\n;");
fs::write(out_path, out).expect("failed writing generated_command_registry.rs");
}
fn parse_u8(value: &str) -> u8 {
if let Some(stripped) = value.strip_prefix("0x") {
u8::from_str_radix(stripped, 16).expect("invalid hex u8")
} else {
value.parse::<u8>().expect("invalid u8")
}
}
fn hex_to_bytes(hex: &str) -> Vec<u8> {
let hex = hex.trim();
if hex.len() % 2 != 0 {
panic!("hex length must be even: {hex}");
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
let raw = hex.as_bytes();
for i in (0..raw.len()).step_by(2) {
let hi = (raw[i] as char)
.to_digit(16)
.unwrap_or_else(|| panic!("invalid hex: {hex}"));
let lo = (raw[i + 1] as char)
.to_digit(16)
.unwrap_or_else(|| panic!("invalid hex: {hex}"));
bytes.push(((hi << 4) | lo) as u8);
}
bytes
}

View File

@@ -20,10 +20,30 @@ pub enum CommandId {
ExitBootloader,
FirmwareChunk,
FirmwareCommit,
Jp108ReadDedicatedMappings,
Jp108WriteDedicatedMapping,
Jp108ReadFeatureFlags,
Jp108WriteFeatureFlags,
Jp108ReadVoice,
Jp108WriteVoice,
U2GetCurrentSlot,
U2ReadConfigSlot,
U2WriteConfigSlot,
U2ReadButtonMap,
U2WriteButtonMap,
U2SetMode,
Jp108EnterBootloader,
Jp108FirmwareChunk,
Jp108FirmwareCommit,
Jp108ExitBootloader,
U2EnterBootloader,
U2FirmwareChunk,
U2FirmwareCommit,
U2ExitBootloader,
}
impl CommandId {
pub const ALL: [CommandId; 17] = [
pub const ALL: [CommandId; 37] = [
CommandId::GetPid,
CommandId::GetReportRevision,
CommandId::GetMode,
@@ -41,6 +61,26 @@ impl CommandId {
CommandId::ExitBootloader,
CommandId::FirmwareChunk,
CommandId::FirmwareCommit,
CommandId::Jp108ReadDedicatedMappings,
CommandId::Jp108WriteDedicatedMapping,
CommandId::Jp108ReadFeatureFlags,
CommandId::Jp108WriteFeatureFlags,
CommandId::Jp108ReadVoice,
CommandId::Jp108WriteVoice,
CommandId::U2GetCurrentSlot,
CommandId::U2ReadConfigSlot,
CommandId::U2WriteConfigSlot,
CommandId::U2ReadButtonMap,
CommandId::U2WriteButtonMap,
CommandId::U2SetMode,
CommandId::Jp108EnterBootloader,
CommandId::Jp108FirmwareChunk,
CommandId::Jp108FirmwareCommit,
CommandId::Jp108ExitBootloader,
CommandId::U2EnterBootloader,
CommandId::U2FirmwareChunk,
CommandId::U2FirmwareCommit,
CommandId::U2ExitBootloader,
];
pub fn all() -> &'static [CommandId] {

View File

@@ -0,0 +1,82 @@
// Hardcoded command declaration table.
//
// Policy model:
// - Confirmed commands can be enabled by default.
// - Inferred safe reads can run only behind experimental/advanced mode.
// - Inferred writes and unsafe paths stay blocked until confirmed.
pub const COMMAND_REGISTRY: &[crate::registry::CommandRegistryRow] = &[
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetReportRevision, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x04;byte5=0x01", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetModeAlt, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetSuperButton, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::SetModeDInput, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 0, 81, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Idle, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Version, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte1=0x22", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::ReadProfile, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::WriteProfile, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 7, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::EnterBootloaderA, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[5, 0, 80, 1, 0, 0], expected_response: "none", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::EnterBootloaderB, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[0, 81, 0, 0, 0, 0], expected_response: "none", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::EnterBootloaderC, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[0, 80, 0, 0, 0], expected_response: "none", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::ExitBootloader, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[5, 0, 81, 1, 0, 0], expected_response: "none", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::FirmwareChunk, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::FirmwareCommit, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108ReadDedicatedMappings, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 48, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108WriteDedicatedMapping, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 49, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108ReadFeatureFlags, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 50, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108WriteFeatureFlags, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 51, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108ReadVoice, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 52, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108WriteVoice, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 53, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2GetCurrentSlot, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 64, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2ReadConfigSlot, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 65, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2WriteConfigSlot, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 66, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2ReadButtonMap, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 67, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2WriteButtonMap, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 68, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2SetMode, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 69, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108EnterBootloader, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[5, 0, 80, 1, 0, 0], expected_response: "none", applies_to: &[21001, 21002], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108FirmwareChunk, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 96, 16, 32, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[21001, 21002], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108FirmwareCommit, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 96, 17, 32, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[21001, 21002], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108ExitBootloader, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[5, 0, 81, 1, 0, 0], expected_response: "none", applies_to: &[21001, 21002], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2EnterBootloader, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[5, 0, 80, 1, 0, 0], expected_response: "none", applies_to: &[24594, 24595], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2FirmwareChunk, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 96, 16, 96, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[24594, 24595], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2FirmwareCommit, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 96, 17, 96, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[24594, 24595], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2ExitBootloader, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[5, 0, 81, 1, 0, 0], expected_response: "none", applies_to: &[24594, 24595], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[12544], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[12544], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[12544], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[12549], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[12549], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[12549], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[8448], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[8448], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[8448], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[8449], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[8449], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[8449], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[36890], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[36890], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[36890], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[24582], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[24582], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[24582], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[20995], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[20995], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[20995], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[20996], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[20996], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[20996], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[12314], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[12314], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[12314], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[36904], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[36904], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[36904], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[12326], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[12326], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[12326], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[12327], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[12327], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[12327], operation_group: "FirmwarePreflight" },
]
;

View File

@@ -1,5 +1,3 @@
#![cfg(feature = "hidapi-backend")]
use crate::error::{BitdoError, Result};
use crate::transport::Transport;
use crate::types::VidPid;

View File

@@ -21,10 +21,11 @@ pub use registry::{
};
pub use session::{
validate_response, CommandExecutionReport, DeviceSession, DiagCommandStatus, DiagProbeResult,
FirmwareTransferReport, IdentifyResult, ModeState, RetryPolicy, SessionConfig, TimeoutProfile,
DiagSeverity, FirmwareTransferReport, IdentifyResult, ModeState, RetryPolicy, SessionConfig,
TimeoutProfile,
};
pub use transport::{MockTransport, Transport};
pub use types::{
CommandConfidence, DeviceProfile, PidCapability, ProtocolFamily, SafetyClass, SupportEvidence,
SupportLevel, VidPid,
CommandConfidence, CommandRuntimePolicy, DeviceProfile, EvidenceConfidence, PidCapability,
ProtocolFamily, SafetyClass, SupportEvidence, SupportLevel, SupportTier, VidPid,
};

View File

@@ -0,0 +1,68 @@
// Hardcoded PID registry.
//
// Design note:
// - Every known PID from sanitized evidence/spec is declared here.
// - Declaration breadth is broad, but runtime execution remains conservative
// through session/runtime policy gates in `session.rs`.
// - PID values are unique here by policy; legacy aliases are documented in
// `spec/alias_index.md` instead of duplicated in runtime tables.
pub const PID_REGISTRY: &[crate::registry::PidRegistryRow] = &[
crate::registry::PidRegistryRow { name: "PID_None", pid: 0, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Unknown },
crate::registry::PidRegistryRow { name: "PID_IDLE", pid: 12553, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_SN30Plus", pid: 24578, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_USB_Ultimate", pid: 12544, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_USB_Ultimate2", pid: 12549, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_USB_UltimateClasses", pid: 12548, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Xcloud", pid: 8448, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Xcloud2", pid: 8449, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_ArcadeStick", pid: 36890, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Pro2", pid: 24579, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Pro2_CY", pid: 24582, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Pro2_Wired", pid: 12304, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Ultimate_PC", pid: 12305, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Ultimate2_4", pid: 12306, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Ultimate2_4RR", pid: 12307, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_UltimateBT", pid: 24583, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_UltimateBTRR", pid: 12550, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_JP", pid: 20992, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_JPUSB", pid: 20993, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_NUMPAD", pid: 20995, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_NUMPADRR", pid: 20996, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_QINGCHUN2", pid: 12554, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_QINGCHUN2RR", pid: 12316, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Xinput", pid: 12555, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Pro3", pid: 24585, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Pro3USB", pid: 24586, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Pro3DOCK", pid: 24589, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_108JP", pid: 21001, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_108JPUSB", pid: 21002, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_XBOXJP", pid: 8232, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_XBOXJPUSB", pid: 8238, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_NGCDIY", pid: 22352, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_NGCRR", pid: 36906, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Ultimate2", pid: 24594, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Ultimate2RR", pid: 24595, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_UltimateBT2", pid: 24591, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_UltimateBT2RR", pid: 24593, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Mouse", pid: 20997, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_MouseRR", pid: 20998, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_SaturnRR", pid: 36907, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_UltimateBT2C", pid: 12314, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Lashen", pid: 12318, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_HitBox", pid: 24587, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_HitBoxRR", pid: 24588, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_N64BT", pid: 12313, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_N64", pid: 12292, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_N64RR", pid: 36904, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_XBOXUK", pid: 12326, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_XBOXUKUSB", pid: 12327, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_LashenX", pid: 8203, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_68JP", pid: 8250, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_68JPUSB", pid: 8265, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_N64JoySticks", pid: 12321, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_DoubleSuper", pid: 8254, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Cube2RR", pid: 8278, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Cube2", pid: 8249, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_ASLGJP", pid: 8282, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
]
;

View File

@@ -1,14 +1,17 @@
use crate::command::CommandId;
use crate::types::{
CommandConfidence, DeviceProfile, PidCapability, ProtocolFamily, SafetyClass, SupportEvidence,
SupportLevel, VidPid,
CommandConfidence, CommandRuntimePolicy, DeviceProfile, EvidenceConfidence, PidCapability,
ProtocolFamily, SafetyClass, SupportEvidence, SupportLevel, SupportTier, VidPid,
};
use std::collections::HashSet;
use std::sync::OnceLock;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PidRegistryRow {
pub name: &'static str,
pub pid: u16,
pub support_level: SupportLevel,
pub support_tier: SupportTier,
pub protocol_family: ProtocolFamily,
}
@@ -21,12 +24,43 @@ pub struct CommandRegistryRow {
pub report_id: u8,
pub request: &'static [u8],
pub expected_response: &'static str,
pub applies_to: &'static [u16],
pub operation_group: &'static str,
}
include!(concat!(env!("OUT_DIR"), "/generated_pid_registry.rs"));
include!(concat!(env!("OUT_DIR"), "/generated_command_registry.rs"));
// Registry data is intentionally hardcoded in source files so support coverage
// is explicit in Rust code and does not depend on build-time CSV generation.
include!("pid_registry_table.rs");
include!("command_registry_table.rs");
impl CommandRegistryRow {
/// Convert evidence confidence into a stable reporting enum.
pub fn evidence_confidence(&self) -> EvidenceConfidence {
match self.confidence {
CommandConfidence::Confirmed => EvidenceConfidence::Confirmed,
CommandConfidence::Inferred => EvidenceConfidence::Inferred,
}
}
/// Runtime policy used by the session gate checker.
///
/// Policy rationale:
/// - Confirmed paths are enabled by default.
/// - Inferred safe reads can run only when experimental mode is enabled.
/// - Inferred write/unsafe paths stay blocked until explicit confirmation.
pub fn runtime_policy(&self) -> CommandRuntimePolicy {
match (self.confidence, self.safety_class) {
(CommandConfidence::Confirmed, _) => CommandRuntimePolicy::EnabledDefault,
(CommandConfidence::Inferred, SafetyClass::SafeRead) => {
CommandRuntimePolicy::ExperimentalGate
}
(CommandConfidence::Inferred, _) => CommandRuntimePolicy::BlockedUntilConfirmed,
}
}
}
pub fn pid_registry() -> &'static [PidRegistryRow] {
ensure_unique_pid_rows();
PID_REGISTRY
}
@@ -35,27 +69,133 @@ pub fn command_registry() -> &'static [CommandRegistryRow] {
}
pub fn find_pid(pid: u16) -> Option<&'static PidRegistryRow> {
PID_REGISTRY.iter().find(|row| row.pid == pid)
pid_registry().iter().find(|row| row.pid == pid)
}
pub fn find_command(id: CommandId) -> Option<&'static CommandRegistryRow> {
COMMAND_REGISTRY.iter().find(|row| row.id == id)
}
pub fn command_applies_to_pid(row: &CommandRegistryRow, pid: u16) -> bool {
row.applies_to.is_empty() || row.applies_to.contains(&pid)
}
pub fn default_capability_for(
support_level: SupportLevel,
_protocol_family: ProtocolFamily,
pid: u16,
support_tier: SupportTier,
protocol_family: ProtocolFamily,
) -> PidCapability {
match support_level {
SupportLevel::Full => PidCapability::full(),
SupportLevel::DetectOnly => PidCapability::identify_only(),
if support_tier == SupportTier::DetectOnly {
return PidCapability::identify_only();
}
const STANDARD_CANDIDATE_READ_DIAG_PIDS: &[u16] = &[
0x6002, 0x6003, 0x3010, 0x3011, 0x3012, 0x3013, 0x3004, 0x3019, 0x3100, 0x3105, 0x2100,
0x2101, 0x901a, 0x6006, 0x5203, 0x5204, 0x301a, 0x9028, 0x3026, 0x3027,
];
const JP_CANDIDATE_DIAG_PIDS: &[u16] = &[0x5200, 0x5201, 0x203a, 0x2049, 0x2028, 0x202e];
match (support_tier, pid) {
(SupportTier::CandidateReadOnly, 0x6002)
| (SupportTier::CandidateReadOnly, 0x6003)
| (SupportTier::CandidateReadOnly, 0x3010)
| (SupportTier::CandidateReadOnly, 0x3011)
| (SupportTier::CandidateReadOnly, 0x3012)
| (SupportTier::CandidateReadOnly, 0x3013)
| (SupportTier::CandidateReadOnly, 0x3004)
| (SupportTier::CandidateReadOnly, 0x3019)
| (SupportTier::CandidateReadOnly, 0x3100)
| (SupportTier::CandidateReadOnly, 0x3105)
| (SupportTier::CandidateReadOnly, 0x2100)
| (SupportTier::CandidateReadOnly, 0x2101)
| (SupportTier::CandidateReadOnly, 0x901a)
| (SupportTier::CandidateReadOnly, 0x6006)
| (SupportTier::CandidateReadOnly, 0x5203)
| (SupportTier::CandidateReadOnly, 0x5204)
| (SupportTier::CandidateReadOnly, 0x301a)
| (SupportTier::CandidateReadOnly, 0x9028)
| (SupportTier::CandidateReadOnly, 0x3026)
| (SupportTier::CandidateReadOnly, 0x3027) => PidCapability {
supports_mode: true,
supports_profile_rw: true,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
},
(SupportTier::CandidateReadOnly, 0x5200)
| (SupportTier::CandidateReadOnly, 0x5201)
| (SupportTier::CandidateReadOnly, 0x203a)
| (SupportTier::CandidateReadOnly, 0x2049)
| (SupportTier::CandidateReadOnly, 0x2028)
| (SupportTier::CandidateReadOnly, 0x202e) => PidCapability {
supports_mode: false,
supports_profile_rw: false,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
},
(SupportTier::CandidateReadOnly, _) if STANDARD_CANDIDATE_READ_DIAG_PIDS.contains(&pid) => {
PidCapability {
supports_mode: true,
supports_profile_rw: true,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
}
}
(SupportTier::CandidateReadOnly, _) if JP_CANDIDATE_DIAG_PIDS.contains(&pid) => {
PidCapability {
supports_mode: false,
supports_profile_rw: false,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
}
}
(_, 0x5209) | (_, 0x520a) => PidCapability {
supports_mode: false,
supports_profile_rw: false,
supports_boot: true,
supports_firmware: true,
supports_jp108_dedicated_map: true,
supports_u2_slot_config: false,
supports_u2_button_map: false,
},
(_, 0x6012) | (_, 0x6013) => PidCapability {
supports_mode: true,
supports_profile_rw: true,
supports_boot: true,
supports_firmware: true,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: true,
supports_u2_button_map: true,
},
_ => {
let mut cap = PidCapability::full();
if protocol_family == ProtocolFamily::JpHandshake {
cap.supports_mode = false;
cap.supports_profile_rw = false;
}
cap.supports_jp108_dedicated_map = false;
cap.supports_u2_slot_config = false;
cap.supports_u2_button_map = false;
cap
}
}
}
pub fn default_evidence_for(support_level: SupportLevel) -> SupportEvidence {
match support_level {
SupportLevel::Full => SupportEvidence::Confirmed,
SupportLevel::DetectOnly => SupportEvidence::Inferred,
pub fn default_evidence_for(support_tier: SupportTier) -> SupportEvidence {
match support_tier {
SupportTier::Full => SupportEvidence::Confirmed,
SupportTier::CandidateReadOnly | SupportTier::DetectOnly => SupportEvidence::Inferred,
}
}
@@ -65,18 +205,35 @@ pub fn device_profile_for(vid_pid: VidPid) -> DeviceProfile {
vid_pid,
name: row.name.to_owned(),
support_level: row.support_level,
support_tier: row.support_tier,
protocol_family: row.protocol_family,
capability: default_capability_for(row.support_level, row.protocol_family),
evidence: default_evidence_for(row.support_level),
capability: default_capability_for(row.pid, row.support_tier, row.protocol_family),
evidence: default_evidence_for(row.support_tier),
}
} else {
DeviceProfile {
vid_pid,
name: "PID_UNKNOWN".to_owned(),
support_level: SupportLevel::DetectOnly,
support_tier: SupportTier::DetectOnly,
protocol_family: ProtocolFamily::Unknown,
capability: PidCapability::identify_only(),
evidence: SupportEvidence::Untested,
}
}
}
fn ensure_unique_pid_rows() {
static CHECK: OnceLock<()> = OnceLock::new();
CHECK.get_or_init(|| {
let mut seen = HashSet::new();
for row in PID_REGISTRY {
assert!(
seen.insert(row.pid),
"duplicate pid in runtime registry: {:#06x} ({})",
row.pid,
row.name
);
}
});
}

View File

@@ -2,11 +2,13 @@ use crate::command::CommandId;
use crate::error::{BitdoError, BitdoErrorCode, Result};
use crate::frame::{CommandFrame, ResponseFrame, ResponseStatus};
use crate::profile::ProfileBlob;
use crate::registry::{device_profile_for, find_command, find_pid, CommandRegistryRow};
use crate::registry::{
command_applies_to_pid, device_profile_for, find_command, find_pid, CommandRegistryRow,
};
use crate::transport::Transport;
use crate::types::{
CommandConfidence, DeviceProfile, PidCapability, ProtocolFamily, SafetyClass, SupportEvidence,
SupportLevel, VidPid,
CommandRuntimePolicy, DeviceProfile, EvidenceConfidence, PidCapability, ProtocolFamily,
SafetyClass, SupportEvidence, SupportLevel, SupportTier, VidPid,
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
@@ -83,15 +85,26 @@ pub struct CommandExecutionReport {
pub struct DiagCommandStatus {
pub command: CommandId,
pub ok: bool,
pub confidence: EvidenceConfidence,
pub is_experimental: bool,
pub severity: DiagSeverity,
pub error_code: Option<BitdoErrorCode>,
pub detail: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum DiagSeverity {
Ok,
Warning,
NeedsAttention,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DiagProbeResult {
pub target: VidPid,
pub profile_name: String,
pub support_level: SupportLevel,
pub support_tier: SupportTier,
pub protocol_family: ProtocolFamily,
pub capability: PidCapability,
pub evidence: SupportEvidence,
@@ -104,6 +117,7 @@ pub struct IdentifyResult {
pub target: VidPid,
pub profile_name: String,
pub support_level: SupportLevel,
pub support_tier: SupportTier,
pub protocol_family: ProtocolFamily,
pub capability: PidCapability,
pub evidence: SupportEvidence,
@@ -187,6 +201,7 @@ impl<T: Transport> DeviceSession<T> {
target: self.target,
profile_name: profile.name,
support_level: profile.support_level,
support_tier: profile.support_tier,
protocol_family: profile.protocol_family,
capability: profile.capability,
evidence: profile.evidence,
@@ -195,33 +210,66 @@ impl<T: Transport> DeviceSession<T> {
}
pub fn diag_probe(&mut self) -> DiagProbeResult {
let checks = [
let target_pid = self.target.pid;
let checks_to_run = [
CommandId::GetPid,
CommandId::GetReportRevision,
CommandId::GetMode,
CommandId::GetControllerVersion,
// Inferred safe reads are intentionally included in diagnostics so
// users always see signal quality, but results are labeled
// experimental and only strict safety conditions escalate.
CommandId::GetSuperButton,
CommandId::ReadProfile,
]
.iter()
.map(|cmd| match self.send_command(*cmd, None) {
Ok(_) => DiagCommandStatus {
command: *cmd,
ok: true,
error_code: None,
detail: "ok".to_owned(),
},
Err(err) => DiagCommandStatus {
command: *cmd,
ok: false,
error_code: Some(err.code()),
detail: err.to_string(),
},
.filter_map(|cmd| {
let row = find_command(*cmd)?;
if row.safety_class != SafetyClass::SafeRead {
return None;
}
if !command_applies_to_pid(row, target_pid) {
return None;
}
Some((*cmd, row.runtime_policy(), row.evidence_confidence()))
})
.collect::<Vec<_>>();
let mut checks = Vec::with_capacity(checks_to_run.len());
for (cmd, runtime_policy, confidence) in checks_to_run {
match self.send_command(cmd, None) {
Ok(_) => checks.push(DiagCommandStatus {
command: cmd,
ok: true,
confidence,
is_experimental: runtime_policy == CommandRuntimePolicy::ExperimentalGate,
severity: DiagSeverity::Ok,
error_code: None,
detail: "ok".to_owned(),
}),
Err(err) => checks.push(DiagCommandStatus {
command: cmd,
ok: false,
confidence,
is_experimental: runtime_policy == CommandRuntimePolicy::ExperimentalGate,
severity: classify_diag_failure(
cmd,
runtime_policy,
confidence,
err.code(),
self.target.pid,
),
error_code: Some(err.code()),
detail: err.to_string(),
}),
}
}
DiagProbeResult {
target: self.target,
profile_name: self.profile.name.clone(),
support_level: self.profile.support_level,
support_tier: self.profile.support_tier,
protocol_family: self.profile.protocol_family,
capability: self.profile.capability,
evidence: self.profile.evidence,
@@ -290,6 +338,116 @@ impl<T: Transport> DeviceSession<T> {
Ok(())
}
pub fn jp108_read_dedicated_mappings(&mut self) -> Result<Vec<(u8, u16)>> {
let resp = self.send_command(CommandId::Jp108ReadDedicatedMappings, None)?;
Ok(parse_indexed_u16_table(&resp.raw, 10))
}
pub fn jp108_write_dedicated_mapping(
&mut self,
index: u8,
target_hid_usage: u16,
) -> Result<()> {
let row = self.ensure_command_allowed(CommandId::Jp108WriteDedicatedMapping)?;
let mut payload = row.request.to_vec();
if payload.len() < 7 {
return Err(BitdoError::InvalidInput(
"Jp108WriteDedicatedMapping payload shorter than expected".to_owned(),
));
}
payload[4] = index;
let usage = target_hid_usage.to_le_bytes();
payload[5] = usage[0];
payload[6] = usage[1];
self.send_row(row, Some(&payload))?;
Ok(())
}
pub fn u2_get_current_slot(&mut self) -> Result<u8> {
let resp = self.send_command(CommandId::U2GetCurrentSlot, None)?;
Ok(resp.parsed_fields.get("slot").copied().unwrap_or(0) as u8)
}
pub fn u2_read_config_slot(&mut self, slot: u8) -> Result<Vec<u8>> {
let row = self.ensure_command_allowed(CommandId::U2ReadConfigSlot)?;
let mut payload = row.request.to_vec();
if payload.len() > 4 {
payload[4] = slot;
}
let resp = self.send_row(row, Some(&payload))?;
Ok(resp.raw)
}
pub fn u2_write_config_slot(&mut self, slot: u8, config_blob: &[u8]) -> Result<()> {
let row = self.ensure_command_allowed(CommandId::U2WriteConfigSlot)?;
let mut payload = row.request.to_vec();
if payload.len() < 8 {
return Err(BitdoError::InvalidInput(
"U2WriteConfigSlot payload shorter than expected".to_owned(),
));
}
payload[4] = slot;
let copy_len = config_blob.len().min(payload.len().saturating_sub(8));
if copy_len > 0 {
payload[8..8 + copy_len].copy_from_slice(&config_blob[..copy_len]);
}
self.send_row(row, Some(&payload))?;
Ok(())
}
pub fn u2_read_button_map(&mut self, slot: u8) -> Result<Vec<(u8, u16)>> {
let row = self.ensure_command_allowed(CommandId::U2ReadButtonMap)?;
let mut payload = row.request.to_vec();
if payload.len() > 4 {
payload[4] = slot;
}
let resp = self.send_row(row, Some(&payload))?;
Ok(parse_indexed_u16_table(&resp.raw, 17))
}
pub fn u2_write_button_map(&mut self, slot: u8, mappings: &[(u8, u16)]) -> Result<()> {
let row = self.ensure_command_allowed(CommandId::U2WriteButtonMap)?;
let mut payload = row.request.to_vec();
if payload.len() < 8 {
return Err(BitdoError::InvalidInput(
"U2WriteButtonMap payload shorter than expected".to_owned(),
));
}
payload[4] = slot;
for (index, usage) in mappings {
let pos = 8usize.saturating_add((*index as usize).saturating_mul(2));
if pos + 1 < payload.len() {
let bytes = usage.to_le_bytes();
payload[pos] = bytes[0];
payload[pos + 1] = bytes[1];
}
}
self.send_row(row, Some(&payload))?;
Ok(())
}
pub fn u2_set_mode(&mut self, mode: u8) -> Result<ModeState> {
let row = self.ensure_command_allowed(CommandId::U2SetMode)?;
let mut payload = row.request.to_vec();
if payload.len() < 5 {
return Err(BitdoError::InvalidInput(
"U2SetMode payload shorter than expected".to_owned(),
));
}
payload[4] = mode;
self.send_row(row, Some(&payload))?;
Ok(ModeState {
mode,
source: "U2SetMode".to_owned(),
})
}
pub fn enter_bootloader(&mut self) -> Result<()> {
self.send_command(CommandId::EnterBootloaderA, None)?;
self.send_command(CommandId::EnterBootloaderB, None)?;
@@ -529,12 +687,28 @@ impl<T: Transport> DeviceSession<T> {
fn ensure_command_allowed(&self, command: CommandId) -> Result<&'static CommandRegistryRow> {
let row = find_command(command).ok_or(BitdoError::UnknownCommand(command))?;
if row.confidence == CommandConfidence::Inferred && !self.config.experimental {
return Err(BitdoError::ExperimentalRequired { command });
// Gate 1: confidence/runtime policy.
// We intentionally keep inferred write/unsafe paths non-executable until
// they are upgraded to confirmed evidence.
match row.runtime_policy() {
CommandRuntimePolicy::EnabledDefault => {}
CommandRuntimePolicy::ExperimentalGate => {
if !self.config.experimental {
return Err(BitdoError::ExperimentalRequired { command });
}
}
CommandRuntimePolicy::BlockedUntilConfirmed => {
return Err(BitdoError::UnsupportedForPid {
command,
pid: self.target.pid,
});
}
}
// Gate 2: PID/family/capability applicability.
if !is_command_allowed_by_family(self.profile.protocol_family, command)
|| !is_command_allowed_by_capability(self.profile.capability, command)
|| !command_applies_to_pid(row, self.target.pid)
{
return Err(BitdoError::UnsupportedForPid {
command,
@@ -542,8 +716,19 @@ impl<T: Transport> DeviceSession<T> {
});
}
// Gate 3: support-tier restrictions.
if self.profile.support_tier == SupportTier::CandidateReadOnly
&& !is_command_allowed_for_candidate_pid(self.target.pid, command, row.safety_class)
{
return Err(BitdoError::UnsupportedForPid {
command,
pid: self.target.pid,
});
}
// Gate 4: explicit unsafe confirmation requirements.
if row.safety_class.is_unsafe() {
if self.profile.support_level != SupportLevel::Full {
if self.profile.support_tier != SupportTier::Full {
return Err(BitdoError::UnsupportedForPid {
command,
pid: self.target.pid,
@@ -555,7 +740,7 @@ impl<T: Transport> DeviceSession<T> {
}
if row.safety_class == SafetyClass::SafeWrite
&& self.profile.support_level == SupportLevel::DetectOnly
&& self.profile.support_tier != SupportTier::Full
{
return Err(BitdoError::UnsupportedForPid {
command,
@@ -567,6 +752,83 @@ impl<T: Transport> DeviceSession<T> {
}
}
fn classify_diag_failure(
command: CommandId,
runtime_policy: CommandRuntimePolicy,
confidence: EvidenceConfidence,
code: BitdoErrorCode,
pid: u16,
) -> DiagSeverity {
if runtime_policy != CommandRuntimePolicy::ExperimentalGate
|| confidence != EvidenceConfidence::Inferred
{
return DiagSeverity::Warning;
}
// Escalation is intentionally narrow for inferred checks:
// - identity mismatch / impossible transitions
// - command/schema applicability mismatch
// - precondition/capability mismatches implied by unsupported errors
let identity_or_transition_issue = matches!(
(command, code),
(CommandId::GetPid, BitdoErrorCode::InvalidResponse)
| (CommandId::GetPid, BitdoErrorCode::MalformedResponse)
| (CommandId::GetMode, BitdoErrorCode::InvalidResponse)
| (CommandId::GetModeAlt, BitdoErrorCode::InvalidResponse)
| (CommandId::ReadProfile, BitdoErrorCode::InvalidResponse)
| (
CommandId::GetControllerVersion,
BitdoErrorCode::InvalidResponse
)
| (CommandId::Version, BitdoErrorCode::InvalidResponse)
);
if identity_or_transition_issue {
return DiagSeverity::NeedsAttention;
}
if code == BitdoErrorCode::UnsupportedForPid
&& find_command(command)
.map(|row| command_applies_to_pid(row, pid))
.unwrap_or(false)
{
return DiagSeverity::NeedsAttention;
}
DiagSeverity::Warning
}
fn is_command_allowed_for_candidate_pid(pid: u16, command: CommandId, safety: SafetyClass) -> bool {
if safety != SafetyClass::SafeRead {
return false;
}
const BASE_DIAG_READS: &[CommandId] = &[
CommandId::GetPid,
CommandId::GetReportRevision,
CommandId::GetControllerVersion,
CommandId::Version,
CommandId::Idle,
];
const STANDARD_CANDIDATE_PIDS: &[u16] = &[
0x6002, 0x6003, 0x3010, 0x3011, 0x3012, 0x3013, 0x3004, 0x3019, 0x3100, 0x3105, 0x2100,
0x2101, 0x901a, 0x6006, 0x5203, 0x5204, 0x301a, 0x9028, 0x3026, 0x3027,
];
const JP_CANDIDATE_PIDS: &[u16] = &[0x5200, 0x5201, 0x203a, 0x2049, 0x2028, 0x202e];
if BASE_DIAG_READS.contains(&command) {
return STANDARD_CANDIDATE_PIDS.contains(&pid) || JP_CANDIDATE_PIDS.contains(&pid);
}
if STANDARD_CANDIDATE_PIDS.contains(&pid) {
return matches!(
command,
CommandId::GetMode | CommandId::GetModeAlt | CommandId::ReadProfile
);
}
false
}
fn is_command_allowed_by_capability(cap: PidCapability, command: CommandId) -> bool {
match command {
CommandId::GetPid
@@ -580,8 +842,29 @@ fn is_command_allowed_by_capability(cap: PidCapability, command: CommandId) -> b
CommandId::EnterBootloaderA
| CommandId::EnterBootloaderB
| CommandId::EnterBootloaderC
| CommandId::ExitBootloader => cap.supports_boot,
CommandId::FirmwareChunk | CommandId::FirmwareCommit => cap.supports_firmware,
| CommandId::ExitBootloader
| CommandId::Jp108EnterBootloader
| CommandId::Jp108ExitBootloader
| CommandId::U2EnterBootloader
| CommandId::U2ExitBootloader => cap.supports_boot,
CommandId::FirmwareChunk
| CommandId::FirmwareCommit
| CommandId::Jp108FirmwareChunk
| CommandId::Jp108FirmwareCommit
| CommandId::U2FirmwareChunk
| CommandId::U2FirmwareCommit => cap.supports_firmware,
CommandId::Jp108ReadDedicatedMappings
| CommandId::Jp108WriteDedicatedMapping
| CommandId::Jp108ReadFeatureFlags
| CommandId::Jp108WriteFeatureFlags
| CommandId::Jp108ReadVoice
| CommandId::Jp108WriteVoice => cap.supports_jp108_dedicated_map,
CommandId::U2GetCurrentSlot
| CommandId::U2ReadConfigSlot
| CommandId::U2WriteConfigSlot => cap.supports_u2_slot_config,
CommandId::U2ReadButtonMap | CommandId::U2WriteButtonMap | CommandId::U2SetMode => {
cap.supports_u2_button_map
}
}
}
@@ -602,6 +885,16 @@ fn is_command_allowed_by_family(family: ProtocolFamily, command: CommandId) -> b
| CommandId::WriteProfile
| CommandId::FirmwareChunk
| CommandId::FirmwareCommit
| CommandId::U2GetCurrentSlot
| CommandId::U2ReadConfigSlot
| CommandId::U2WriteConfigSlot
| CommandId::U2ReadButtonMap
| CommandId::U2WriteButtonMap
| CommandId::U2SetMode
| CommandId::U2EnterBootloader
| CommandId::U2FirmwareChunk
| CommandId::U2FirmwareCommit
| CommandId::U2ExitBootloader
),
ProtocolFamily::DS4Boot => matches!(
command,
@@ -653,6 +946,21 @@ pub fn validate_response(command: CommandId, response: &[u8]) -> ResponseStatus
ResponseStatus::Invalid
}
}
CommandId::Jp108ReadDedicatedMappings
| CommandId::Jp108ReadFeatureFlags
| CommandId::Jp108ReadVoice
| CommandId::U2ReadConfigSlot
| CommandId::U2ReadButtonMap
| CommandId::U2GetCurrentSlot => {
if response.len() < 6 {
return ResponseStatus::Malformed;
}
if response[0] == 0x02 && response[1] == 0x05 {
ResponseStatus::Ok
} else {
ResponseStatus::Invalid
}
}
CommandId::GetControllerVersion | CommandId::Version => {
if response.len() < 5 {
return ResponseStatus::Malformed;
@@ -689,6 +997,12 @@ fn minimum_response_len(command: CommandId) -> usize {
CommandId::GetPid => 24,
CommandId::GetReportRevision => 6,
CommandId::GetMode | CommandId::GetModeAlt => 6,
CommandId::U2GetCurrentSlot => 6,
CommandId::Jp108ReadDedicatedMappings
| CommandId::Jp108ReadFeatureFlags
| CommandId::Jp108ReadVoice
| CommandId::U2ReadConfigSlot
| CommandId::U2ReadButtonMap => 12,
CommandId::GetControllerVersion | CommandId::Version => 5,
_ => 2,
}
@@ -709,7 +1023,27 @@ fn parse_fields(command: CommandId, response: &[u8]) -> BTreeMap<String, u32> {
parsed.insert("version_x100".to_owned(), fw);
parsed.insert("beta".to_owned(), response[4] as u32);
}
CommandId::U2GetCurrentSlot if response.len() >= 6 => {
parsed.insert("slot".to_owned(), response[5] as u32);
}
_ => {}
}
parsed
}
fn parse_indexed_u16_table(raw: &[u8], expected_items: usize) -> Vec<(u8, u16)> {
let mut out = Vec::with_capacity(expected_items);
let offset = if raw.len() >= 8 { 8 } else { 2 };
for idx in 0..expected_items {
let pos = offset + idx * 2;
let usage = if pos + 1 < raw.len() {
u16::from_le_bytes([raw[pos], raw[pos + 1]])
} else {
0
};
out.push((idx as u8, usage));
}
out
}

View File

@@ -50,6 +50,13 @@ pub enum SupportLevel {
DetectOnly,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum SupportTier {
DetectOnly,
CandidateReadOnly,
Full,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum SafetyClass {
SafeRead,
@@ -70,6 +77,24 @@ pub enum CommandConfidence {
Inferred,
}
/// Runtime execution policy for a declared command path.
///
/// This allows us to hardcode every evidenced command in the registry while
/// still keeping unsafe or low-confidence paths blocked by default.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CommandRuntimePolicy {
EnabledDefault,
ExperimentalGate,
BlockedUntilConfirmed,
}
/// Evidence confidence used by policy/reporting surfaces.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum EvidenceConfidence {
Confirmed,
Inferred,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum SupportEvidence {
Confirmed,
@@ -83,6 +108,9 @@ pub struct PidCapability {
pub supports_profile_rw: bool,
pub supports_boot: bool,
pub supports_firmware: bool,
pub supports_jp108_dedicated_map: bool,
pub supports_u2_slot_config: bool,
pub supports_u2_button_map: bool,
}
impl PidCapability {
@@ -92,6 +120,9 @@ impl PidCapability {
supports_profile_rw: true,
supports_boot: true,
supports_firmware: true,
supports_jp108_dedicated_map: true,
supports_u2_slot_config: true,
supports_u2_button_map: true,
}
}
@@ -101,6 +132,9 @@ impl PidCapability {
supports_profile_rw: false,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
}
}
}
@@ -110,6 +144,7 @@ pub struct DeviceProfile {
pub vid_pid: VidPid,
pub name: String,
pub support_level: SupportLevel,
pub support_tier: SupportTier,
pub protocol_family: ProtocolFamily,
pub capability: PidCapability,
pub evidence: SupportEvidence,

View File

@@ -0,0 +1,19 @@
[package]
name = "bitdo_tui"
version = "0.1.0"
edition = "2021"
license = "BSD-3-Clause"
[dependencies]
anyhow = { workspace = true }
ratatui = { workspace = true }
crossterm = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
chrono = { workspace = true }
toml = { workspace = true }
bitdo_proto = { path = "../bitdo_proto" }
bitdo_app_core = { path = "../bitdo_app_core" }
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "time"] }

View File

@@ -0,0 +1,69 @@
use anyhow::{anyhow, Result};
use std::path::Path;
use std::process::{Command, Stdio};
/// Open a file or directory with the user's default desktop application.
pub(crate) fn open_path_with_default_app(path: &Path) -> Result<()> {
let mut cmd = if cfg!(target_os = "macos") {
let mut c = Command::new("open");
c.arg(path);
c
} else {
let mut c = Command::new("xdg-open");
c.arg(path);
c
};
let status = cmd.status()?;
if status.success() {
Ok(())
} else {
Err(anyhow!(
"failed to open path with default app: {}",
path.to_string_lossy()
))
}
}
/// Copy text into the system clipboard using platform-appropriate commands.
pub(crate) fn copy_text_to_clipboard(text: &str) -> Result<()> {
if cfg!(target_os = "macos") {
return copy_via_command("pbcopy", &[], text);
}
if command_exists("wl-copy") {
return copy_via_command("wl-copy", &[], text);
}
if command_exists("xclip") {
return copy_via_command("xclip", &["-selection", "clipboard"], text);
}
Err(anyhow!(
"no clipboard utility found (tried pbcopy/wl-copy/xclip)"
))
}
fn command_exists(name: &str) -> bool {
Command::new("sh")
.arg("-c")
.arg(format!("command -v {name} >/dev/null 2>&1"))
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn copy_via_command(command: &str, args: &[&str], text: &str) -> Result<()> {
let mut child = Command::new(command)
.args(args)
.stdin(Stdio::piped())
.spawn()?;
if let Some(stdin) = child.stdin.as_mut() {
use std::io::Write as _;
stdin.write_all(text.as_bytes())?;
}
let status = child.wait()?;
if status.success() {
Ok(())
} else {
Err(anyhow!("clipboard command failed: {command}"))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
use crate::ReportSaveMode;
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Clone, Debug, Serialize, Deserialize)]
struct PersistedSettings {
#[serde(default = "default_settings_schema_version")]
schema_version: u32,
#[serde(default)]
advanced_mode: bool,
#[serde(default)]
report_save_mode: ReportSaveMode,
}
const fn default_settings_schema_version() -> u32 {
1
}
/// Persist beginner/advanced preferences in a small TOML config file.
pub(crate) fn persist_user_settings(
path: &Path,
advanced_mode: bool,
report_save_mode: ReportSaveMode,
) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let body = toml::to_string_pretty(&PersistedSettings {
schema_version: default_settings_schema_version(),
advanced_mode,
report_save_mode,
})
.map_err(|err| anyhow!("failed to serialize user settings: {err}"))?;
std::fs::write(path, body)?;
Ok(())
}

View File

@@ -0,0 +1,217 @@
use crate::AppDevice;
use anyhow::{anyhow, Result};
use bitdo_app_core::FirmwareFinalReport;
use bitdo_proto::{DiagProbeResult, SupportLevel, SupportTier};
use chrono::Utc;
use serde::Serialize;
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
const REPORT_MAX_COUNT: usize = 20;
const REPORT_MAX_AGE_DAYS: u64 = 30;
#[derive(Clone, Debug, Serialize)]
struct SupportReport {
schema_version: u32,
generated_at_utc: String,
operation: String,
device: Option<SupportReportDevice>,
status: String,
message: String,
diag: Option<DiagProbeResult>,
firmware: Option<FirmwareFinalReport>,
}
#[derive(Clone, Debug, Serialize)]
struct SupportReportDevice {
vid: u16,
pid: u16,
name: String,
canonical_id: String,
runtime_label: String,
serial: Option<String>,
support_level: String,
support_tier: String,
}
/// Persist a troubleshooting report as TOML.
///
/// Reports are intended for failure/support paths and are named with a timestamp plus
/// a serial-or-VID/PID token so users can share deterministic artifacts with support.
pub(crate) async fn persist_support_report(
operation: &str,
device: Option<&AppDevice>,
status: &str,
message: String,
diag: Option<&DiagProbeResult>,
firmware: Option<&FirmwareFinalReport>,
) -> Result<PathBuf> {
let now = Utc::now();
let report = SupportReport {
schema_version: 1,
generated_at_utc: now.to_rfc3339(),
operation: operation.to_owned(),
device: device.map(|d| SupportReportDevice {
vid: d.vid_pid.vid,
pid: d.vid_pid.pid,
name: d.name.clone(),
canonical_id: d.name.clone(),
runtime_label: d.support_status().as_str().to_owned(),
serial: d.serial.clone(),
support_level: match d.support_level {
SupportLevel::Full => "full".to_owned(),
SupportLevel::DetectOnly => "detect-only".to_owned(),
},
support_tier: match d.support_tier {
SupportTier::Full => "full".to_owned(),
SupportTier::CandidateReadOnly => "candidate-readonly".to_owned(),
SupportTier::DetectOnly => "detect-only".to_owned(),
},
}),
status: status.to_owned(),
message,
diag: diag.cloned(),
firmware: firmware.cloned(),
};
let report_dir = default_report_directory();
tokio::fs::create_dir_all(&report_dir).await?;
let token = report_subject_token(device);
let file_name = format!(
"{}-{}-{}.toml",
sanitize_token(operation),
now.format("%Y%m%d-%H%M%S"),
token
);
let path = report_dir.join(file_name);
let body = toml::to_string_pretty(&report)
.map_err(|err| anyhow!("failed to serialize support report: {err}"))?;
tokio::fs::write(&path, body).await?;
let _ = prune_reports_on_write().await;
Ok(path)
}
/// Startup pruning is age-based to keep stale files out of user systems.
pub(crate) async fn prune_reports_on_startup() -> Result<()> {
prune_reports_by_age().await
}
/// Write-time pruning is count-based to keep growth bounded deterministically.
async fn prune_reports_on_write() -> Result<()> {
prune_reports_by_count().await
}
pub(crate) fn report_subject_token(device: Option<&AppDevice>) -> String {
if let Some(device) = device {
if let Some(serial) = device.serial.as_deref() {
let cleaned = sanitize_token(serial);
if !cleaned.is_empty() {
return cleaned;
}
}
return format!("{:04x}{:04x}", device.vid_pid.vid, device.vid_pid.pid);
}
"unknown".to_owned()
}
fn sanitize_token(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for ch in value.chars() {
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
out.push(ch);
} else {
out.push('_');
}
}
out.trim_matches('_').to_owned()
}
fn default_report_directory() -> PathBuf {
if cfg!(target_os = "macos") {
return home_directory()
.join("Library")
.join("Application Support")
.join("OpenBitdo")
.join("reports");
}
if cfg!(target_os = "linux") {
if let Some(xdg_data_home) = std::env::var_os("XDG_DATA_HOME") {
return PathBuf::from(xdg_data_home)
.join("openbitdo")
.join("reports");
}
return home_directory()
.join(".local")
.join("share")
.join("openbitdo")
.join("reports");
}
std::env::temp_dir().join("openbitdo").join("reports")
}
async fn list_report_files() -> Result<Vec<PathBuf>> {
let report_dir = default_report_directory();
let mut out = Vec::new();
let mut entries = match tokio::fs::read_dir(&report_dir).await {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(out),
Err(err) => return Err(err.into()),
};
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("toml") {
out.push(path);
}
}
Ok(out)
}
async fn prune_reports_by_count() -> Result<()> {
let mut files = list_report_files().await?;
files.sort_by_key(|path| {
std::fs::metadata(path)
.and_then(|meta| meta.modified())
.unwrap_or(SystemTime::UNIX_EPOCH)
});
files.reverse();
for path in files.into_iter().skip(REPORT_MAX_COUNT) {
let _ = tokio::fs::remove_file(path).await;
}
Ok(())
}
async fn prune_reports_by_age() -> Result<()> {
let now = SystemTime::now();
let max_age = Duration::from_secs(REPORT_MAX_AGE_DAYS * 24 * 60 * 60);
for path in list_report_files().await? {
let Ok(meta) = std::fs::metadata(&path) else {
continue;
};
let Ok(modified) = meta.modified() else {
continue;
};
if now.duration_since(modified).unwrap_or_default() > max_age {
let _ = tokio::fs::remove_file(path).await;
}
}
Ok(())
}
fn home_directory() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir)
}

View File

@@ -0,0 +1,370 @@
use super::*;
use crate::support_report::report_subject_token;
use bitdo_app_core::{FirmwareOutcome, OpenBitdoCoreConfig};
use bitdo_proto::SupportLevel;
#[test]
fn about_state_roundtrip_returns_home() {
let mut app = TuiApp::default();
app.refresh_devices(vec![AppDevice {
vid_pid: VidPid::new(0x2dc8, 0x6009),
name: "Test".to_owned(),
support_level: SupportLevel::Full,
support_tier: SupportTier::Full,
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
capability: bitdo_proto::PidCapability::full(),
evidence: bitdo_proto::SupportEvidence::Confirmed,
serial: Some("SERIAL1".to_owned()),
connected: true,
}]);
app.open_about();
assert_eq!(app.state, TuiWorkflowState::About);
app.close_overlay();
assert_eq!(app.state, TuiWorkflowState::Home);
}
#[test]
fn refresh_devices_without_any_device_enters_wait_state() {
let mut app = TuiApp::default();
app.refresh_devices(Vec::new());
assert_eq!(app.state, TuiWorkflowState::WaitForDevice);
assert!(app.selected.is_none());
}
#[test]
fn refresh_devices_autoselects_single_device() {
let mut app = TuiApp::default();
app.refresh_devices(vec![AppDevice {
vid_pid: VidPid::new(0x2dc8, 0x6009),
name: "One".to_owned(),
support_level: SupportLevel::Full,
support_tier: SupportTier::Full,
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
capability: bitdo_proto::PidCapability::full(),
evidence: bitdo_proto::SupportEvidence::Confirmed,
serial: None,
connected: true,
}]);
assert_eq!(app.state, TuiWorkflowState::Home);
assert_eq!(app.selected_index, 0);
assert_eq!(app.selected, Some(VidPid::new(0x2dc8, 0x6009)));
}
#[test]
fn serial_token_prefers_serial_then_vidpid() {
let with_serial = AppDevice {
vid_pid: VidPid::new(0x2dc8, 0x6009),
name: "S".to_owned(),
support_level: SupportLevel::Full,
support_tier: SupportTier::Full,
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
capability: bitdo_proto::PidCapability::full(),
evidence: bitdo_proto::SupportEvidence::Confirmed,
serial: Some("ABC 123".to_owned()),
connected: true,
};
assert_eq!(report_subject_token(Some(&with_serial)), "ABC_123");
let without_serial = AppDevice {
serial: None,
..with_serial
};
assert_eq!(report_subject_token(Some(&without_serial)), "2dc86009");
}
#[test]
fn launch_options_default_to_failure_only_reports() {
let opts = TuiLaunchOptions::default();
assert_eq!(opts.report_save_mode, ReportSaveMode::FailureOnly);
}
#[test]
fn blocked_panel_text_matches_support_tier() {
let mut app = TuiApp::default();
app.refresh_devices(vec![AppDevice {
vid_pid: VidPid::new(0x2dc8, 0x2100),
name: "Candidate".to_owned(),
support_level: SupportLevel::DetectOnly,
support_tier: SupportTier::CandidateReadOnly,
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
capability: bitdo_proto::PidCapability {
supports_mode: true,
supports_profile_rw: true,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
},
evidence: bitdo_proto::SupportEvidence::Inferred,
serial: None,
connected: true,
}]);
let selected = app.selected_device().expect("selected");
let text = blocked_action_panel_text(selected);
assert!(text.contains("blocked"));
assert!(text.contains("Status shown as Blocked"));
assert_eq!(beginner_status_label(selected), "Blocked");
}
#[test]
fn non_advanced_report_mode_skips_off_setting() {
let mut app = TuiApp {
advanced_mode: false,
..Default::default()
};
assert_eq!(app.report_save_mode, ReportSaveMode::FailureOnly);
app.cycle_report_save_mode().expect("cycle");
assert_eq!(app.report_save_mode, ReportSaveMode::Always);
app.cycle_report_save_mode().expect("cycle");
assert_eq!(app.report_save_mode, ReportSaveMode::FailureOnly);
}
#[test]
fn unknown_device_label_is_beginner_friendly() {
let device = AppDevice {
vid_pid: VidPid::new(0x2dc8, 0xabcd),
name: "PID_UNKNOWN".to_owned(),
support_level: SupportLevel::DetectOnly,
support_tier: SupportTier::DetectOnly,
protocol_family: bitdo_proto::ProtocolFamily::Unknown,
capability: bitdo_proto::PidCapability::identify_only(),
evidence: bitdo_proto::SupportEvidence::Untested,
serial: None,
connected: true,
};
let label = super::display_device_name(&device);
assert!(label.contains("Unknown 8BitDo Device"));
assert!(label.contains("2dc8:abcd"));
}
#[tokio::test]
async fn home_refresh_loads_devices() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
default_chunk_size: 16,
progress_interval_ms: 1,
..Default::default()
});
let mut app = TuiApp::default();
app.refresh_devices(core.list_devices().await.expect("devices"));
assert!(!app.devices.is_empty());
assert!(app.selected_device().is_some());
}
#[tokio::test]
async fn run_tui_app_no_ui_blocks_detect_only_pid() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
default_chunk_size: 16,
progress_interval_ms: 1,
..Default::default()
});
let result = run_tui_app(
core,
TuiLaunchOptions {
no_ui: true,
selected_vid_pid: Some(VidPid::new(0x2dc8, 0x2100)),
..Default::default()
},
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn run_tui_app_no_ui_full_support_completes() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
default_chunk_size: 16,
progress_interval_ms: 1,
..Default::default()
});
run_tui_app(
core,
TuiLaunchOptions {
no_ui: true,
selected_vid_pid: Some(VidPid::new(0x2dc8, 0x6009)),
..Default::default()
},
)
.await
.expect("run app");
}
#[tokio::test]
async fn tui_flow_with_manual_path_completes() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
default_chunk_size: 16,
progress_interval_ms: 1,
..Default::default()
});
let path = std::env::temp_dir().join("openbitdo-tui-flow.bin");
tokio::fs::write(&path, vec![1u8; 128])
.await
.expect("write");
let report = run_tui_flow(
core,
TuiRunRequest {
vid_pid: VidPid::new(0x2dc8, 0x6009),
firmware_path: path.clone(),
allow_unsafe: true,
brick_risk_ack: true,
experimental: true,
chunk_size: Some(32),
acknowledged_risk: true,
no_ui: true,
},
)
.await
.expect("run tui flow");
assert_eq!(report.status, FirmwareOutcome::Completed);
let _ = tokio::fs::remove_file(path).await;
}
#[tokio::test]
async fn support_report_is_toml_file() {
let device = AppDevice {
vid_pid: VidPid::new(0x2dc8, 0x6009),
name: "Test".to_owned(),
support_level: SupportLevel::Full,
support_tier: SupportTier::Full,
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
capability: bitdo_proto::PidCapability::full(),
evidence: bitdo_proto::SupportEvidence::Confirmed,
serial: Some("RPT-1".to_owned()),
connected: true,
};
let report_path = persist_support_report(
"diag-probe",
Some(&device),
"ok",
"all checks passed".to_owned(),
None,
None,
)
.await
.expect("report path");
assert_eq!(
report_path.extension().and_then(|s| s.to_str()),
Some("toml")
);
let _ = tokio::fs::remove_file(report_path).await;
}
#[tokio::test]
async fn update_action_enters_jp108_wizard_for_jp108_device() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
..Default::default()
});
let mut app = TuiApp::default();
app.refresh_devices(core.list_devices().await.expect("devices"));
let jp108_idx = app
.devices
.iter()
.position(|d| d.vid_pid.pid == 0x5209)
.expect("jp108 fixture");
app.select_index(jp108_idx);
app.state = TuiWorkflowState::Home;
let mut terminal = None;
let mut events = None;
let opts = TuiLaunchOptions::default();
execute_home_action(
&core,
&mut terminal,
&mut app,
&opts,
&mut events,
HomeAction::Update,
)
.await
.expect("execute");
assert_eq!(app.state, TuiWorkflowState::Jp108Mapping);
assert!(!app.jp108_mappings.is_empty());
}
#[tokio::test]
async fn update_action_enters_u2_wizard_for_ultimate2_device() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
..Default::default()
});
let mut app = TuiApp::default();
app.refresh_devices(core.list_devices().await.expect("devices"));
let u2_idx = app
.devices
.iter()
.position(|d| d.vid_pid.pid == 0x6012)
.expect("u2 fixture");
app.select_index(u2_idx);
app.state = TuiWorkflowState::Home;
let mut terminal = None;
let mut events = None;
let opts = TuiLaunchOptions::default();
execute_home_action(
&core,
&mut terminal,
&mut app,
&opts,
&mut events,
HomeAction::Update,
)
.await
.expect("execute");
assert_eq!(app.state, TuiWorkflowState::U2CoreProfile);
assert!(app.u2_profile.is_some());
}
#[tokio::test]
async fn device_flow_backup_apply_sets_backup_id() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
..Default::default()
});
let mut app = TuiApp::default();
app.refresh_devices(core.list_devices().await.expect("devices"));
let jp108_idx = app
.devices
.iter()
.position(|d| d.vid_pid.pid == 0x5209)
.expect("jp108 fixture");
app.select_index(jp108_idx);
app.begin_jp108_mapping(
core.jp108_read_dedicated_mapping(VidPid::new(0x2dc8, 0x5209))
.await
.expect("read"),
);
let mut terminal = None;
let mut events = None;
let opts = TuiLaunchOptions::default();
execute_device_flow_action(
&core,
&mut terminal,
&mut app,
&opts,
&mut events,
DeviceFlowAction::BackupApply,
)
.await
.expect("apply");
assert!(app.latest_backup.is_some());
}

View File

@@ -1,20 +0,0 @@
[package]
name = "bitdoctl"
version = "0.1.0"
edition = "2021"
license = "MIT"
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true }
serde_json = { workspace = true }
hex = { workspace = true }
bitdo_proto = { path = "../bitdo_proto" }
[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.1"
[[test]]
name = "cli_snapshot"
path = "../../tests/cli_snapshot.rs"

View File

@@ -1,518 +0,0 @@
use anyhow::{anyhow, Result};
use bitdo_proto::{
command_registry, device_profile_for, enumerate_hid_devices, BitdoErrorCode, CommandId,
DeviceSession, FirmwareTransferReport, HidTransport, MockTransport, ProfileBlob, RetryPolicy,
SessionConfig, TimeoutProfile, Transport, VidPid,
};
use clap::{Parser, Subcommand};
use serde_json::json;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(name = "bitdoctl")]
#[command(about = "OpenBitdo clean-room protocol CLI")]
struct Cli {
#[arg(long)]
vid: Option<String>,
#[arg(long)]
pid: Option<String>,
#[arg(long)]
json: bool,
#[arg(long = "unsafe")]
allow_unsafe: bool,
#[arg(long = "i-understand-brick-risk")]
brick_risk_ack: bool,
#[arg(long)]
experimental: bool,
#[arg(long)]
mock: bool,
#[arg(long, default_value_t = 3)]
max_attempts: u8,
#[arg(long, default_value_t = 10)]
backoff_ms: u64,
#[arg(long, default_value_t = 200)]
probe_timeout_ms: u64,
#[arg(long, default_value_t = 400)]
io_timeout_ms: u64,
#[arg(long, default_value_t = 1200)]
firmware_timeout_ms: u64,
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
List,
Identify,
Diag {
#[command(subcommand)]
command: DiagCommand,
},
Profile {
#[command(subcommand)]
command: ProfileCommand,
},
Mode {
#[command(subcommand)]
command: ModeCommand,
},
Boot {
#[command(subcommand)]
command: BootCommand,
},
Fw {
#[command(subcommand)]
command: FwCommand,
},
}
#[derive(Debug, Subcommand)]
enum DiagCommand {
Probe,
}
#[derive(Debug, Subcommand)]
enum ProfileCommand {
Dump {
#[arg(long)]
slot: u8,
},
Apply {
#[arg(long)]
slot: u8,
#[arg(long)]
file: PathBuf,
},
}
#[derive(Debug, Subcommand)]
enum ModeCommand {
Get,
Set {
#[arg(long)]
mode: u8,
},
}
#[derive(Debug, Subcommand)]
enum BootCommand {
Enter,
Exit,
}
#[derive(Debug, Subcommand)]
enum FwCommand {
Write {
#[arg(long)]
file: PathBuf,
#[arg(long, default_value_t = 56)]
chunk_size: usize,
#[arg(long)]
dry_run: bool,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
if let Err(err) = run(cli) {
eprintln!("error: {err}");
return Err(err);
}
Ok(())
}
fn run(cli: Cli) -> Result<()> {
match &cli.command {
Commands::List => handle_list(&cli),
Commands::Identify
| Commands::Diag { .. }
| Commands::Profile { .. }
| Commands::Mode { .. }
| Commands::Boot { .. }
| Commands::Fw { .. } => {
let target = resolve_target(&cli)?;
let transport: Box<dyn Transport> = if cli.mock {
Box::new(mock_transport_for(&cli.command, target)?)
} else {
Box::new(HidTransport::new())
};
let config = SessionConfig {
retry_policy: RetryPolicy {
max_attempts: cli.max_attempts,
backoff_ms: cli.backoff_ms,
},
timeout_profile: TimeoutProfile {
probe_ms: cli.probe_timeout_ms,
io_ms: cli.io_timeout_ms,
firmware_ms: cli.firmware_timeout_ms,
},
allow_unsafe: cli.allow_unsafe,
brick_risk_ack: cli.brick_risk_ack,
experimental: cli.experimental,
trace_enabled: true,
};
let mut session = DeviceSession::new(transport, target, config)?;
match &cli.command {
Commands::Identify => {
let info = session.identify()?;
if cli.json {
println!("{}", serde_json::to_string_pretty(&info)?);
} else {
println!(
"target={} profile={} support={:?} family={:?} evidence={:?} capability={:?} detected_pid={}",
info.target,
info.profile_name,
info.support_level,
info.protocol_family,
info.evidence,
info.capability,
info.detected_pid
.map(|v| format!("{v:#06x}"))
.unwrap_or_else(|| "none".to_owned())
);
}
}
Commands::Diag { command } => match command {
DiagCommand::Probe => {
let diag = session.diag_probe();
if cli.json {
println!("{}", serde_json::to_string_pretty(&diag)?);
} else {
println!(
"diag target={} profile={} family={:?}",
diag.target, diag.profile_name, diag.protocol_family
);
for check in diag.command_checks {
println!(
" {:?}: ok={} code={}",
check.command,
check.ok,
check
.error_code
.map(|c| format!("{c:?}"))
.unwrap_or_else(|| "none".to_owned())
);
}
}
}
},
Commands::Mode { command } => match command {
ModeCommand::Get => {
let mode = session.get_mode()?;
print_mode(mode.mode, &mode.source, cli.json);
}
ModeCommand::Set { mode } => {
let mode_state = session.set_mode(*mode)?;
print_mode(mode_state.mode, &mode_state.source, cli.json);
}
},
Commands::Profile { command } => match command {
ProfileCommand::Dump { slot } => {
let profile = session.read_profile(*slot)?;
if cli.json {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"slot": profile.slot,
"payload_hex": hex::encode(&profile.payload),
}))?
);
} else {
println!(
"slot={} payload_hex={}",
profile.slot,
hex::encode(&profile.payload)
);
}
}
ProfileCommand::Apply { slot, file } => {
let bytes = fs::read(file)?;
let parsed = ProfileBlob::from_bytes(&bytes)?;
let blob = ProfileBlob {
slot: *slot,
payload: parsed.payload,
};
session.write_profile(*slot, &blob)?;
if cli.json {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"applied": true,
"slot": slot,
}))?
);
} else {
println!("applied profile to slot={slot}");
}
}
},
Commands::Boot { command } => {
match command {
BootCommand::Enter => session.enter_bootloader()?,
BootCommand::Exit => session.exit_bootloader()?,
}
if cli.json {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"ok": true,
"command": format!("{:?}", command),
}))?
);
} else {
println!("{:?} completed", command);
}
}
Commands::Fw { command } => match command {
FwCommand::Write {
file,
chunk_size,
dry_run,
} => {
let image = fs::read(file)?;
let report = session.firmware_transfer(&image, *chunk_size, *dry_run)?;
print_fw_report(report, cli.json)?;
}
},
Commands::List => unreachable!(),
}
session.close()?;
Ok(())
}
}
}
fn handle_list(cli: &Cli) -> Result<()> {
if cli.mock {
let profile = device_profile_for(VidPid::new(0x2dc8, 0x6009));
if cli.json {
println!(
"{}",
serde_json::to_string_pretty(&vec![json!({
"vid": "0x2dc8",
"pid": "0x6009",
"product": "Mock 8BitDo Device",
"support_level": format!("{:?}", profile.support_level),
"protocol_family": format!("{:?}", profile.protocol_family),
"capability": profile.capability,
"evidence": format!("{:?}", profile.evidence),
})])?
);
} else {
println!("2dc8:6009 Mock 8BitDo Device");
}
return Ok(());
}
let devices = enumerate_hid_devices()?;
let filtered: Vec<_> = devices
.into_iter()
.filter(|d| d.vid_pid.vid == 0x2dc8)
.collect();
if cli.json {
let out: Vec<_> = filtered
.iter()
.map(|d| {
let profile = device_profile_for(d.vid_pid);
json!({
"vid": format!("{:#06x}", d.vid_pid.vid),
"pid": format!("{:#06x}", d.vid_pid.pid),
"product": d.product,
"manufacturer": d.manufacturer,
"serial": d.serial,
"path": d.path,
"support_level": format!("{:?}", profile.support_level),
"protocol_family": format!("{:?}", profile.protocol_family),
"capability": profile.capability,
"evidence": format!("{:?}", profile.evidence),
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&out)?);
} else {
for d in &filtered {
println!(
"{} {}",
d.vid_pid,
d.product.as_deref().unwrap_or("(unknown product)")
);
}
}
Ok(())
}
fn resolve_target(cli: &Cli) -> Result<VidPid> {
let vid = cli
.vid
.as_deref()
.map(parse_u16)
.transpose()?
.unwrap_or(0x2dc8);
let pid_str = cli
.pid
.as_deref()
.ok_or_else(|| anyhow!("--pid is required for this command"))?;
let pid = parse_u16(pid_str)?;
Ok(VidPid::new(vid, pid))
}
fn parse_u16(input: &str) -> Result<u16> {
if let Some(hex) = input
.strip_prefix("0x")
.or_else(|| input.strip_prefix("0X"))
{
return Ok(u16::from_str_radix(hex, 16)?);
}
Ok(input.parse::<u16>()?)
}
fn mock_transport_for(command: &Commands, target: VidPid) -> Result<MockTransport> {
let mut t = MockTransport::default();
match command {
Commands::Identify => {
t.push_read_data(build_pid_response(target.pid));
}
Commands::Diag { command } => match command {
DiagCommand::Probe => {
t.push_read_data(build_pid_response(target.pid));
t.push_read_data(build_rr_response());
t.push_read_data(build_mode_response(2));
t.push_read_data(build_version_response());
}
},
Commands::Mode { command } => match command {
ModeCommand::Get => t.push_read_data(build_mode_response(2)),
ModeCommand::Set { mode } => {
t.push_read_data(build_ack_response());
t.push_read_data(build_mode_response(*mode));
}
},
Commands::Profile { command } => match command {
ProfileCommand::Dump { slot } => {
let mut raw = vec![0x02, 0x06, 0x00, *slot];
raw.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
t.push_read_data(raw);
}
ProfileCommand::Apply { .. } => {
t.push_read_data(build_ack_response());
}
},
Commands::Boot { .. } => {}
Commands::Fw { command } => {
let chunks = match command {
FwCommand::Write {
file,
chunk_size,
dry_run,
} => {
if *dry_run {
0
} else {
let sz = fs::metadata(file).map(|m| m.len() as usize).unwrap_or(0);
sz.div_ceil(*chunk_size) + 1
}
}
};
for _ in 0..chunks {
t.push_read_data(build_ack_response());
}
}
Commands::List => {}
}
if matches!(command, Commands::Profile { .. } | Commands::Fw { .. })
&& !command_registry()
.iter()
.any(|c| c.id == CommandId::ReadProfile)
{
return Err(anyhow!("command registry is empty"));
}
Ok(t)
}
fn build_ack_response() -> Vec<u8> {
vec![0x02, 0x01, 0x00, 0x00]
}
fn build_mode_response(mode: u8) -> Vec<u8> {
let mut out = vec![0u8; 64];
out[0] = 0x02;
out[1] = 0x05;
out[5] = mode;
out
}
fn build_rr_response() -> Vec<u8> {
let mut out = vec![0u8; 64];
out[0] = 0x02;
out[1] = 0x04;
out[5] = 0x01;
out
}
fn build_version_response() -> Vec<u8> {
let mut out = vec![0u8; 64];
out[0] = 0x02;
out[1] = 0x22;
out[2] = 0x2A;
out[3] = 0x00;
out[4] = 0x01;
out
}
fn build_pid_response(pid: u16) -> Vec<u8> {
let mut out = vec![0u8; 64];
out[0] = 0x02;
out[1] = 0x05;
out[4] = 0xC1;
let [lo, hi] = pid.to_le_bytes();
out[22] = lo;
out[23] = hi;
out
}
fn print_mode(mode: u8, source: &str, as_json: bool) {
if as_json {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"mode": mode,
"source": source,
}))
.expect("json serialization")
);
} else {
println!("mode={} source={}", mode, source);
}
}
fn print_fw_report(report: FirmwareTransferReport, as_json: bool) -> Result<()> {
if as_json {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
println!(
"bytes_total={} chunk_size={} chunks_sent={} dry_run={}",
report.bytes_total, report.chunk_size, report.chunks_sent, report.dry_run
);
}
Ok(())
}
#[allow(dead_code)]
fn print_error_code(code: BitdoErrorCode, as_json: bool) {
if as_json {
println!(
"{}",
serde_json::to_string_pretty(&json!({ "error_code": format!("{:?}", code) }))
.expect("json serialization")
);
} else {
println!("error_code={:?}", code);
}
}

View File

@@ -0,0 +1,21 @@
[package]
name = "openbitdo"
version = "0.1.0"
edition = "2021"
license = "BSD-3-Clause"
build = "build.rs"
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true }
serde = { workspace = true }
toml = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
bitdo_app_core = { path = "../bitdo_app_core" }
bitdo_tui = { path = "../bitdo_tui" }
[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.1"

View File

@@ -0,0 +1,42 @@
use std::env;
use std::process::Command;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
emit("OPENBITDO_APP_VERSION", env::var("CARGO_PKG_VERSION").ok());
emit("OPENBITDO_TARGET_TRIPLE", env::var("TARGET").ok());
emit(
"OPENBITDO_GIT_COMMIT_FULL",
run_cmd("git", &["rev-parse", "HEAD"]),
);
emit(
"OPENBITDO_GIT_COMMIT_SHORT",
run_cmd("git", &["rev-parse", "--short=12", "HEAD"]),
);
emit(
"OPENBITDO_BUILD_DATE_UTC",
run_cmd("date", &["-u", "+%Y-%m-%dT%H:%M:%SZ"]),
);
}
fn emit(key: &str, value: Option<String>) {
let normalized = value
.as_deref()
.map(str::trim)
.filter(|v| !v.is_empty())
.unwrap_or("unknown");
println!("cargo:rustc-env={key}={normalized}");
}
fn run_cmd(program: &str, args: &[&str]) -> Option<String> {
let output = Command::new(program).args(args).output().ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()
.map(|v| v.trim().to_owned())
.filter(|v| !v.is_empty())
}

View File

@@ -0,0 +1,218 @@
use bitdo_app_core::{signing_key_fingerprint_active_sha256, signing_key_fingerprint_next_sha256};
use bitdo_tui::ReportSaveMode;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BuildInfo {
pub app_version: String,
pub git_commit_short: String,
pub git_commit_full: String,
pub build_date_utc: String,
pub target_triple: String,
pub runtime_platform: String,
pub signing_key_fingerprint_short: String,
pub signing_key_fingerprint_full: String,
pub signing_key_next_fingerprint_short: String,
}
impl BuildInfo {
pub fn current() -> Self {
Self::from_raw(
option_env!("OPENBITDO_APP_VERSION"),
option_env!("OPENBITDO_GIT_COMMIT_SHORT"),
option_env!("OPENBITDO_GIT_COMMIT_FULL"),
option_env!("OPENBITDO_BUILD_DATE_UTC"),
option_env!("OPENBITDO_TARGET_TRIPLE"),
)
}
pub fn to_tui_info(&self) -> bitdo_tui::BuildInfo {
bitdo_tui::BuildInfo {
app_version: self.app_version.clone(),
git_commit_short: self.git_commit_short.clone(),
git_commit_full: self.git_commit_full.clone(),
build_date_utc: self.build_date_utc.clone(),
target_triple: self.target_triple.clone(),
runtime_platform: self.runtime_platform.clone(),
signing_key_fingerprint_short: self.signing_key_fingerprint_short.clone(),
signing_key_fingerprint_full: self.signing_key_fingerprint_full.clone(),
signing_key_next_fingerprint_short: self.signing_key_next_fingerprint_short.clone(),
}
}
fn from_raw(
app_version: Option<&'static str>,
git_commit_short: Option<&'static str>,
git_commit_full: Option<&'static str>,
build_date_utc: Option<&'static str>,
target_triple: Option<&'static str>,
) -> Self {
Self {
app_version: normalize(app_version),
git_commit_short: normalize(git_commit_short),
git_commit_full: normalize(git_commit_full),
build_date_utc: normalize(build_date_utc),
target_triple: normalize(target_triple),
runtime_platform: format!("{}/{}", std::env::consts::OS, std::env::consts::ARCH),
signing_key_fingerprint_short: short_fingerprint(
&signing_key_fingerprint_active_sha256(),
),
signing_key_fingerprint_full: signing_key_fingerprint_active_sha256(),
signing_key_next_fingerprint_short: short_fingerprint(
&signing_key_fingerprint_next_sha256(),
),
}
}
}
fn normalize(value: Option<&str>) -> String {
value
.map(str::trim)
.filter(|v| !v.is_empty())
.unwrap_or("unknown")
.to_owned()
}
fn short_fingerprint(full: &str) -> String {
if full == "unknown" {
return "unknown".to_owned();
}
full.chars().take(16).collect()
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct UserSettings {
#[serde(default = "default_settings_schema_version")]
pub schema_version: u32,
#[serde(default)]
pub advanced_mode: bool,
#[serde(default)]
pub report_save_mode: ReportSaveMode,
}
impl Default for UserSettings {
fn default() -> Self {
Self {
schema_version: default_settings_schema_version(),
advanced_mode: false,
report_save_mode: ReportSaveMode::FailureOnly,
}
}
}
const fn default_settings_schema_version() -> u32 {
1
}
pub fn user_settings_path() -> PathBuf {
if cfg!(target_os = "macos") {
return home_directory()
.join("Library")
.join("Application Support")
.join("OpenBitdo")
.join("config.toml");
}
if cfg!(target_os = "linux") {
if let Some(xdg_config_home) = std::env::var_os("XDG_CONFIG_HOME") {
return PathBuf::from(xdg_config_home)
.join("openbitdo")
.join("config.toml");
}
return home_directory()
.join(".config")
.join("openbitdo")
.join("config.toml");
}
std::env::temp_dir().join("openbitdo").join("config.toml")
}
pub fn load_user_settings(path: &Path) -> UserSettings {
let Ok(raw) = std::fs::read_to_string(path) else {
return UserSettings::default();
};
let mut settings: UserSettings = toml::from_str(&raw).unwrap_or_default();
if !settings.advanced_mode && settings.report_save_mode == ReportSaveMode::Off {
settings.report_save_mode = ReportSaveMode::FailureOnly;
}
settings
}
pub fn save_user_settings(path: &Path, settings: &UserSettings) -> anyhow::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let body = toml::to_string_pretty(settings)?;
std::fs::write(path, body)?;
Ok(())
}
fn home_directory() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn build_info_falls_back_to_unknown_when_missing() {
let info = BuildInfo::from_raw(None, None, None, None, None);
assert_eq!(info.app_version, "unknown");
assert_eq!(info.git_commit_short, "unknown");
assert_eq!(info.git_commit_full, "unknown");
assert_eq!(info.build_date_utc, "unknown");
assert_eq!(info.target_triple, "unknown");
}
#[test]
fn runtime_platform_has_expected_separator() {
let info = BuildInfo::from_raw(None, None, None, None, None);
assert!(info.runtime_platform.contains('/'));
}
#[test]
fn normalize_trims_and_preserves_values() {
let info = BuildInfo::from_raw(
Some(" 0.1.0 "),
Some(" abc123 "),
Some(" abc123def456 "),
Some(" 2026-01-01T00:00:00Z "),
Some(" x86_64-unknown-linux-gnu "),
);
assert_eq!(info.app_version, "0.1.0");
assert_eq!(info.git_commit_short, "abc123");
assert_eq!(info.git_commit_full, "abc123def456");
assert_eq!(info.build_date_utc, "2026-01-01T00:00:00Z");
assert_eq!(info.target_triple, "x86_64-unknown-linux-gnu");
}
#[test]
fn settings_roundtrip_toml() {
let tmp =
std::env::temp_dir().join(format!("openbitdo-settings-{}.toml", std::process::id()));
let settings = UserSettings {
schema_version: 1,
advanced_mode: true,
report_save_mode: ReportSaveMode::Always,
};
save_user_settings(&tmp, &settings).expect("save settings");
let loaded = load_user_settings(&tmp);
assert_eq!(loaded, settings);
let _ = std::fs::remove_file(tmp);
}
#[test]
fn missing_settings_uses_defaults() {
let path = PathBuf::from("/tmp/openbitdo-nonexistent-settings.toml");
let loaded = load_user_settings(&path);
assert!(!loaded.advanced_mode);
assert_eq!(loaded.report_save_mode, ReportSaveMode::FailureOnly);
}
}

View File

@@ -0,0 +1,58 @@
use anyhow::Result;
use bitdo_app_core::{OpenBitdoCore, OpenBitdoCoreConfig};
use bitdo_tui::{run_tui_app, TuiLaunchOptions};
use clap::Parser;
use openbitdo::{load_user_settings, user_settings_path, BuildInfo};
#[derive(Debug, Parser)]
#[command(name = "openbitdo")]
#[command(about = "OpenBitdo beginner-first launcher")]
struct Cli {
#[arg(long, help = "Use mock transport/devices")]
mock: bool,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let cli = Cli::parse();
let settings_path = user_settings_path();
let settings = load_user_settings(&settings_path);
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: cli.mock,
advanced_mode: settings.advanced_mode,
progress_interval_ms: 5,
..Default::default()
});
run_tui_app(
core,
TuiLaunchOptions {
build_info: BuildInfo::current().to_tui_info(),
advanced_mode: settings.advanced_mode,
report_save_mode: settings.report_save_mode,
settings_path: Some(settings_path),
..Default::default()
},
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::error::ErrorKind;
#[test]
fn cli_supports_mock_only() {
let cli = Cli::parse_from(["openbitdo", "--mock"]);
assert!(cli.mock);
}
#[test]
fn cli_rejects_cmd_subcommand() {
let err = Cli::try_parse_from(["openbitdo", "cmd"]).expect_err("must reject cmd");
assert_eq!(err.kind(), ErrorKind::UnknownArgument);
}
}

View File

@@ -0,0 +1,13 @@
use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;
#[test]
fn help_mentions_beginner_flow() {
let mut cmd = cargo_bin_cmd!("openbitdo");
cmd.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("beginner-first"))
.stdout(predicate::str::contains("--mock"))
.stdout(predicate::str::contains("cmd").not());
}

View File

@@ -5,9 +5,9 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
forbidden_pattern='decompiled(_dll|_autoupdate)?/|bundle_extract/|extracted(_net)?/|session-ses_35e4|8BitDo_Ultimate_Software_V2\.decompiled\.cs'
scan_paths=(crates tests .github)
scan_paths=(crates tests scripts ../.github)
if rg -n --hidden -g '!target/**' "$forbidden_pattern" "${scan_paths[@]}"; then
if rg -n --hidden -g '!target/**' -g '!scripts/cleanroom_guard.sh' "$forbidden_pattern" "${scan_paths[@]}"; then
echo "cleanroom guard failed: forbidden dirty-room reference detected"
exit 1
fi

68
sdk/scripts/package-linux.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REPO_ROOT="$(cd "$ROOT/.." && pwd)"
VERSION="${1:-v0.0.1-rc.1}"
ARCH_LABEL="${2:-$(uname -m)}"
TARGET_TRIPLE="${3:-}"
if [[ "$(uname -s)" != "Linux" ]]; then
echo "package-linux.sh must run on Linux" >&2
exit 1
fi
case "$ARCH_LABEL" in
x86_64|aarch64) ;;
arm64) ARCH_LABEL="aarch64" ;;
*)
echo "unsupported linux arch label: $ARCH_LABEL" >&2
exit 1
;;
esac
DIST_ROOT="$ROOT/dist"
PKG_NAME="openbitdo-${VERSION}-linux-${ARCH_LABEL}"
PKG_DIR="$DIST_ROOT/$PKG_NAME"
BIN_ASSET="$DIST_ROOT/${PKG_NAME}"
checksum_file() {
local path="$1"
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$path" > "${path}.sha256"
elif command -v sha256sum >/dev/null 2>&1; then
sha256sum "$path" > "${path}.sha256"
else
echo "warning: no checksum tool found for $path" >&2
fi
}
build_binary() {
cd "$ROOT"
if [[ -n "$TARGET_TRIPLE" ]]; then
cargo build --release -p openbitdo --target "$TARGET_TRIPLE"
echo "$ROOT/target/$TARGET_TRIPLE/release/openbitdo"
else
cargo build --release -p openbitdo
echo "$ROOT/target/release/openbitdo"
fi
}
BIN_PATH="$(build_binary)"
rm -rf "$PKG_DIR"
mkdir -p "$PKG_DIR/bin" "$DIST_ROOT"
cp "$BIN_PATH" "$PKG_DIR/bin/openbitdo"
cp "$BIN_PATH" "$BIN_ASSET"
cp "$REPO_ROOT/README.md" "$PKG_DIR/README.md"
cp "$ROOT/README.md" "$PKG_DIR/SDK_README.md"
cp "$REPO_ROOT/LICENSE" "$PKG_DIR/LICENSE"
tar -C "$DIST_ROOT" -czf "$DIST_ROOT/${PKG_NAME}.tar.gz" "$PKG_NAME"
checksum_file "$DIST_ROOT/${PKG_NAME}.tar.gz"
checksum_file "$BIN_ASSET"
echo "created package: $DIST_ROOT/${PKG_NAME}.tar.gz"
echo "created standalone binary: $BIN_ASSET"

77
sdk/scripts/package-macos.sh Executable file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REPO_ROOT="$(cd "$ROOT/.." && pwd)"
VERSION="${1:-v0.0.1-rc.1}"
ARCH_LABEL="${2:-arm64}"
TARGET_TRIPLE="${3:-aarch64-apple-darwin}"
INSTALL_PREFIX="${4:-/opt/homebrew/bin}"
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "package-macos.sh must run on macOS" >&2
exit 1
fi
if [[ "$ARCH_LABEL" != "arm64" ]]; then
echo "unsupported macOS arch label: $ARCH_LABEL (expected arm64)" >&2
exit 1
fi
DIST_ROOT="$ROOT/dist"
PKG_NAME="openbitdo-${VERSION}-macos-${ARCH_LABEL}"
PKG_DIR="$DIST_ROOT/$PKG_NAME"
BIN_ASSET="$DIST_ROOT/${PKG_NAME}"
PKG_ASSET="$DIST_ROOT/${PKG_NAME}.pkg"
PKGROOT="$DIST_ROOT/${PKG_NAME}-pkgroot"
checksum_file() {
local path="$1"
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$path" > "${path}.sha256"
elif command -v sha256sum >/dev/null 2>&1; then
sha256sum "$path" > "${path}.sha256"
else
echo "warning: no checksum tool found for $path" >&2
fi
}
build_binary() {
cd "$ROOT"
cargo build --release -p openbitdo --target "$TARGET_TRIPLE"
echo "$ROOT/target/$TARGET_TRIPLE/release/openbitdo"
}
BIN_PATH="$(build_binary)"
VERSION_STRIPPED="${VERSION#v}"
rm -rf "$PKG_DIR" "$PKGROOT" "$PKG_ASSET"
mkdir -p "$PKG_DIR/bin" "$DIST_ROOT"
cp "$BIN_PATH" "$PKG_DIR/bin/openbitdo"
cp "$BIN_PATH" "$BIN_ASSET"
cp "$REPO_ROOT/README.md" "$PKG_DIR/README.md"
cp "$ROOT/README.md" "$PKG_DIR/SDK_README.md"
cp "$REPO_ROOT/LICENSE" "$PKG_DIR/LICENSE"
tar -C "$DIST_ROOT" -czf "$DIST_ROOT/${PKG_NAME}.tar.gz" "$PKG_NAME"
mkdir -p "$PKGROOT${INSTALL_PREFIX}"
cp "$BIN_PATH" "$PKGROOT${INSTALL_PREFIX}/openbitdo"
chmod 755 "$PKGROOT${INSTALL_PREFIX}/openbitdo"
pkgbuild \
--root "$PKGROOT" \
--identifier "io.openbitdo.cli" \
--version "$VERSION_STRIPPED" \
"$PKG_ASSET"
rm -rf "$PKGROOT"
checksum_file "$DIST_ROOT/${PKG_NAME}.tar.gz"
checksum_file "$BIN_ASSET"
checksum_file "$PKG_ASSET"
echo "created package: $DIST_ROOT/${PKG_NAME}.tar.gz"
echo "created standalone binary: $BIN_ASSET"
echo "created installer pkg: $PKG_ASSET"

View File

@@ -3,37 +3,234 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
LAB_CONFIG="${ROOT}/../harness/lab/device_lab.yaml"
if [[ ! -f "$LAB_CONFIG" ]]; then
echo "missing lab config: $LAB_CONFIG" >&2
exit 1
fi
REPORT_DIR="${ROOT}/../harness/reports"
mkdir -p "$REPORT_DIR"
TS="$(date +%Y%m%d-%H%M%S)"
REPORT_PATH="${1:-$REPORT_DIR/hardware_smoke_${TS}.json}"
LIST_JSON="$(cargo run -q -p bitdoctl -- --json list 2>/dev/null || echo '[]')"
SUITE="${BITDO_REQUIRED_SUITE:-family}"
REQUIRED_FAMILIES="${BITDO_REQUIRED_FAMILIES:-Standard64,DInput}"
TEST_OUTPUT_FILE="$(mktemp)"
PARSE_OUTPUT="$(mktemp)"
set +e
BITDO_HARDWARE=1 cargo test --workspace --test hardware_smoke -- --ignored >"$TEST_OUTPUT_FILE" 2>&1
TEST_STATUS=$?
python3 - <<'PY' "$LAB_CONFIG" "$SUITE" "$REQUIRED_FAMILIES" >"$PARSE_OUTPUT"
import pathlib
import re
import sys
config_path = pathlib.Path(sys.argv[1])
suite = sys.argv[2].strip()
required_families = [item.strip() for item in sys.argv[3].split(",") if item.strip()]
lines = config_path.read_text().splitlines()
devices = []
current = None
in_devices = False
def parse_scalar(text: str):
value = text.split("#", 1)[0].strip()
if not value:
return value
if value.startswith(("0x", "0X")):
return int(value, 16)
try:
return int(value)
except ValueError:
return value
for line in lines:
stripped = line.strip()
if stripped.startswith("devices:"):
in_devices = True
continue
if not in_devices:
continue
if stripped.startswith("policies:"):
if current:
devices.append(current)
current = None
break
if re.match(r"^\s*-\s+", line):
if current:
devices.append(current)
current = {}
continue
if current is None:
continue
field_match = re.match(r"^\s*([A-Za-z0-9_]+)\s*:\s*(.+)$", line)
if not field_match:
continue
key = field_match.group(1)
value = parse_scalar(field_match.group(2))
current[key] = value
if current:
devices.append(current)
if not devices:
sys.stderr.write(f"no devices found in {config_path}\n")
sys.exit(1)
family_to_pid = {}
fixture_to_pid = {}
for device in devices:
family = device.get("protocol_family")
pid = device.get("pid")
fixture_id = device.get("fixture_id")
if isinstance(family, str) and isinstance(pid, int) and family not in family_to_pid:
family_to_pid[family] = pid
if isinstance(fixture_id, str) and isinstance(pid, int) and fixture_id not in fixture_to_pid:
fixture_to_pid[fixture_id] = pid
if suite == "family":
missing = [fam for fam in required_families if fam not in family_to_pid]
if missing:
available = ", ".join(sorted(family_to_pid.keys())) if family_to_pid else "none"
sys.stderr.write(
f"missing required family fixtures in {config_path}: {', '.join(missing)}; available: {available}\n"
)
sys.exit(1)
for fam in required_families:
print(f"FAMILY:{fam}={family_to_pid[fam]:#06x}")
elif suite == "ultimate2":
if "ultimate2" not in fixture_to_pid:
available = ", ".join(sorted(fixture_to_pid.keys())) if fixture_to_pid else "none"
sys.stderr.write(
f"missing fixture_id=ultimate2 in {config_path}; available fixture_ids: {available}\n"
)
sys.exit(1)
print(f"FIXTURE:ultimate2={fixture_to_pid['ultimate2']:#06x}")
elif suite == "108jp":
if "108jp" not in fixture_to_pid:
available = ", ".join(sorted(fixture_to_pid.keys())) if fixture_to_pid else "none"
sys.stderr.write(
f"missing fixture_id=108jp in {config_path}; available fixture_ids: {available}\n"
)
sys.exit(1)
print(f"FIXTURE:108jp={fixture_to_pid['108jp']:#06x}")
else:
sys.stderr.write(f"unsupported BITDO_REQUIRED_SUITE value: {suite}\n")
sys.exit(1)
PY
PARSE_STATUS=$?
set -e
python3 - <<'PY' "$REPORT_PATH" "$TEST_STATUS" "$TEST_OUTPUT_FILE" "$LIST_JSON"
if [[ $PARSE_STATUS -ne 0 ]]; then
rm -f "$PARSE_OUTPUT"
exit $PARSE_STATUS
fi
while IFS='=' read -r key pid_hex; do
[[ -z "$key" ]] && continue
if [[ "$key" == FAMILY:* ]]; then
family="${key#FAMILY:}"
case "$family" in
DInput) export BITDO_EXPECT_DINPUT_PID="$pid_hex" ;;
Standard64) export BITDO_EXPECT_STANDARD64_PID="$pid_hex" ;;
JpHandshake) export BITDO_EXPECT_JPHANDSHAKE_PID="$pid_hex" ;;
*)
echo "unsupported family in parsed lab config: $family" >&2
rm -f "$PARSE_OUTPUT"
exit 1
;;
esac
elif [[ "$key" == FIXTURE:* ]]; then
fixture="${key#FIXTURE:}"
case "$fixture" in
ultimate2) export BITDO_EXPECT_ULTIMATE2_PID="$pid_hex" ;;
108jp) export BITDO_EXPECT_108JP_PID="$pid_hex" ;;
*)
echo "unsupported fixture in parsed lab config: $fixture" >&2
rm -f "$PARSE_OUTPUT"
exit 1
;;
esac
fi
done <"$PARSE_OUTPUT"
rm -f "$PARSE_OUTPUT"
TEST_OUTPUT_FILE="$(mktemp)"
TEST_STATUS=0
run_test() {
local test_name="$1"
set +e
BITDO_HARDWARE=1 cargo test --workspace --test hardware_smoke -- --ignored --exact "$test_name" >>"$TEST_OUTPUT_FILE" 2>&1
local status=$?
set -e
if [[ $status -ne 0 ]]; then
TEST_STATUS=$status
fi
}
run_test "hardware_smoke_detect_devices"
case "$SUITE" in
family)
IFS=',' read -r -a FAMILY_LIST <<<"$REQUIRED_FAMILIES"
for family in "${FAMILY_LIST[@]}"; do
case "$family" in
DInput) run_test "hardware_smoke_dinput_family" ;;
Standard64) run_test "hardware_smoke_standard64_family" ;;
JpHandshake) run_test "hardware_smoke_jphandshake_family" ;;
*)
echo "unsupported required family for tests: $family" >>"$TEST_OUTPUT_FILE"
TEST_STATUS=1
;;
esac
done
;;
ultimate2)
run_test "hardware_smoke_ultimate2_core_ops"
;;
108jp)
run_test "hardware_smoke_108jp_dedicated_ops"
;;
*)
echo "unsupported suite: $SUITE" >>"$TEST_OUTPUT_FILE"
TEST_STATUS=1
;;
esac
python3 - <<'PY' "$REPORT_PATH" "$TEST_STATUS" "$TEST_OUTPUT_FILE" "$SUITE" "$REQUIRED_FAMILIES" "${BITDO_EXPECT_STANDARD64_PID:-}" "${BITDO_EXPECT_DINPUT_PID:-}" "${BITDO_EXPECT_JPHANDSHAKE_PID:-}" "${BITDO_EXPECT_ULTIMATE2_PID:-}" "${BITDO_EXPECT_108JP_PID:-}"
import json, sys, pathlib, datetime
report_path = pathlib.Path(sys.argv[1])
test_status = int(sys.argv[2])
output_file = pathlib.Path(sys.argv[3])
list_json_raw = sys.argv[4]
try:
devices = json.loads(list_json_raw)
except Exception:
devices = []
suite = sys.argv[4]
required_families = [x for x in sys.argv[5].split(",") if x]
expected_standard64 = sys.argv[6]
expected_dinput = sys.argv[7]
expected_jphandshake = sys.argv[8]
expected_ultimate2 = sys.argv[9]
expected_108jp = sys.argv[10]
report = {
"timestamp_utc": datetime.datetime.utcnow().isoformat() + "Z",
"suite": suite,
"test_status": test_status,
"tests_passed": test_status == 0,
"devices": devices,
"required_families": required_families,
"required_family_fixtures": {
"Standard64": expected_standard64,
"DInput": expected_dinput,
"JpHandshake": expected_jphandshake,
},
"required_device_fixtures": {
"ultimate2": expected_ultimate2,
"108jp": expected_108jp,
},
"raw_test_output": output_file.read_text(errors="replace"),
}
@@ -43,3 +240,4 @@ PY
rm -f "$TEST_OUTPUT_FILE"
echo "hardware smoke report written: $REPORT_PATH"
exit "$TEST_STATUS"

View File

@@ -0,0 +1,26 @@
use bitdo_proto::pid_registry;
use std::fs;
use std::path::PathBuf;
#[test]
fn alias_index_matches_unique_registry_policy() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let alias_path = manifest.join("../../../spec/alias_index.md");
let body = fs::read_to_string(alias_path).expect("read alias_index.md");
assert!(body.contains("PID_Pro2_OLD"));
assert!(body.contains("PID_Pro2"));
assert!(body.contains("0x6003"));
assert!(body.contains("PID_ASLGMouse"));
assert!(body.contains("PID_Mouse"));
assert!(body.contains("0x5205"));
let names = pid_registry()
.iter()
.map(|row| row.name)
.collect::<Vec<_>>();
assert!(names.contains(&"PID_Pro2"));
assert!(names.contains(&"PID_Mouse"));
assert!(!names.contains(&"PID_Pro2_OLD"));
assert!(!names.contains(&"PID_ASLGMouse"));
}

View File

@@ -0,0 +1,136 @@
use bitdo_proto::{
device_profile_for, BitdoError, DeviceSession, MockTransport, SessionConfig, SupportLevel,
SupportTier, VidPid,
};
const CANDIDATE_READONLY_PIDS: &[u16] = &[
0x6002, 0x6003, 0x3010, 0x3011, 0x3012, 0x3013, 0x5200, 0x5201, 0x203a, 0x2049, 0x2028, 0x202e,
0x3004, 0x3019, 0x3100, 0x3105, 0x2100, 0x2101, 0x901a, 0x6006, 0x5203, 0x5204, 0x301a, 0x9028,
0x3026, 0x3027,
];
#[test]
fn candidate_targets_are_candidate_readonly() {
for pid in CANDIDATE_READONLY_PIDS {
let profile = device_profile_for(VidPid::new(0x2dc8, *pid));
assert_eq!(
profile.support_tier,
SupportTier::CandidateReadOnly,
"expected candidate-readonly for pid={pid:#06x}"
);
assert_eq!(
profile.support_level,
SupportLevel::DetectOnly,
"support_level remains detect-only until full promotion"
);
}
}
#[test]
fn candidate_standard_pid_allows_diag_read_but_blocks_write_and_unsafe() {
let pid = 0x6002;
let mut transport = MockTransport::default();
// get_mode issues up to 3 reads; allow timeout outcome to prove it was permitted by policy.
transport.push_read_timeout();
transport.push_read_timeout();
transport.push_read_timeout();
let mut session = DeviceSession::new(
transport,
VidPid::new(0x2dc8, pid),
SessionConfig {
experimental: true,
..Default::default()
},
)
.expect("open session");
let mode_err = session
.get_mode()
.expect_err("candidate get_mode should execute and fail only at transport/response stage");
assert!(matches!(
mode_err,
BitdoError::Timeout | BitdoError::MalformedResponse { .. }
));
let write_err = session
.set_mode(1)
.expect_err("candidate safe-write must be blocked");
assert!(matches!(write_err, BitdoError::UnsupportedForPid { .. }));
let unsafe_err = session
.enter_bootloader()
.expect_err("candidate unsafe command must be blocked");
assert!(matches!(unsafe_err, BitdoError::UnsupportedForPid { .. }));
let _ = session.close();
}
#[test]
fn candidate_jp_pid_remains_diag_only() {
let pid = 0x5200;
let mut transport = MockTransport::default();
transport.push_read_data({
let mut response = vec![0u8; 64];
response[0] = 0x02;
response[1] = 0x05;
response[4] = 0xC1;
response
});
let mut session = DeviceSession::new(
transport,
VidPid::new(0x2dc8, pid),
SessionConfig {
experimental: true,
..Default::default()
},
)
.expect("open session");
let identify = session.identify().expect("identify allowed");
assert_eq!(identify.target.pid, pid);
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
assert_eq!(profile.support_tier, SupportTier::CandidateReadOnly);
let mode_err = session
.get_mode()
.expect_err("jp candidate should not expose mode read path");
assert!(matches!(mode_err, BitdoError::UnsupportedForPid { .. }));
let _ = session.close();
}
#[test]
fn wave2_candidate_standard_pid_allows_safe_reads_only() {
let pid = 0x3100;
let mut transport = MockTransport::default();
transport.push_read_timeout();
transport.push_read_timeout();
transport.push_read_timeout();
let mut session = DeviceSession::new(
transport,
VidPid::new(0x2dc8, pid),
SessionConfig {
experimental: true,
..Default::default()
},
)
.expect("open session");
let mode_err = session.get_mode().expect_err(
"wave2 candidate get_mode should be permitted and fail at transport/response stage",
);
assert!(matches!(
mode_err,
BitdoError::Timeout | BitdoError::MalformedResponse { .. }
));
let write_err = session
.set_mode(1)
.expect_err("wave2 candidate safe-write must be blocked");
assert!(matches!(write_err, BitdoError::UnsupportedForPid { .. }));
let _ = session.close();
}

View File

@@ -1,66 +0,0 @@
use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;
use std::fs;
#[test]
fn list_mock_text_snapshot() {
let mut cmd = cargo_bin_cmd!("bitdoctl");
cmd.args(["--mock", "list"])
.assert()
.success()
.stdout(predicate::str::contains("2dc8:6009"));
}
#[test]
fn identify_mock_json_snapshot() {
let mut cmd = cargo_bin_cmd!("bitdoctl");
cmd.args(["--mock", "--json", "--pid", "24585", "identify"])
.assert()
.success()
.stdout(predicate::str::contains("\"capability\""));
}
#[test]
fn mode_get_mock_json_snapshot() {
let mut cmd = cargo_bin_cmd!("bitdoctl");
cmd.args(["--mock", "--json", "--pid", "24585", "mode", "get"])
.assert()
.success()
.stdout(predicate::str::contains("\"mode\": 2"));
}
#[test]
fn diag_probe_mock_json_snapshot() {
let mut cmd = cargo_bin_cmd!("bitdoctl");
cmd.args(["--mock", "--json", "--pid", "24585", "diag", "probe"])
.assert()
.success()
.stdout(predicate::str::contains("\"command_checks\""));
}
#[test]
fn firmware_dry_run_snapshot() {
let tmp = std::env::temp_dir().join("bitdoctl-fw-test.bin");
fs::write(&tmp, vec![0xAA; 128]).expect("write temp fw");
let mut cmd = cargo_bin_cmd!("bitdoctl");
cmd.args([
"--mock",
"--json",
"--pid",
"24585",
"--unsafe",
"--i-understand-brick-risk",
"--experimental",
"fw",
"write",
"--file",
tmp.to_str().expect("path"),
"--dry-run",
])
.assert()
.success()
.stdout(predicate::str::contains("\"dry_run\": true"));
let _ = fs::remove_file(tmp);
}

View File

@@ -0,0 +1,59 @@
use bitdo_proto::{command_registry, CommandRuntimePolicy};
use std::fs;
use std::path::PathBuf;
#[test]
fn command_registry_matches_spec_rows_and_runtime_policy() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let csv_path = manifest.join("../../../spec/command_matrix.csv");
let content = fs::read_to_string(csv_path).expect("read command_matrix.csv");
let mut lines = content.lines();
let header = lines.next().expect("command matrix header");
let columns = header.split(',').collect::<Vec<_>>();
let idx_command = col_index(&columns, "command_id");
let idx_safety = col_index(&columns, "safety_class");
let idx_confidence = col_index(&columns, "confidence");
let spec_rows = content
.lines()
.skip(1)
.filter(|row| !row.trim().is_empty() && !row.starts_with("command_id,"))
.collect::<Vec<_>>();
assert_eq!(
spec_rows.len(),
command_registry().len(),
"command registry size mismatch vs command_matrix.csv"
);
for row in spec_rows {
let fields = row.split(',').collect::<Vec<_>>();
let command_name = fields[idx_command];
let safety = fields[idx_safety];
let confidence = fields[idx_confidence];
let reg = command_registry()
.iter()
.find(|entry| format!("{:?}", entry.id) == command_name)
.unwrap_or_else(|| panic!("missing command in registry: {command_name}"));
let expected_policy = match (confidence, safety) {
("confirmed", _) => CommandRuntimePolicy::EnabledDefault,
("inferred", "SafeRead") => CommandRuntimePolicy::ExperimentalGate,
("inferred", _) => CommandRuntimePolicy::BlockedUntilConfirmed,
other => panic!("unknown confidence/safety tuple: {other:?}"),
};
assert_eq!(
reg.runtime_policy(),
expected_policy,
"runtime policy mismatch for command={command_name}"
);
}
}
fn col_index(columns: &[&str], name: &str) -> usize {
columns
.iter()
.position(|c| *c == name)
.unwrap_or_else(|| panic!("missing column: {name}"))
}

View File

@@ -32,14 +32,27 @@ fn diag_probe_returns_command_checks() {
ver[4] = 1;
transport.push_read_data(ver);
let mut super_button = vec![0u8; 64];
super_button[0] = 0x02;
super_button[1] = 0x05;
transport.push_read_data(super_button);
let mut profile = vec![0u8; 64];
profile[0] = 0x02;
profile[1] = 0x05;
transport.push_read_data(profile);
let mut session = DeviceSession::new(
transport,
VidPid::new(0x2dc8, 24585),
SessionConfig::default(),
SessionConfig {
experimental: true,
..Default::default()
},
)
.expect("session init");
let diag = session.diag_probe();
assert_eq!(diag.command_checks.len(), 4);
assert_eq!(diag.command_checks.len(), 6);
assert!(diag.command_checks.iter().all(|c| c.ok));
}

View File

@@ -1,7 +1,7 @@
use bitdo_proto::{DeviceSession, MockTransport, SessionConfig, VidPid};
use bitdo_proto::{BitdoError, DeviceSession, MockTransport, SessionConfig, VidPid};
#[test]
fn firmware_transfer_chunks_and_commit() {
fn inferred_firmware_transfer_is_blocked_until_confirmed() {
let mut transport = MockTransport::default();
for _ in 0..4 {
transport.push_read_data(vec![0x02, 0x10, 0x00, 0x00]);
@@ -20,11 +20,11 @@ fn firmware_transfer_chunks_and_commit() {
.expect("session init");
let image = vec![0xAB; 120];
let report = session
let err = session
.firmware_transfer(&image, 50, false)
.expect("fw transfer");
assert_eq!(report.chunks_sent, 3);
.expect_err("inferred firmware chunk/commit must remain blocked");
assert!(matches!(err, BitdoError::UnsupportedForPid { .. }));
let transport = session.into_transport();
assert_eq!(transport.writes().len(), 4);
assert_eq!(transport.writes().len(), 0);
}

View File

@@ -1,8 +1,14 @@
use bitdo_proto::{command_registry, CommandFrame, CommandId, Report64};
use std::collections::HashSet;
#[test]
fn frame_encode_decode_roundtrip_for_all_commands() {
assert_eq!(command_registry().len(), CommandId::all().len());
let unique = command_registry()
.iter()
.map(|row| row.id)
.collect::<HashSet<_>>();
assert_eq!(unique.len(), CommandId::all().len());
assert!(command_registry().len() >= unique.len());
for row in command_registry() {
let frame = CommandFrame {

View File

@@ -1,23 +1,91 @@
use bitdo_proto::{device_profile_for, enumerate_hid_devices, ProtocolFamily, VidPid};
use bitdo_proto::{
device_profile_for, enumerate_hid_devices, DeviceSession, HidTransport, ProtocolFamily,
SessionConfig, VidPid,
};
fn hardware_enabled() -> bool {
std::env::var("BITDO_HARDWARE").ok().as_deref() == Some("1")
}
fn expected_pid(env_key: &str) -> Option<u16> {
std::env::var(env_key).ok().and_then(|v| {
let trimmed = v.trim();
if let Some(hex) = trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
{
u16::from_str_radix(hex, 16).ok()
} else {
trimmed.parse::<u16>().ok()
}
fn parse_pid(input: &str) -> Option<u16> {
let trimmed = input.trim();
if let Some(hex) = trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
{
u16::from_str_radix(hex, 16).ok()
} else {
trimmed.parse::<u16>().ok()
}
}
fn expected_pid(env_key: &str, family: &str) -> u16 {
let raw = std::env::var(env_key)
.unwrap_or_else(|_| panic!("missing required {env_key} for {family} family hardware gate"));
parse_pid(&raw).unwrap_or_else(|| {
panic!("invalid {env_key} value '{raw}' for {family} family hardware gate")
})
}
fn attached_8bitdo_pids() -> Vec<u16> {
enumerate_hid_devices()
.expect("enumeration")
.into_iter()
.filter(|d| d.vid_pid.vid == 0x2dc8)
.map(|d| d.vid_pid.pid)
.collect()
}
fn assert_family_fixture(env_key: &str, family: &str, expected_family: ProtocolFamily) {
if !hardware_enabled() {
return;
}
let pid = expected_pid(env_key, family);
let attached_pids = attached_8bitdo_pids();
assert!(
attached_pids.contains(&pid),
"missing fixture for {family}: expected attached pid={pid:#06x}, attached={:?}",
attached_pids
.iter()
.map(|value| format!("{value:#06x}"))
.collect::<Vec<_>>()
);
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
assert_eq!(
profile.protocol_family, expected_family,
"expected {family} family for pid={pid:#06x}, got {:?}",
profile.protocol_family
);
}
fn assert_named_fixture(env_key: &str, name: &str, expected_family: ProtocolFamily) -> u16 {
if !hardware_enabled() {
return 0;
}
let pid = expected_pid(env_key, name);
let attached_pids = attached_8bitdo_pids();
assert!(
attached_pids.contains(&pid),
"missing fixture for {name}: expected attached pid={pid:#06x}, attached={:?}",
attached_pids
.iter()
.map(|value| format!("{value:#06x}"))
.collect::<Vec<_>>()
);
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
assert_eq!(
profile.protocol_family, expected_family,
"expected {name} family {:?} for pid={pid:#06x}, got {:?}",
expected_family, profile.protocol_family
);
pid
}
#[test]
#[ignore = "requires lab hardware and BITDO_HARDWARE=1"]
fn hardware_smoke_detect_devices() {
@@ -36,61 +104,140 @@ fn hardware_smoke_detect_devices() {
}
#[test]
#[ignore = "optional family check; set BITDO_EXPECT_DINPUT_PID"]
#[ignore = "requires lab hardware and BITDO_EXPECT_DINPUT_PID"]
fn hardware_smoke_dinput_family() {
if !hardware_enabled() {
return;
}
let Some(pid) = expected_pid("BITDO_EXPECT_DINPUT_PID") else {
eprintln!("BITDO_EXPECT_DINPUT_PID not set, skipping DInput family check");
return;
};
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
assert_eq!(
profile.protocol_family,
ProtocolFamily::DInput,
"expected DInput family for pid={pid:#06x}, got {:?}",
profile.protocol_family
);
assert_family_fixture("BITDO_EXPECT_DINPUT_PID", "DInput", ProtocolFamily::DInput);
}
#[test]
#[ignore = "optional family check; set BITDO_EXPECT_STANDARD64_PID"]
#[ignore = "requires lab hardware and BITDO_EXPECT_STANDARD64_PID"]
fn hardware_smoke_standard64_family() {
if !hardware_enabled() {
return;
}
let Some(pid) = expected_pid("BITDO_EXPECT_STANDARD64_PID") else {
eprintln!("BITDO_EXPECT_STANDARD64_PID not set, skipping Standard64 family check");
return;
};
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
assert_eq!(
profile.protocol_family,
assert_family_fixture(
"BITDO_EXPECT_STANDARD64_PID",
"Standard64",
ProtocolFamily::Standard64,
"expected Standard64 family for pid={pid:#06x}, got {:?}",
profile.protocol_family
);
}
#[test]
#[ignore = "optional family check; set BITDO_EXPECT_JPHANDSHAKE_PID"]
#[ignore = "requires lab hardware and BITDO_EXPECT_JPHANDSHAKE_PID"]
fn hardware_smoke_jphandshake_family() {
assert_family_fixture(
"BITDO_EXPECT_JPHANDSHAKE_PID",
"JpHandshake",
ProtocolFamily::JpHandshake,
);
}
#[test]
#[ignore = "requires lab hardware and BITDO_EXPECT_ULTIMATE2_PID"]
fn hardware_smoke_ultimate2_core_ops() {
if !hardware_enabled() {
return;
}
let Some(pid) = expected_pid("BITDO_EXPECT_JPHANDSHAKE_PID") else {
eprintln!("BITDO_EXPECT_JPHANDSHAKE_PID not set, skipping JpHandshake family check");
return;
};
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
assert_eq!(
profile.protocol_family,
ProtocolFamily::JpHandshake,
"expected JpHandshake family for pid={pid:#06x}, got {:?}",
profile.protocol_family
let pid = assert_named_fixture(
"BITDO_EXPECT_ULTIMATE2_PID",
"Ultimate2",
ProtocolFamily::DInput,
);
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
assert!(profile.capability.supports_u2_slot_config);
assert!(profile.capability.supports_u2_button_map);
let mut session = DeviceSession::new(
HidTransport::new(),
VidPid::new(0x2dc8, pid),
SessionConfig {
experimental: true,
..Default::default()
},
)
.expect("open session");
let mode_before = session.get_mode().expect("read mode").mode;
session
.u2_set_mode(mode_before)
.expect("mode read/write/readback");
let mode_after = session.get_mode().expect("read mode after write").mode;
assert_eq!(mode_after, mode_before);
let slot = session.u2_get_current_slot().expect("read current slot");
let config_before = session.u2_read_config_slot(slot).expect("read config slot");
session
.u2_write_config_slot(slot, &config_before)
.expect("write config slot");
let config_after = session
.u2_read_config_slot(slot)
.expect("read config readback");
assert!(!config_after.is_empty());
let map_before = session.u2_read_button_map(slot).expect("read button map");
session
.u2_write_button_map(slot, &map_before)
.expect("write button map");
let map_after = session
.u2_read_button_map(slot)
.expect("read button map readback");
assert_eq!(map_before.len(), map_after.len());
// Firmware smoke is preflight-only in CI: dry_run avoids any transfer/write.
session
.firmware_transfer(&[0xAA; 128], 32, true)
.expect("firmware preflight dry-run");
let _ = session.close();
}
#[test]
#[ignore = "requires lab hardware and BITDO_EXPECT_108JP_PID"]
fn hardware_smoke_108jp_dedicated_ops() {
if !hardware_enabled() {
return;
}
let pid = assert_named_fixture(
"BITDO_EXPECT_108JP_PID",
"JP108",
ProtocolFamily::JpHandshake,
);
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
assert!(profile.capability.supports_jp108_dedicated_map);
let mut session = DeviceSession::new(
HidTransport::new(),
VidPid::new(0x2dc8, pid),
SessionConfig {
experimental: true,
..Default::default()
},
)
.expect("open session");
let mappings_before = session
.jp108_read_dedicated_mappings()
.expect("read dedicated mappings");
assert!(mappings_before.len() >= 3);
for idx in [0u8, 1u8, 2u8] {
let usage = mappings_before
.iter()
.find(|(entry_idx, _)| *entry_idx == idx)
.map(|(_, usage)| *usage)
.unwrap_or(0);
session
.jp108_write_dedicated_mapping(idx, usage)
.expect("write dedicated mapping");
}
let mappings_after = session
.jp108_read_dedicated_mappings()
.expect("read dedicated mappings readback");
assert!(mappings_after.len() >= 3);
session
.firmware_transfer(&[0xBB; 128], 32, true)
.expect("firmware preflight dry-run");
let _ = session.close();
}

View File

@@ -1,4 +1,5 @@
use bitdo_proto::pid_registry;
use bitdo_proto::{find_pid, pid_registry, ProtocolFamily, SupportLevel, SupportTier};
use std::collections::HashSet;
use std::fs;
use std::path::PathBuf;
@@ -7,10 +8,87 @@ fn pid_registry_matches_spec_rows() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let csv_path = manifest.join("../../../spec/pid_matrix.csv");
let content = fs::read_to_string(csv_path).expect("read pid_matrix.csv");
let rows = content
.lines()
.skip(1)
.filter(|l| !l.trim().is_empty())
.count();
let mut lines = content.lines();
let header = lines.next().expect("pid matrix header");
let columns = header.split(',').collect::<Vec<_>>();
let idx_name = col_index(&columns, "pid_name");
let idx_pid = col_index(&columns, "pid_hex");
let idx_level = col_index(&columns, "support_level");
let idx_tier = col_index(&columns, "support_tier");
let idx_family = col_index(&columns, "protocol_family");
let rows = lines.filter(|l| !l.trim().is_empty()).count();
assert_eq!(rows, pid_registry().len());
let mut seen = HashSet::new();
for row in content.lines().skip(1).filter(|l| !l.trim().is_empty()) {
let fields = row.split(',').collect::<Vec<_>>();
let name = fields[idx_name];
let pid_hex = fields[idx_pid];
let level = fields[idx_level];
let tier = fields[idx_tier];
let family = fields[idx_family];
let pid = parse_hex_u16(pid_hex);
assert!(
seen.insert(pid),
"duplicate PID found in pid_matrix.csv: {pid_hex} (name={name})"
);
let reg = find_pid(pid).unwrap_or_else(|| panic!("missing pid in registry: {pid_hex}"));
assert_eq!(reg.name, name, "name mismatch for pid={pid_hex}");
assert_eq!(
reg.support_level,
parse_support_level(level),
"support_level mismatch for pid={pid_hex}"
);
assert_eq!(
reg.support_tier,
parse_support_tier(tier),
"support_tier mismatch for pid={pid_hex}"
);
assert_eq!(
reg.protocol_family,
parse_family(family),
"protocol_family mismatch for pid={pid_hex}"
);
}
}
fn col_index(columns: &[&str], name: &str) -> usize {
columns
.iter()
.position(|c| *c == name)
.unwrap_or_else(|| panic!("missing column: {name}"))
}
fn parse_hex_u16(v: &str) -> u16 {
u16::from_str_radix(v.trim_start_matches("0x"), 16).expect("valid pid hex")
}
fn parse_support_level(v: &str) -> SupportLevel {
match v {
"full" => SupportLevel::Full,
"detect-only" => SupportLevel::DetectOnly,
other => panic!("unknown support_level: {other}"),
}
}
fn parse_support_tier(v: &str) -> SupportTier {
match v {
"full" => SupportTier::Full,
"candidate-readonly" => SupportTier::CandidateReadOnly,
"detect-only" => SupportTier::DetectOnly,
other => panic!("unknown support_tier: {other}"),
}
}
fn parse_family(v: &str) -> ProtocolFamily {
match v {
"Standard64" => ProtocolFamily::Standard64,
"JpHandshake" => ProtocolFamily::JpHandshake,
"DInput" => ProtocolFamily::DInput,
"DS4Boot" => ProtocolFamily::DS4Boot,
"Unknown" => ProtocolFamily::Unknown,
other => panic!("unknown protocol_family: {other}"),
}
}

View File

@@ -0,0 +1,15 @@
use bitdo_proto::pid_registry;
use std::collections::HashSet;
#[test]
fn pid_registry_contains_unique_pid_values() {
let mut seen = HashSet::new();
for row in pid_registry() {
assert!(
seen.insert(row.pid),
"duplicate pid in runtime registry: {:#06x} ({})",
row.pid,
row.name
);
}
}

View File

@@ -0,0 +1,78 @@
use bitdo_proto::{
find_command, BitdoError, CommandId, CommandRuntimePolicy, DeviceSession, DiagSeverity,
EvidenceConfidence, MockTransport, SessionConfig, VidPid,
};
#[test]
fn inferred_safe_read_requires_experimental_mode() {
let row = find_command(CommandId::GetSuperButton).expect("command present");
assert_eq!(row.runtime_policy(), CommandRuntimePolicy::ExperimentalGate);
let mut session = DeviceSession::new(
MockTransport::default(),
VidPid::new(0x2dc8, 0x6012),
SessionConfig::default(),
)
.expect("session opens");
let err = session
.send_command(CommandId::GetSuperButton, None)
.expect_err("experimental gate must deny inferred safe-read by default");
assert!(matches!(err, BitdoError::ExperimentalRequired { .. }));
}
#[test]
fn inferred_write_is_blocked_until_confirmed() {
let row = find_command(CommandId::WriteProfile).expect("command present");
assert_eq!(
row.runtime_policy(),
CommandRuntimePolicy::BlockedUntilConfirmed
);
let mut session = DeviceSession::new(
MockTransport::default(),
VidPid::new(0x2dc8, 0x6012),
SessionConfig {
experimental: true,
..Default::default()
},
)
.expect("session opens");
let err = session
.send_command(CommandId::WriteProfile, Some(&[1, 2, 3]))
.expect_err("inferred writes remain blocked even in experimental mode");
assert!(matches!(err, BitdoError::UnsupportedForPid { .. }));
}
#[test]
fn confirmed_read_remains_enabled_default() {
let row = find_command(CommandId::GetPid).expect("command present");
assert_eq!(row.runtime_policy(), CommandRuntimePolicy::EnabledDefault);
}
#[test]
fn diag_probe_marks_inferred_reads_as_experimental() {
let mut session = DeviceSession::new(
MockTransport::default(),
VidPid::new(0x2dc8, 0x6012),
SessionConfig {
experimental: true,
..Default::default()
},
)
.expect("session opens");
let diag = session.diag_probe();
let inferred = diag
.command_checks
.iter()
.find(|c| c.command == CommandId::GetSuperButton)
.expect("inferred check present");
assert!(inferred.is_experimental);
assert_eq!(inferred.confidence, EvidenceConfidence::Inferred);
assert!(matches!(
inferred.severity,
DiagSeverity::Ok | DiagSeverity::Warning | DiagSeverity::NeedsAttention
));
}

9
spec/alias_index.md Normal file
View File

@@ -0,0 +1,9 @@
# Alias Index
OpenBitdo uses a strict no-duplicate PID model in runtime and canonical spec tables.
These legacy names are preserved here as aliases only.
| Alias PID Name | Canonical PID Name | PID (hex) | Note |
| --- | --- | --- | --- |
| `PID_Pro2_OLD` | `PID_Pro2` | `0x6003` | Legacy constant alias in 8BitDo source; runtime/spec use only `PID_Pro2`. |
| `PID_ASLGMouse` | `PID_Mouse` | `0x5205` | Alias constant maps to the same PID as `PID_Mouse`; runtime/spec use only `PID_Mouse`. |

View File

@@ -1,18 +1,74 @@
command_id,safety_class,confidence,experimental_default,report_id,request_len,request_hex,expected_response,notes
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,"Primary PID detection request"
GetReportRevision,SafeRead,confirmed,false,0x81,64,81040001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x04;byte5=0x01,"RR read preflight"
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,"Mode read"
GetModeAlt,SafeRead,confirmed,false,0x81,64,81050800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,"Alternate mode read"
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,"Controller version"
GetSuperButton,SafeRead,inferred,true,0x81,64,81052100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,"Super button capability"
SetModeDInput,SafeWrite,confirmed,false,0x81,64,81050051020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,"Mode write to DInput"
Idle,SafeRead,confirmed,false,0x81,64,81040001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,"Idle check"
Version,SafeRead,confirmed,false,0x81,64,81042101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte1=0x22,"Version check"
ReadProfile,SafeRead,inferred,true,0x81,64,81060001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,"Profile slot read (sanitized)"
WriteProfile,SafeWrite,inferred,true,0x81,64,81070001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,"Profile slot write (sanitized)"
EnterBootloaderA,UnsafeBoot,confirmed,false,0x81,6,050050010000,none,"Boot stage A"
EnterBootloaderB,UnsafeBoot,confirmed,false,0x81,6,005100000000,none,"Boot stage B"
EnterBootloaderC,UnsafeBoot,confirmed,false,0x81,5,0050000000,none,"Boot stage C"
ExitBootloader,UnsafeBoot,inferred,true,0x81,6,050051010000,none,"Boot exit (sanitized inferred)"
FirmwareChunk,UnsafeFirmware,inferred,true,0x81,64,81100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,"Firmware chunk transfer"
FirmwareCommit,UnsafeFirmware,inferred,true,0x81,64,81110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,"Firmware commit"
command_id,safety_class,confidence,experimental_default,report_id,request_len,request_hex,expected_response,notes,applies_to,operation_group,dossier_id,evidence_static,evidence_runtime,evidence_hardware,promotion_gate
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Primary PID detection request,*,Core,DOS-CORE-GLOBAL,yes,yes,yes,n/a
GetReportRevision,SafeRead,confirmed,false,0x81,64,81040001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x04;byte5=0x01,RR read preflight,*,Core,DOS-CORE-GLOBAL,yes,yes,yes,n/a
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Mode read,*,Core,DOS-CORE-GLOBAL,yes,yes,yes,n/a
GetModeAlt,SafeRead,confirmed,false,0x81,64,81050800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Alternate mode read,*,Core,DOS-CORE-GLOBAL,yes,yes,yes,n/a
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Controller version,*,Core,DOS-CORE-GLOBAL,yes,yes,yes,n/a
GetSuperButton,SafeRead,inferred,true,0x81,64,81052100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Super button capability,*,Core,DOS-CORE-GLOBAL,yes,no,no,n/a
SetModeDInput,SafeWrite,confirmed,false,0x81,64,81050051020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,Mode write to DInput,*,Core,DOS-CORE-GLOBAL,yes,yes,yes,n/a
Idle,SafeRead,confirmed,false,0x81,64,81040001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,Idle check,*,Core,DOS-CORE-GLOBAL,yes,yes,yes,n/a
Version,SafeRead,confirmed,false,0x81,64,81042101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte1=0x22,Version check,*,Core,DOS-CORE-GLOBAL,yes,yes,yes,n/a
ReadProfile,SafeRead,inferred,true,0x81,64,81060001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,Profile slot read (sanitized),*,Core,DOS-CORE-GLOBAL,yes,no,no,n/a
WriteProfile,SafeWrite,inferred,true,0x81,64,81070001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,Profile slot write (sanitized),*,Core,DOS-CORE-GLOBAL,yes,no,no,n/a
EnterBootloaderA,UnsafeBoot,confirmed,false,0x81,6,050050010000,none,Boot stage A,*,Core,DOS-CORE-GLOBAL,yes,yes,yes,n/a
EnterBootloaderB,UnsafeBoot,confirmed,false,0x81,6,005100000000,none,Boot stage B,*,Core,DOS-CORE-GLOBAL,yes,yes,yes,n/a
EnterBootloaderC,UnsafeBoot,confirmed,false,0x81,5,0050000000,none,Boot stage C,*,Core,DOS-CORE-GLOBAL,yes,yes,yes,n/a
ExitBootloader,UnsafeBoot,inferred,true,0x81,6,050051010000,none,Boot exit (sanitized inferred),*,Core,DOS-CORE-GLOBAL,yes,no,no,n/a
FirmwareChunk,UnsafeFirmware,inferred,true,0x81,64,81100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,Firmware chunk transfer,*,Core,DOS-CORE-GLOBAL,yes,no,no,n/a
FirmwareCommit,UnsafeFirmware,inferred,true,0x81,64,81110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,Firmware commit,*,Core,DOS-CORE-GLOBAL,yes,no,no,n/a
Jp108ReadDedicatedMappings,SafeRead,inferred,true,0x81,64,81053020010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,JP108 dedicated-button mapping read,0x5209;0x520a,JP108Dedicated,DOS-CORE-GLOBAL,yes,no,no,n/a
Jp108WriteDedicatedMapping,SafeWrite,inferred,true,0x81,64,81053120010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,JP108 dedicated-button mapping write (index/value payload),0x5209;0x520a,JP108Dedicated,DOS-CORE-GLOBAL,yes,no,no,n/a
Jp108ReadFeatureFlags,SafeRead,inferred,true,0x81,64,81053220010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,JP108 feature flags read,0x5209;0x520a,JP108Dedicated,DOS-CORE-GLOBAL,yes,no,no,n/a
Jp108WriteFeatureFlags,SafeWrite,inferred,true,0x81,64,81053320010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,JP108 feature flags write,0x5209;0x520a,JP108Dedicated,DOS-CORE-GLOBAL,yes,no,no,n/a
Jp108ReadVoice,SafeRead,inferred,true,0x81,64,81053420010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,JP108 voice read,0x5209;0x520a,JP108Dedicated,DOS-CORE-GLOBAL,yes,no,no,n/a
Jp108WriteVoice,SafeWrite,inferred,true,0x81,64,81053520010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,JP108 voice write,0x5209;0x520a,JP108Dedicated,DOS-CORE-GLOBAL,yes,no,no,n/a
U2GetCurrentSlot,SafeRead,inferred,true,0x81,64,81054012010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Ultimate2 current slot read,0x6012;0x6013,Ultimate2Core,DOS-CORE-GLOBAL,yes,no,no,n/a
U2ReadConfigSlot,SafeRead,inferred,true,0x81,64,81054112010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Ultimate2 slot config read,0x6012;0x6013,Ultimate2Core,DOS-CORE-GLOBAL,yes,no,no,n/a
U2WriteConfigSlot,SafeWrite,inferred,true,0x81,64,81054212010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,Ultimate2 slot config write,0x6012;0x6013,Ultimate2Core,DOS-CORE-GLOBAL,yes,no,no,n/a
U2ReadButtonMap,SafeRead,inferred,true,0x81,64,81054312010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Ultimate2 core button map read,0x6012;0x6013,Ultimate2Core,DOS-CORE-GLOBAL,yes,no,no,n/a
U2WriteButtonMap,SafeWrite,inferred,true,0x81,64,81054412010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,Ultimate2 core button map write,0x6012;0x6013,Ultimate2Core,DOS-CORE-GLOBAL,yes,no,no,n/a
U2SetMode,SafeWrite,inferred,true,0x81,64,81054512010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,Ultimate2 mode write,0x6012;0x6013,Ultimate2Core,DOS-CORE-GLOBAL,yes,no,no,n/a
Jp108EnterBootloader,UnsafeBoot,inferred,true,0x81,6,050050010000,none,JP108 boot enter,0x5209;0x520a,Firmware,DOS-CORE-GLOBAL,yes,no,no,n/a
Jp108FirmwareChunk,UnsafeFirmware,inferred,true,0x81,64,81601020090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,JP108 firmware chunk,0x5209;0x520a,Firmware,DOS-CORE-GLOBAL,yes,no,no,n/a
Jp108FirmwareCommit,UnsafeFirmware,inferred,true,0x81,64,81601120090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,JP108 firmware commit,0x5209;0x520a,Firmware,DOS-CORE-GLOBAL,yes,no,no,n/a
Jp108ExitBootloader,UnsafeBoot,inferred,true,0x81,6,050051010000,none,JP108 boot exit,0x5209;0x520a,Firmware,DOS-CORE-GLOBAL,yes,no,no,n/a
U2EnterBootloader,UnsafeBoot,inferred,true,0x81,6,050050010000,none,Ultimate2 boot enter,0x6012;0x6013,Firmware,DOS-CORE-GLOBAL,yes,no,no,n/a
U2FirmwareChunk,UnsafeFirmware,inferred,true,0x81,64,81601060120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,Ultimate2 firmware chunk,0x6012;0x6013,Firmware,DOS-CORE-GLOBAL,yes,no,no,n/a
U2FirmwareCommit,UnsafeFirmware,inferred,true,0x81,64,81601160120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02,Ultimate2 firmware commit,0x6012;0x6013,Firmware,DOS-CORE-GLOBAL,yes,no,no,n/a
U2ExitBootloader,UnsafeBoot,inferred,true,0x81,6,050051010000,none,Ultimate2 boot exit,0x6012;0x6013,Firmware,DOS-CORE-GLOBAL,yes,no,no,n/a
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x3100,CoreDiag,DOS-3100-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x3100,ModeProfileRead,DOS-3100-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x3100,FirmwarePreflight,DOS-3100-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x3105,CoreDiag,DOS-3105-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x3105,ModeProfileRead,DOS-3105-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x3105,FirmwarePreflight,DOS-3105-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x2100,CoreDiag,DOS-2100-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x2100,ModeProfileRead,DOS-2100-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x2100,FirmwarePreflight,DOS-2100-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x2101,CoreDiag,DOS-2101-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x2101,ModeProfileRead,DOS-2101-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x2101,FirmwarePreflight,DOS-2101-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x901a,CoreDiag,DOS-901A-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x901a,ModeProfileRead,DOS-901A-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x901a,FirmwarePreflight,DOS-901A-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x6006,CoreDiag,DOS-6006-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x6006,ModeProfileRead,DOS-6006-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x6006,FirmwarePreflight,DOS-6006-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x5203,CoreDiag,DOS-5203-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x5203,ModeProfileRead,DOS-5203-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x5203,FirmwarePreflight,DOS-5203-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x5204,CoreDiag,DOS-5204-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x5204,ModeProfileRead,DOS-5204-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x5204,FirmwarePreflight,DOS-5204-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x301a,CoreDiag,DOS-301A-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x301a,ModeProfileRead,DOS-301A-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x301a,FirmwarePreflight,DOS-301A-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x9028,CoreDiag,DOS-9028-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x9028,ModeProfileRead,DOS-9028-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x9028,FirmwarePreflight,DOS-9028-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x3026,CoreDiag,DOS-3026-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x3026,ModeProfileRead,DOS-3026-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x3026,FirmwarePreflight,DOS-3026-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
GetPid,SafeRead,confirmed,false,0x81,64,8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05;byte4=0xC1,Wave2 static core diagnostics coverage for candidate-readonly PID,0x3027,CoreDiag,DOS-3027-CORE-DIAG-W2,yes,no,no,blocked/no_runtime
GetMode,SafeRead,confirmed,false,0x81,64,81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x05,Wave2 static mode/profile read coverage; read-only and gated,0x3027,ModeProfileRead,DOS-3027-MODEPROFILE-W2,yes,no,no,blocked/no_runtime
GetControllerVersion,SafeRead,confirmed,false,0x81,64,81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,byte0=0x02;byte1=0x22,Wave2 static firmware preflight metadata coverage; transfer blocked,0x3027,FirmwarePreflight,DOS-3027-FW-PREFLIGHT-W2,yes,no,no,blocked/no_runtime
1 command_id safety_class confidence experimental_default report_id request_len request_hex expected_response notes applies_to operation_group dossier_id evidence_static evidence_runtime evidence_hardware promotion_gate
2 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Primary PID detection request * Core DOS-CORE-GLOBAL yes yes yes n/a
3 GetReportRevision SafeRead confirmed false 0x81 64 81040001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x04;byte5=0x01 RR read preflight * Core DOS-CORE-GLOBAL yes yes yes n/a
4 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Mode read * Core DOS-CORE-GLOBAL yes yes yes n/a
5 GetModeAlt SafeRead confirmed false 0x81 64 81050800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Alternate mode read * Core DOS-CORE-GLOBAL yes yes yes n/a
6 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Controller version * Core DOS-CORE-GLOBAL yes yes yes n/a
7 GetSuperButton SafeRead inferred true 0x81 64 81052100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Super button capability * Core DOS-CORE-GLOBAL yes no no n/a
8 SetModeDInput SafeWrite confirmed false 0x81 64 81050051020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 Mode write to DInput * Core DOS-CORE-GLOBAL yes yes yes n/a
9 Idle SafeRead confirmed false 0x81 64 81040001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 Idle check * Core DOS-CORE-GLOBAL yes yes yes n/a
10 Version SafeRead confirmed false 0x81 64 81042101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte1=0x22 Version check * Core DOS-CORE-GLOBAL yes yes yes n/a
11 ReadProfile SafeRead inferred true 0x81 64 81060001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 Profile slot read (sanitized) * Core DOS-CORE-GLOBAL yes no no n/a
12 WriteProfile SafeWrite inferred true 0x81 64 81070001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 Profile slot write (sanitized) * Core DOS-CORE-GLOBAL yes no no n/a
13 EnterBootloaderA UnsafeBoot confirmed false 0x81 6 050050010000 none Boot stage A * Core DOS-CORE-GLOBAL yes yes yes n/a
14 EnterBootloaderB UnsafeBoot confirmed false 0x81 6 005100000000 none Boot stage B * Core DOS-CORE-GLOBAL yes yes yes n/a
15 EnterBootloaderC UnsafeBoot confirmed false 0x81 5 0050000000 none Boot stage C * Core DOS-CORE-GLOBAL yes yes yes n/a
16 ExitBootloader UnsafeBoot inferred true 0x81 6 050051010000 none Boot exit (sanitized inferred) * Core DOS-CORE-GLOBAL yes no no n/a
17 FirmwareChunk UnsafeFirmware inferred true 0x81 64 81100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 Firmware chunk transfer * Core DOS-CORE-GLOBAL yes no no n/a
18 FirmwareCommit UnsafeFirmware inferred true 0x81 64 81110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 Firmware commit * Core DOS-CORE-GLOBAL yes no no n/a
19 Jp108ReadDedicatedMappings SafeRead inferred true 0x81 64 81053020010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 JP108 dedicated-button mapping read 0x5209;0x520a JP108Dedicated DOS-CORE-GLOBAL yes no no n/a
20 Jp108WriteDedicatedMapping SafeWrite inferred true 0x81 64 81053120010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 JP108 dedicated-button mapping write (index/value payload) 0x5209;0x520a JP108Dedicated DOS-CORE-GLOBAL yes no no n/a
21 Jp108ReadFeatureFlags SafeRead inferred true 0x81 64 81053220010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 JP108 feature flags read 0x5209;0x520a JP108Dedicated DOS-CORE-GLOBAL yes no no n/a
22 Jp108WriteFeatureFlags SafeWrite inferred true 0x81 64 81053320010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 JP108 feature flags write 0x5209;0x520a JP108Dedicated DOS-CORE-GLOBAL yes no no n/a
23 Jp108ReadVoice SafeRead inferred true 0x81 64 81053420010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 JP108 voice read 0x5209;0x520a JP108Dedicated DOS-CORE-GLOBAL yes no no n/a
24 Jp108WriteVoice SafeWrite inferred true 0x81 64 81053520010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 JP108 voice write 0x5209;0x520a JP108Dedicated DOS-CORE-GLOBAL yes no no n/a
25 U2GetCurrentSlot SafeRead inferred true 0x81 64 81054012010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Ultimate2 current slot read 0x6012;0x6013 Ultimate2Core DOS-CORE-GLOBAL yes no no n/a
26 U2ReadConfigSlot SafeRead inferred true 0x81 64 81054112010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Ultimate2 slot config read 0x6012;0x6013 Ultimate2Core DOS-CORE-GLOBAL yes no no n/a
27 U2WriteConfigSlot SafeWrite inferred true 0x81 64 81054212010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 Ultimate2 slot config write 0x6012;0x6013 Ultimate2Core DOS-CORE-GLOBAL yes no no n/a
28 U2ReadButtonMap SafeRead inferred true 0x81 64 81054312010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Ultimate2 core button map read 0x6012;0x6013 Ultimate2Core DOS-CORE-GLOBAL yes no no n/a
29 U2WriteButtonMap SafeWrite inferred true 0x81 64 81054412010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 Ultimate2 core button map write 0x6012;0x6013 Ultimate2Core DOS-CORE-GLOBAL yes no no n/a
30 U2SetMode SafeWrite inferred true 0x81 64 81054512010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 Ultimate2 mode write 0x6012;0x6013 Ultimate2Core DOS-CORE-GLOBAL yes no no n/a
31 Jp108EnterBootloader UnsafeBoot inferred true 0x81 6 050050010000 none JP108 boot enter 0x5209;0x520a Firmware DOS-CORE-GLOBAL yes no no n/a
32 Jp108FirmwareChunk UnsafeFirmware inferred true 0x81 64 81601020090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 JP108 firmware chunk 0x5209;0x520a Firmware DOS-CORE-GLOBAL yes no no n/a
33 Jp108FirmwareCommit UnsafeFirmware inferred true 0x81 64 81601120090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 JP108 firmware commit 0x5209;0x520a Firmware DOS-CORE-GLOBAL yes no no n/a
34 Jp108ExitBootloader UnsafeBoot inferred true 0x81 6 050051010000 none JP108 boot exit 0x5209;0x520a Firmware DOS-CORE-GLOBAL yes no no n/a
35 U2EnterBootloader UnsafeBoot inferred true 0x81 6 050050010000 none Ultimate2 boot enter 0x6012;0x6013 Firmware DOS-CORE-GLOBAL yes no no n/a
36 U2FirmwareChunk UnsafeFirmware inferred true 0x81 64 81601060120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 Ultimate2 firmware chunk 0x6012;0x6013 Firmware DOS-CORE-GLOBAL yes no no n/a
37 U2FirmwareCommit UnsafeFirmware inferred true 0x81 64 81601160120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02 Ultimate2 firmware commit 0x6012;0x6013 Firmware DOS-CORE-GLOBAL yes no no n/a
38 U2ExitBootloader UnsafeBoot inferred true 0x81 6 050051010000 none Ultimate2 boot exit 0x6012;0x6013 Firmware DOS-CORE-GLOBAL yes no no n/a
39 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x3100 CoreDiag DOS-3100-CORE-DIAG-W2 yes no no blocked/no_runtime
40 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x3100 ModeProfileRead DOS-3100-MODEPROFILE-W2 yes no no blocked/no_runtime
41 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x3100 FirmwarePreflight DOS-3100-FW-PREFLIGHT-W2 yes no no blocked/no_runtime
42 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x3105 CoreDiag DOS-3105-CORE-DIAG-W2 yes no no blocked/no_runtime
43 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x3105 ModeProfileRead DOS-3105-MODEPROFILE-W2 yes no no blocked/no_runtime
44 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x3105 FirmwarePreflight DOS-3105-FW-PREFLIGHT-W2 yes no no blocked/no_runtime
45 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x2100 CoreDiag DOS-2100-CORE-DIAG-W2 yes no no blocked/no_runtime
46 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x2100 ModeProfileRead DOS-2100-MODEPROFILE-W2 yes no no blocked/no_runtime
47 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x2100 FirmwarePreflight DOS-2100-FW-PREFLIGHT-W2 yes no no blocked/no_runtime
48 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x2101 CoreDiag DOS-2101-CORE-DIAG-W2 yes no no blocked/no_runtime
49 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x2101 ModeProfileRead DOS-2101-MODEPROFILE-W2 yes no no blocked/no_runtime
50 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x2101 FirmwarePreflight DOS-2101-FW-PREFLIGHT-W2 yes no no blocked/no_runtime
51 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x901a CoreDiag DOS-901A-CORE-DIAG-W2 yes no no blocked/no_runtime
52 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x901a ModeProfileRead DOS-901A-MODEPROFILE-W2 yes no no blocked/no_runtime
53 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x901a FirmwarePreflight DOS-901A-FW-PREFLIGHT-W2 yes no no blocked/no_runtime
54 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x6006 CoreDiag DOS-6006-CORE-DIAG-W2 yes no no blocked/no_runtime
55 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x6006 ModeProfileRead DOS-6006-MODEPROFILE-W2 yes no no blocked/no_runtime
56 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x6006 FirmwarePreflight DOS-6006-FW-PREFLIGHT-W2 yes no no blocked/no_runtime
57 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x5203 CoreDiag DOS-5203-CORE-DIAG-W2 yes no no blocked/no_runtime
58 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x5203 ModeProfileRead DOS-5203-MODEPROFILE-W2 yes no no blocked/no_runtime
59 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x5203 FirmwarePreflight DOS-5203-FW-PREFLIGHT-W2 yes no no blocked/no_runtime
60 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x5204 CoreDiag DOS-5204-CORE-DIAG-W2 yes no no blocked/no_runtime
61 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x5204 ModeProfileRead DOS-5204-MODEPROFILE-W2 yes no no blocked/no_runtime
62 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x5204 FirmwarePreflight DOS-5204-FW-PREFLIGHT-W2 yes no no blocked/no_runtime
63 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x301a CoreDiag DOS-301A-CORE-DIAG-W2 yes no no blocked/no_runtime
64 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x301a ModeProfileRead DOS-301A-MODEPROFILE-W2 yes no no blocked/no_runtime
65 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x301a FirmwarePreflight DOS-301A-FW-PREFLIGHT-W2 yes no no blocked/no_runtime
66 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x9028 CoreDiag DOS-9028-CORE-DIAG-W2 yes no no blocked/no_runtime
67 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x9028 ModeProfileRead DOS-9028-MODEPROFILE-W2 yes no no blocked/no_runtime
68 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x9028 FirmwarePreflight DOS-9028-FW-PREFLIGHT-W2 yes no no blocked/no_runtime
69 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x3026 CoreDiag DOS-3026-CORE-DIAG-W2 yes no no blocked/no_runtime
70 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x3026 ModeProfileRead DOS-3026-MODEPROFILE-W2 yes no no blocked/no_runtime
71 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x3026 FirmwarePreflight DOS-3026-FW-PREFLIGHT-W2 yes no no blocked/no_runtime
72 GetPid SafeRead confirmed false 0x81 64 8105c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05;byte4=0xC1 Wave2 static core diagnostics coverage for candidate-readonly PID 0x3027 CoreDiag DOS-3027-CORE-DIAG-W2 yes no no blocked/no_runtime
73 GetMode SafeRead confirmed false 0x81 64 81040501000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x05 Wave2 static mode/profile read coverage; read-only and gated 0x3027 ModeProfileRead DOS-3027-MODEPROFILE-W2 yes no no blocked/no_runtime
74 GetControllerVersion SafeRead confirmed false 0x81 64 81042101000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 byte0=0x02;byte1=0x22 Wave2 static firmware preflight metadata coverage; transfer blocked 0x3027 FirmwarePreflight DOS-3027-FW-PREFLIGHT-W2 yes no no blocked/no_runtime

View File

@@ -0,0 +1,68 @@
# Device Name Catalog
This catalog is the sanitized naming source of truth for OpenBitdo runtime/docs.
Canonical rows are unique by PID (no duplicates in this table).
| canonical_pid_name | pid_hex | display_name_en | protocol_family | name_source | source_confidence | aliases |
| --- | --- | --- | --- | --- | --- | --- |
| PID_None | 0x0000 | No Device (Sentinel) | Unknown | internal-fallback | low | |
| PID_IDLE | 0x3109 | Unconfirmed Internal Device (PID_IDLE) | Standard64 | internal-fallback | low | |
| PID_SN30Plus | 0x6002 | SN30 Pro+ | Standard64 | official-web | medium | |
| PID_USB_Ultimate | 0x3100 | Unconfirmed Internal Device (PID_USB_Ultimate) | Standard64 | internal-fallback | low | |
| PID_USB_Ultimate2 | 0x3105 | Unconfirmed Internal Device (PID_USB_Ultimate2) | Standard64 | internal-fallback | low | |
| PID_USB_UltimateClasses | 0x3104 | Unconfirmed Internal Device (PID_USB_UltimateClasses) | Standard64 | internal-fallback | low | |
| PID_Xcloud | 0x2100 | Unconfirmed Internal Device (PID_Xcloud) | Standard64 | internal-fallback | low | |
| PID_Xcloud2 | 0x2101 | Unconfirmed Internal Device (PID_Xcloud2) | Standard64 | internal-fallback | low | |
| PID_ArcadeStick | 0x901a | Arcade Stick | Standard64 | internal-fallback | medium | |
| PID_Pro2 | 0x6003 | Pro 2 Bluetooth Controller | Standard64 | official-web | high | PID_Pro2_OLD |
| PID_Pro2_CY | 0x6006 | Unconfirmed Variant Name (PID_Pro2_CY) | Standard64 | internal-fallback | low | |
| PID_Pro2_Wired | 0x3010 | Pro 2 Wired Controller | Standard64 | internal-fallback | medium | |
| PID_Ultimate_PC | 0x3011 | Ultimate PC Controller | Standard64 | internal-fallback | medium | |
| PID_Ultimate2_4 | 0x3012 | Ultimate 2.4G Controller | Standard64 | internal-fallback | medium | |
| PID_Ultimate2_4RR | 0x3013 | Ultimate 2.4G Adapter | Standard64 | internal-fallback | medium | |
| PID_UltimateBT | 0x6007 | Ultimate Wireless Controller | Standard64 | vendor-language-map | high | |
| PID_UltimateBTRR | 0x3106 | Ultimate Wireless Adapter | Standard64 | internal-fallback | medium | |
| PID_JP | 0x5200 | Retro Mechanical Keyboard | JpHandshake | vendor-language-map | high | |
| PID_JPUSB | 0x5201 | Retro Mechanical Keyboard Receiver | JpHandshake | vendor-language-map | high | |
| PID_NUMPAD | 0x5203 | Retro 18 Mechanical Numpad | Standard64 | vendor-language-map | high | |
| PID_NUMPADRR | 0x5204 | Retro 18 Adapter | Standard64 | vendor-language-map | high | |
| PID_QINGCHUN2 | 0x310a | Ultimate 2C Controller | DInput | official-web | high | |
| PID_QINGCHUN2RR | 0x301c | Ultimate 2C Wireless Adapter | DInput | vendor-language-map | high | |
| PID_Xinput | 0x310b | Unconfirmed Interface Name (PID_Xinput) | DInput | internal-fallback | low | |
| PID_Pro3 | 0x6009 | Pro 3 Bluetooth Gamepad | DInput | vendor-language-map | high | |
| PID_Pro3USB | 0x600a | Pro 3 Bluetooth Adapter | DInput | vendor-language-map | high | |
| PID_Pro3DOCK | 0x600d | Charging Dock for Pro 3S Gamepad | Standard64 | vendor-language-map | high | |
| PID_108JP | 0x5209 | Retro 108 Mechanical Keyboard | JpHandshake | official-web | high | |
| PID_108JPUSB | 0x520a | Retro 108 Mechanical Adapter | JpHandshake | vendor-language-map | high | |
| PID_XBOXJP | 0x2028 | Retro 87 Mechanical Keyboard - Xbox Edition | JpHandshake | official-web | high | |
| PID_XBOXJPUSB | 0x202e | Retro 87 Mechanical Keyboard Adapter - Xbox Edition | JpHandshake | vendor-language-map | high | |
| PID_NGCDIY | 0x5750 | Mod Kit for NGC Controller | Standard64 | vendor-language-map | high | |
| PID_NGCRR | 0x902a | Retro Receiver for NGC | Standard64 | vendor-language-map | high | |
| PID_Ultimate2 | 0x6012 | Ultimate 2 Wireless Controller | DInput | official-web | high | |
| PID_Ultimate2RR | 0x6013 | Ultimate 2 Wireless Adapter | DInput | vendor-language-map | high | |
| PID_UltimateBT2 | 0x600f | Ultimate 2 Bluetooth Controller | DInput | official-web | high | |
| PID_UltimateBT2RR | 0x6011 | Ultimate 2 Bluetooth Adapter | DInput | vendor-language-map | high | |
| PID_Mouse | 0x5205 | Retro R8 Mouse | Standard64 | official-web | high | PID_ASLGMouse |
| PID_MouseRR | 0x5206 | Retro R8 Adapter | Standard64 | vendor-language-map | high | |
| PID_SaturnRR | 0x902b | Retro Receiver for Saturn | Standard64 | vendor-language-map | high | |
| PID_UltimateBT2C | 0x301a | Ultimate 2C Bluetooth Controller | Standard64 | official-web | high | |
| PID_Lashen | 0x301e | Ultimate Mobile Gaming Controller | Standard64 | vendor-language-map | high | |
| PID_HitBox | 0x600b | Arcade Controller | DInput | official-web | high | |
| PID_HitBoxRR | 0x600c | Arcade Controller Adapter | DInput | vendor-language-map | high | |
| PID_N64BT | 0x3019 | 64 Bluetooth Controller | Standard64 | official-web | high | |
| PID_N64 | 0x3004 | 64 2.4G Wireless Controller | Standard64 | vendor-language-map | high | |
| PID_N64RR | 0x9028 | Retro Receiver for N64 | Standard64 | vendor-language-map | high | |
| PID_XBOXUK | 0x3026 | Retro 87 Mechanical Keyboard - Xbox (UK) | Standard64 | vendor-language-map | high | |
| PID_XBOXUKUSB | 0x3027 | Retro 87 Mechanical Keyboard Adapter - Xbox (UK) | Standard64 | vendor-language-map | high | |
| PID_LashenX | 0x200b | Ultimate Mobile Gaming Controller For Xbox | Standard64 | vendor-language-map | high | |
| PID_68JP | 0x203a | Retro 68 Keyboard - N40 | JpHandshake | vendor-language-map | high | |
| PID_68JPUSB | 0x2049 | Retro 68 Keyboard Adapter - N40 | JpHandshake | vendor-language-map | high | |
| PID_N64JoySticks | 0x3021 | Joystick v2 for N64 Controller | Standard64 | vendor-language-map | high | |
| PID_DoubleSuper | 0x203e | Wireless Dual Super Button | Standard64 | vendor-language-map | high | |
| PID_Cube2RR | 0x2056 | Retro Cube 2 Adapter - N Edition | Standard64 | vendor-language-map | high | |
| PID_Cube2 | 0x2039 | Retro Cube 2 Speaker - N Edition | Standard64 | vendor-language-map | high | |
| PID_ASLGJP | 0x205a | Riviera Keyboard | JpHandshake | vendor-language-map | high | |
## Notes
- Name-source references are indexed in `/Users/brooklyn/data/8bitdo/cleanroom/process/device_name_sources.md`.
- Alias names are documented in `/Users/brooklyn/data/8bitdo/cleanroom/spec/alias_index.md` and intentionally excluded from canonical PID rows.

View File

@@ -0,0 +1,16 @@
# Sanitized static dirty-room dossier
dossier_id = "DOS-2028-CORE"
pid_hex = "0x2028"
operation_group = "CoreDiag"
command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"]
request_shape = "64-byte HID report with fixed command marker bytes and JP handshake gating"
response_shape = "status header plus minimal payload"
validator_rules = ["byte0 == 0x02", "response length >= 4", "status byte must indicate success"]
retry_behavior = "retry timeout/malformed response up to configured attempts with bounded backoff"
failure_signatures = ["timeout", "malformed response", "unsupported for pid"]
evidence_source = "static"
confidence = "inferred"
requirement_ids = ["REQ-DR-001", "REQ-DR-002", "REQ-PROM-001", "REQ-PROM-002", "REQ-PID-002"]
class_family = "JpHandshake dispatch"
notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet"

View File

@@ -0,0 +1,16 @@
# Sanitized static dirty-room dossier
dossier_id = "DOS-2028-FIRMWARE"
pid_hex = "0x2028"
operation_group = "Firmware"
command_id = ["EnterBootloaderA", "EnterBootloaderB", "EnterBootloaderC", "FirmwareChunk", "FirmwareCommit", "ExitBootloader"]
request_shape = "bootloader entry sequence followed by chunked firmware transfer frames"
response_shape = "ack/status oriented responses with transfer progression"
validator_rules = ["boot transition acknowledged", "chunk response indicates acceptance", "commit response indicates completion"]
retry_behavior = "firmware retries are bounded; unsafe flows remain blocked for candidate-readonly"
failure_signatures = ["boot transition failure", "chunk ack mismatch", "commit timeout"]
evidence_source = "static"
confidence = "inferred"
requirement_ids = ["REQ-DR-001", "REQ-DR-002", "REQ-PROM-001", "REQ-PROM-002"]
class_family = "Firmware update dispatch"
notes = "Firmware path is intentionally blocked for candidate-readonly support tier in this wave"

View File

@@ -0,0 +1,16 @@
# Sanitized static dirty-room dossier
dossier_id = "DOS-202E-CORE"
pid_hex = "0x202e"
operation_group = "CoreDiag"
command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"]
request_shape = "64-byte HID report with fixed command marker bytes and JP handshake gating"
response_shape = "status header plus minimal payload"
validator_rules = ["byte0 == 0x02", "response length >= 4", "status byte must indicate success"]
retry_behavior = "retry timeout/malformed response up to configured attempts with bounded backoff"
failure_signatures = ["timeout", "malformed response", "unsupported for pid"]
evidence_source = "static"
confidence = "inferred"
requirement_ids = ["REQ-DR-001", "REQ-DR-002", "REQ-PROM-001", "REQ-PROM-002", "REQ-PID-002"]
class_family = "JpHandshake dispatch"
notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet"

View File

@@ -0,0 +1,16 @@
# Sanitized static dirty-room dossier
dossier_id = "DOS-202E-FIRMWARE"
pid_hex = "0x202e"
operation_group = "Firmware"
command_id = ["EnterBootloaderA", "EnterBootloaderB", "EnterBootloaderC", "FirmwareChunk", "FirmwareCommit", "ExitBootloader"]
request_shape = "bootloader entry sequence followed by chunked firmware transfer frames"
response_shape = "ack/status oriented responses with transfer progression"
validator_rules = ["boot transition acknowledged", "chunk response indicates acceptance", "commit response indicates completion"]
retry_behavior = "firmware retries are bounded; unsafe flows remain blocked for candidate-readonly"
failure_signatures = ["boot transition failure", "chunk ack mismatch", "commit timeout"]
evidence_source = "static"
confidence = "inferred"
requirement_ids = ["REQ-DR-001", "REQ-DR-002", "REQ-PROM-001", "REQ-PROM-002"]
class_family = "Firmware update dispatch"
notes = "Firmware path is intentionally blocked for candidate-readonly support tier in this wave"

View File

@@ -0,0 +1,16 @@
# Sanitized static dirty-room dossier
dossier_id = "DOS-2039-CORE"
pid_hex = "0x2039"
operation_group = "CoreDiag"
command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"]
request_shape = "64-byte HID report with command marker bytes"
response_shape = "status header plus optional payload"
validator_rules = ["byte0 == 0x02", "response length >= 4"]
retry_behavior = "retry timeout/malformed response up to configured attempts with bounded backoff"
failure_signatures = ["timeout", "malformed response", "unsupported for pid"]
evidence_source = "static"
confidence = "inferred"
requirement_ids = ["REQ-DR-001", "REQ-DR-002", "REQ-PROM-001", "REQ-PROM-002", "REQ-PID-002"]
class_family = "Peripheral/Specialty dispatch"
notes = "Stretch target dossier; candidate-readonly until runtime+hardware evidence"

View File

@@ -0,0 +1,16 @@
# Sanitized static dirty-room dossier
dossier_id = "DOS-203A-CORE"
pid_hex = "0x203a"
operation_group = "CoreDiag"
command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"]
request_shape = "64-byte HID report with fixed command marker bytes and JP handshake gating"
response_shape = "status header plus minimal payload"
validator_rules = ["byte0 == 0x02", "response length >= 4", "status byte must indicate success"]
retry_behavior = "retry timeout/malformed response up to configured attempts with bounded backoff"
failure_signatures = ["timeout", "malformed response", "unsupported for pid"]
evidence_source = "static"
confidence = "inferred"
requirement_ids = ["REQ-DR-001", "REQ-DR-002", "REQ-PROM-001", "REQ-PROM-002", "REQ-PID-002"]
class_family = "JpHandshake dispatch"
notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet"

View File

@@ -0,0 +1,16 @@
# Sanitized static dirty-room dossier
dossier_id = "DOS-203A-FIRMWARE"
pid_hex = "0x203a"
operation_group = "Firmware"
command_id = ["EnterBootloaderA", "EnterBootloaderB", "EnterBootloaderC", "FirmwareChunk", "FirmwareCommit", "ExitBootloader"]
request_shape = "bootloader entry sequence followed by chunked firmware transfer frames"
response_shape = "ack/status oriented responses with transfer progression"
validator_rules = ["boot transition acknowledged", "chunk response indicates acceptance", "commit response indicates completion"]
retry_behavior = "firmware retries are bounded; unsafe flows remain blocked for candidate-readonly"
failure_signatures = ["boot transition failure", "chunk ack mismatch", "commit timeout"]
evidence_source = "static"
confidence = "inferred"
requirement_ids = ["REQ-DR-001", "REQ-DR-002", "REQ-PROM-001", "REQ-PROM-002"]
class_family = "Firmware update dispatch"
notes = "Firmware path is intentionally blocked for candidate-readonly support tier in this wave"

View File

@@ -0,0 +1,16 @@
# Sanitized static dirty-room dossier
dossier_id = "DOS-2049-CORE"
pid_hex = "0x2049"
operation_group = "CoreDiag"
command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"]
request_shape = "64-byte HID report with fixed command marker bytes and JP handshake gating"
response_shape = "status header plus minimal payload"
validator_rules = ["byte0 == 0x02", "response length >= 4", "status byte must indicate success"]
retry_behavior = "retry timeout/malformed response up to configured attempts with bounded backoff"
failure_signatures = ["timeout", "malformed response", "unsupported for pid"]
evidence_source = "static"
confidence = "inferred"
requirement_ids = ["REQ-DR-001", "REQ-DR-002", "REQ-PROM-001", "REQ-PROM-002", "REQ-PID-002"]
class_family = "JpHandshake dispatch"
notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet"

View File

@@ -0,0 +1,16 @@
# Sanitized static dirty-room dossier
dossier_id = "DOS-2049-FIRMWARE"
pid_hex = "0x2049"
operation_group = "Firmware"
command_id = ["EnterBootloaderA", "EnterBootloaderB", "EnterBootloaderC", "FirmwareChunk", "FirmwareCommit", "ExitBootloader"]
request_shape = "bootloader entry sequence followed by chunked firmware transfer frames"
response_shape = "ack/status oriented responses with transfer progression"
validator_rules = ["boot transition acknowledged", "chunk response indicates acceptance", "commit response indicates completion"]
retry_behavior = "firmware retries are bounded; unsafe flows remain blocked for candidate-readonly"
failure_signatures = ["boot transition failure", "chunk ack mismatch", "commit timeout"]
evidence_source = "static"
confidence = "inferred"
requirement_ids = ["REQ-DR-001", "REQ-DR-002", "REQ-PROM-001", "REQ-PROM-002"]
class_family = "Firmware update dispatch"
notes = "Firmware path is intentionally blocked for candidate-readonly support tier in this wave"

View File

@@ -0,0 +1,16 @@
# Sanitized static dirty-room dossier
dossier_id = "DOS-2056-CORE"
pid_hex = "0x2056"
operation_group = "CoreDiag"
command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"]
request_shape = "64-byte HID report with command marker bytes"
response_shape = "status header plus optional payload"
validator_rules = ["byte0 == 0x02", "response length >= 4"]
retry_behavior = "retry timeout/malformed response up to configured attempts with bounded backoff"
failure_signatures = ["timeout", "malformed response", "unsupported for pid"]
evidence_source = "static"
confidence = "inferred"
requirement_ids = ["REQ-DR-001", "REQ-DR-002", "REQ-PROM-001", "REQ-PROM-002", "REQ-PID-002"]
class_family = "Peripheral/Specialty dispatch"
notes = "Stretch target dossier; candidate-readonly until runtime+hardware evidence"

Some files were not shown because too many files have changed in this diff Show More