mirror of
https://github.com/bybrooklyn/openbitdo.git
synced 2026-03-19 04:12:56 -04:00
release prep: rc.1 baseline and gating updates
This commit is contained in:
59
.github/ISSUE_TEMPLATE/dirtyroom-evidence.yml
vendored
Normal file
59
.github/ISSUE_TEMPLATE/dirtyroom-evidence.yml
vendored
Normal 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.
|
||||
54
.github/ISSUE_TEMPLATE/hardware-report.yml
vendored
Normal file
54
.github/ISSUE_TEMPLATE/hardware-report.yml
vendored
Normal 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
|
||||
73
.github/ISSUE_TEMPLATE/release-blocker.yml
vendored
Normal file
73
.github/ISSUE_TEMPLATE/release-blocker.yml
vendored
Normal 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
130
.github/workflows/aur-publish.yml
vendored
Normal 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
|
||||
203
.github/workflows/ci.yml
vendored
203
.github/workflows/ci.yml
vendored
@@ -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
287
.github/workflows/release.yml
vendored
Normal 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
12
.gitignore
vendored
@@ -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
24
CHANGELOG.md
Normal 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
29
LICENSE
Normal 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
54
MIGRATION.md
Normal 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
139
RC_CHECKLIST.md
Normal 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
169
README.md
@@ -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)).
|
||||
|
||||
4
harness/golden/jp108/read_mapping_roundtrip.toml
Normal file
4
harness/golden/jp108/read_mapping_roundtrip.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
operation = "jp108_read_dedicated_mapping"
|
||||
pid = "0x5209"
|
||||
status = "sanitized-placeholder"
|
||||
notes = "Trace placeholder for clean-room replay harness"
|
||||
4
harness/golden/jp108/write_mapping_apply.toml
Normal file
4
harness/golden/jp108/write_mapping_apply.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
operation = "jp108_write_dedicated_mapping"
|
||||
pid = "0x520a"
|
||||
status = "sanitized-placeholder"
|
||||
notes = "Trace placeholder for clean-room replay harness"
|
||||
4
harness/golden/ultimate2/read_profile_slot.toml
Normal file
4
harness/golden/ultimate2/read_profile_slot.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
operation = "u2_read_core_profile"
|
||||
pid = "0x6012"
|
||||
status = "sanitized-placeholder"
|
||||
notes = "Trace placeholder for clean-room replay harness"
|
||||
4
harness/golden/ultimate2/write_profile_slot.toml
Normal file
4
harness/golden/ultimate2/write_profile_slot.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
operation = "u2_write_core_profile"
|
||||
pid = "0x6013"
|
||||
status = "sanitized-placeholder"
|
||||
notes = "Trace placeholder for clean-room replay harness"
|
||||
@@ -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
19
packaging/aur/README.md
Normal 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`
|
||||
15
packaging/aur/openbitdo-bin/.SRCINFO
Normal file
15
packaging/aur/openbitdo-bin/.SRCINFO
Normal 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
|
||||
26
packaging/aur/openbitdo-bin/PKGBUILD
Normal file
26
packaging/aur/openbitdo-bin/PKGBUILD
Normal 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"
|
||||
}
|
||||
26
packaging/aur/openbitdo-bin/PKGBUILD.tmpl
Normal file
26
packaging/aur/openbitdo-bin/PKGBUILD.tmpl
Normal 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"
|
||||
}
|
||||
14
packaging/aur/openbitdo/.SRCINFO
Normal file
14
packaging/aur/openbitdo/.SRCINFO
Normal 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
|
||||
24
packaging/aur/openbitdo/PKGBUILD
Normal file
24
packaging/aur/openbitdo/PKGBUILD
Normal 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"
|
||||
}
|
||||
24
packaging/aur/openbitdo/PKGBUILD.tmpl
Normal file
24
packaging/aur/openbitdo/PKGBUILD.tmpl
Normal 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"
|
||||
}
|
||||
33
packaging/homebrew/Formula/openbitdo.rb
Normal file
33
packaging/homebrew/Formula/openbitdo.rb
Normal 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
|
||||
31
packaging/homebrew/Formula/openbitdo.rb.tmpl
Normal file
31
packaging/homebrew/Formula/openbitdo.rb.tmpl
Normal 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
|
||||
14
packaging/homebrew/README.md
Normal file
14
packaging/homebrew/README.md
Normal 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
36
packaging/homebrew/sync_tap.sh
Executable 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
|
||||
111
packaging/scripts/render_release_metadata.sh
Executable file
111
packaging/scripts/render_release_metadata.sh
Executable 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}"
|
||||
68
process/add_device_guide.md
Normal file
68
process/add_device_guide.md
Normal 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`
|
||||
17
process/commenting_standard.md
Normal file
17
process/commenting_standard.md
Normal 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.
|
||||
30
process/community_evidence_intake.md
Normal file
30
process/community_evidence_intake.md
Normal 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.
|
||||
33
process/device_name_sources.md
Normal file
33
process/device_name_sources.md
Normal 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_*)`
|
||||
60
process/dirtyroom_collection_playbook.md
Normal file
60
process/dirtyroom_collection_playbook.md
Normal 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.
|
||||
56
process/dirtyroom_dossier_schema.md
Normal file
56
process/dirtyroom_dossier_schema.md
Normal 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"]
|
||||
```
|
||||
50
process/dirtyroom_evidence_backlog.md
Normal file
50
process/dirtyroom_evidence_backlog.md
Normal 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.
|
||||
6
process/release_scope_gate.toml
Normal file
6
process/release_scope_gate.toml
Normal 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
23
process/wave1_baseline.md
Normal 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
23
process/wave1_results.md
Normal 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
32
process/wave2_baseline.md
Normal 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.
|
||||
22
process/wave2_pid_scorecard.md
Normal file
22
process/wave2_pid_scorecard.md
Normal 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
23
process/wave2_results.md
Normal 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.
|
||||
38
process/wave2_runtime_intake.md
Normal file
38
process/wave2_runtime_intake.md
Normal 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
2383
sdk/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"] }
|
||||
|
||||
102
sdk/README.md
102
sdk/README.md
@@ -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`)
|
||||
|
||||
24
sdk/crates/bitdo_app_core/Cargo.toml
Normal file
24
sdk/crates/bitdo_app_core/Cargo.toml
Normal 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"] }
|
||||
2060
sdk/crates/bitdo_app_core/src/lib.rs
Normal file
2060
sdk/crates/bitdo_app_core/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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] {
|
||||
|
||||
82
sdk/crates/bitdo_proto/src/command_registry_table.rs
Normal file
82
sdk/crates/bitdo_proto/src/command_registry_table.rs
Normal 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" },
|
||||
]
|
||||
;
|
||||
@@ -1,5 +1,3 @@
|
||||
#![cfg(feature = "hidapi-backend")]
|
||||
|
||||
use crate::error::{BitdoError, Result};
|
||||
use crate::transport::Transport;
|
||||
use crate::types::VidPid;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
68
sdk/crates/bitdo_proto/src/pid_registry_table.rs
Normal file
68
sdk/crates/bitdo_proto/src/pid_registry_table.rs
Normal 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 },
|
||||
]
|
||||
;
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
19
sdk/crates/bitdo_tui/Cargo.toml
Normal file
19
sdk/crates/bitdo_tui/Cargo.toml
Normal 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"] }
|
||||
69
sdk/crates/bitdo_tui/src/desktop_io.rs
Normal file
69
sdk/crates/bitdo_tui/src/desktop_io.rs
Normal 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}"))
|
||||
}
|
||||
}
|
||||
3151
sdk/crates/bitdo_tui/src/lib.rs
Normal file
3151
sdk/crates/bitdo_tui/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
38
sdk/crates/bitdo_tui/src/settings.rs
Normal file
38
sdk/crates/bitdo_tui/src/settings.rs
Normal 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(())
|
||||
}
|
||||
217
sdk/crates/bitdo_tui/src/support_report.rs
Normal file
217
sdk/crates/bitdo_tui/src/support_report.rs
Normal 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)
|
||||
}
|
||||
370
sdk/crates/bitdo_tui/src/tests.rs
Normal file
370
sdk/crates/bitdo_tui/src/tests.rs
Normal 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());
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
21
sdk/crates/openbitdo/Cargo.toml
Normal file
21
sdk/crates/openbitdo/Cargo.toml
Normal 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"
|
||||
42
sdk/crates/openbitdo/build.rs
Normal file
42
sdk/crates/openbitdo/build.rs
Normal 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())
|
||||
}
|
||||
218
sdk/crates/openbitdo/src/lib.rs
Normal file
218
sdk/crates/openbitdo/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
58
sdk/crates/openbitdo/src/main.rs
Normal file
58
sdk/crates/openbitdo/src/main.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
13
sdk/crates/openbitdo/tests/cli_smoke.rs
Normal file
13
sdk/crates/openbitdo/tests/cli_smoke.rs
Normal 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());
|
||||
}
|
||||
@@ -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
68
sdk/scripts/package-linux.sh
Executable 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
77
sdk/scripts/package-macos.sh
Executable 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"
|
||||
@@ -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"
|
||||
|
||||
26
sdk/tests/alias_index_integrity.rs
Normal file
26
sdk/tests/alias_index_integrity.rs
Normal 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"));
|
||||
}
|
||||
136
sdk/tests/candidate_readonly_gating.rs
Normal file
136
sdk/tests/candidate_readonly_gating.rs
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
59
sdk/tests/command_matrix_coverage.rs
Normal file
59
sdk/tests/command_matrix_coverage.rs
Normal 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}"))
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
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();
|
||||
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"))
|
||||
@@ -15,9 +17,75 @@ fn expected_pid(env_key: &str) -> Option<u16> {
|
||||
} 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();
|
||||
}
|
||||
|
||||
@@ -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}"),
|
||||
}
|
||||
}
|
||||
|
||||
15
sdk/tests/pid_registry_unique.rs
Normal file
15
sdk/tests/pid_registry_unique.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
78
sdk/tests/runtime_policy.rs
Normal file
78
sdk/tests/runtime_policy.rs
Normal 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
9
spec/alias_index.md
Normal 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`. |
|
||||
@@ -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
|
||||
|
||||
|
68
spec/device_name_catalog.md
Normal file
68
spec/device_name_catalog.md
Normal 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.
|
||||
16
spec/dossiers/2028/core_diag.toml
Normal file
16
spec/dossiers/2028/core_diag.toml
Normal 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"
|
||||
16
spec/dossiers/2028/firmware.toml
Normal file
16
spec/dossiers/2028/firmware.toml
Normal 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"
|
||||
16
spec/dossiers/202e/core_diag.toml
Normal file
16
spec/dossiers/202e/core_diag.toml
Normal 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"
|
||||
16
spec/dossiers/202e/firmware.toml
Normal file
16
spec/dossiers/202e/firmware.toml
Normal 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"
|
||||
16
spec/dossiers/2039/core_diag.toml
Normal file
16
spec/dossiers/2039/core_diag.toml
Normal 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"
|
||||
16
spec/dossiers/203a/core_diag.toml
Normal file
16
spec/dossiers/203a/core_diag.toml
Normal 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"
|
||||
16
spec/dossiers/203a/firmware.toml
Normal file
16
spec/dossiers/203a/firmware.toml
Normal 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"
|
||||
16
spec/dossiers/2049/core_diag.toml
Normal file
16
spec/dossiers/2049/core_diag.toml
Normal 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"
|
||||
16
spec/dossiers/2049/firmware.toml
Normal file
16
spec/dossiers/2049/firmware.toml
Normal 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"
|
||||
16
spec/dossiers/2056/core_diag.toml
Normal file
16
spec/dossiers/2056/core_diag.toml
Normal 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
Reference in New Issue
Block a user