From f43b2b24b6fc24a04fc616c3b8720b480dcc208a Mon Sep 17 00:00:00 2001 From: bybrooklyn Date: Mon, 2 Mar 2026 15:54:55 -0500 Subject: [PATCH] release prep: rc.1 baseline and gating updates --- .github/ISSUE_TEMPLATE/dirtyroom-evidence.yml | 59 + .github/ISSUE_TEMPLATE/hardware-report.yml | 54 + .github/ISSUE_TEMPLATE/release-blocker.yml | 73 + .github/workflows/aur-publish.yml | 130 + .github/workflows/ci.yml | 203 +- .github/workflows/release.yml | 287 ++ .gitignore | 12 + CHANGELOG.md | 24 + LICENSE | 29 + MIGRATION.md | 54 + RC_CHECKLIST.md | 139 + README.md | 169 +- VERSION | 1 + .../golden/jp108/read_mapping_roundtrip.toml | 4 + harness/golden/jp108/write_mapping_apply.toml | 4 + .../golden/ultimate2/read_profile_slot.toml | 4 + .../golden/ultimate2/write_profile_slot.toml | 4 + harness/lab/device_lab.yaml | 30 +- packaging/aur/README.md | 19 + packaging/aur/openbitdo-bin/.SRCINFO | 15 + packaging/aur/openbitdo-bin/PKGBUILD | 26 + packaging/aur/openbitdo-bin/PKGBUILD.tmpl | 26 + packaging/aur/openbitdo/.SRCINFO | 14 + packaging/aur/openbitdo/PKGBUILD | 24 + packaging/aur/openbitdo/PKGBUILD.tmpl | 24 + packaging/homebrew/Formula/openbitdo.rb | 33 + packaging/homebrew/Formula/openbitdo.rb.tmpl | 31 + packaging/homebrew/README.md | 14 + packaging/homebrew/sync_tap.sh | 36 + packaging/scripts/render_release_metadata.sh | 111 + process/add_device_guide.md | 68 + process/commenting_standard.md | 17 + process/community_evidence_intake.md | 30 + process/device_name_sources.md | 33 + process/dirtyroom_collection_playbook.md | 60 + process/dirtyroom_dossier_schema.md | 56 + process/dirtyroom_evidence_backlog.md | 50 + process/release_scope_gate.toml | 6 + process/wave1_baseline.md | 23 + process/wave1_results.md | 23 + process/wave2_baseline.md | 32 + process/wave2_pid_scorecard.md | 22 + process/wave2_results.md | 23 + process/wave2_runtime_intake.md | 38 + sdk/Cargo.lock | 2383 ++++++++++++- sdk/Cargo.toml | 18 +- sdk/README.md | 102 +- sdk/crates/bitdo_app_core/Cargo.toml | 24 + sdk/crates/bitdo_app_core/src/lib.rs | 2060 +++++++++++ sdk/crates/bitdo_proto/Cargo.toml | 18 +- sdk/crates/bitdo_proto/build.rs | 121 - sdk/crates/bitdo_proto/src/command.rs | 42 +- .../bitdo_proto/src/command_registry_table.rs | 82 + sdk/crates/bitdo_proto/src/hid_transport.rs | 2 - sdk/crates/bitdo_proto/src/lib.rs | 7 +- .../bitdo_proto/src/pid_registry_table.rs | 68 + sdk/crates/bitdo_proto/src/registry.rs | 189 +- sdk/crates/bitdo_proto/src/session.rs | 380 +- sdk/crates/bitdo_proto/src/types.rs | 35 + sdk/crates/bitdo_tui/Cargo.toml | 19 + sdk/crates/bitdo_tui/src/desktop_io.rs | 69 + sdk/crates/bitdo_tui/src/lib.rs | 3151 +++++++++++++++++ sdk/crates/bitdo_tui/src/settings.rs | 38 + sdk/crates/bitdo_tui/src/support_report.rs | 217 ++ sdk/crates/bitdo_tui/src/tests.rs | 370 ++ sdk/crates/bitdoctl/Cargo.toml | 20 - sdk/crates/bitdoctl/src/main.rs | 518 --- sdk/crates/openbitdo/Cargo.toml | 21 + sdk/crates/openbitdo/build.rs | 42 + sdk/crates/openbitdo/src/lib.rs | 218 ++ sdk/crates/openbitdo/src/main.rs | 58 + sdk/crates/openbitdo/tests/cli_smoke.rs | 13 + sdk/scripts/cleanroom_guard.sh | 4 +- sdk/scripts/package-linux.sh | 68 + sdk/scripts/package-macos.sh | 77 + sdk/scripts/run_hardware_smoke.sh | 222 +- sdk/tests/alias_index_integrity.rs | 26 + sdk/tests/candidate_readonly_gating.rs | 136 + sdk/tests/cli_snapshot.rs | 66 - sdk/tests/command_matrix_coverage.rs | 59 + sdk/tests/diag_probe.rs | 17 +- sdk/tests/firmware_chunk.rs | 12 +- sdk/tests/frame_roundtrip.rs | 8 +- sdk/tests/hardware_smoke.rs | 253 +- sdk/tests/pid_matrix_coverage.rs | 90 +- sdk/tests/pid_registry_unique.rs | 15 + sdk/tests/runtime_policy.rs | 78 + spec/alias_index.md | 9 + spec/command_matrix.csv | 92 +- spec/device_name_catalog.md | 68 + spec/dossiers/2028/core_diag.toml | 16 + spec/dossiers/2028/firmware.toml | 16 + spec/dossiers/202e/core_diag.toml | 16 + spec/dossiers/202e/firmware.toml | 16 + spec/dossiers/2039/core_diag.toml | 16 + spec/dossiers/203a/core_diag.toml | 16 + spec/dossiers/203a/firmware.toml | 16 + spec/dossiers/2049/core_diag.toml | 16 + spec/dossiers/2049/firmware.toml | 16 + spec/dossiers/2056/core_diag.toml | 16 + spec/dossiers/2100/core_diag.toml | 30 + spec/dossiers/2100/firmware_preflight.toml | 30 + spec/dossiers/2100/mode_or_profile_read.toml | 30 + spec/dossiers/2101/core_diag.toml | 30 + spec/dossiers/2101/firmware_preflight.toml | 30 + spec/dossiers/2101/mode_or_profile_read.toml | 30 + spec/dossiers/3004/core_diag.toml | 16 + spec/dossiers/3004/firmware.toml | 16 + spec/dossiers/3010/core_diag.toml | 16 + spec/dossiers/3010/firmware.toml | 16 + spec/dossiers/3011/core_diag.toml | 16 + spec/dossiers/3011/firmware.toml | 16 + spec/dossiers/3012/core_diag.toml | 16 + spec/dossiers/3012/firmware.toml | 16 + spec/dossiers/3013/core_diag.toml | 16 + spec/dossiers/3013/firmware.toml | 16 + spec/dossiers/3019/core_diag.toml | 16 + spec/dossiers/3019/firmware.toml | 16 + spec/dossiers/301a/core_diag.toml | 30 + spec/dossiers/301a/firmware_preflight.toml | 30 + spec/dossiers/301a/mode_or_profile_read.toml | 30 + spec/dossiers/3021/core_diag.toml | 16 + spec/dossiers/3026/core_diag.toml | 30 + spec/dossiers/3026/firmware_preflight.toml | 30 + spec/dossiers/3026/mode_or_profile_read.toml | 30 + spec/dossiers/3027/core_diag.toml | 30 + spec/dossiers/3027/firmware_preflight.toml | 30 + spec/dossiers/3027/mode_or_profile_read.toml | 30 + spec/dossiers/3100/core_diag.toml | 30 + spec/dossiers/3100/firmware_preflight.toml | 30 + spec/dossiers/3100/mode_or_profile_read.toml | 30 + spec/dossiers/3105/core_diag.toml | 30 + spec/dossiers/3105/firmware_preflight.toml | 30 + spec/dossiers/3105/mode_or_profile_read.toml | 30 + spec/dossiers/5200/core_diag.toml | 16 + spec/dossiers/5200/firmware.toml | 16 + spec/dossiers/5201/core_diag.toml | 16 + spec/dossiers/5201/firmware.toml | 16 + spec/dossiers/5203/core_diag.toml | 30 + spec/dossiers/5203/firmware_preflight.toml | 30 + spec/dossiers/5203/mode_or_profile_read.toml | 30 + spec/dossiers/5204/core_diag.toml | 30 + spec/dossiers/5204/firmware_preflight.toml | 30 + spec/dossiers/5204/mode_or_profile_read.toml | 30 + spec/dossiers/5205/core_diag.toml | 16 + spec/dossiers/5206/core_diag.toml | 16 + spec/dossiers/5209/firmware_core.toml | 16 + spec/dossiers/5209/jp108_dedicated.toml | 16 + spec/dossiers/6002/core_diag.toml | 16 + spec/dossiers/6002/firmware.toml | 16 + spec/dossiers/6003/core_diag.toml | 16 + spec/dossiers/6003/firmware.toml | 16 + spec/dossiers/6006/core_diag.toml | 30 + spec/dossiers/6006/firmware_preflight.toml | 30 + spec/dossiers/6006/mode_or_profile_read.toml | 30 + spec/dossiers/6012/firmware_core.toml | 16 + spec/dossiers/6012/u2_core.toml | 16 + spec/dossiers/901a/core_diag.toml | 30 + spec/dossiers/901a/firmware_preflight.toml | 30 + spec/dossiers/901a/mode_or_profile_read.toml | 30 + spec/dossiers/9028/core_diag.toml | 30 + spec/dossiers/9028/firmware_preflight.toml | 30 + spec/dossiers/9028/mode_or_profile_read.toml | 30 + spec/dossiers/global/core.toml | 16 + spec/evidence_index.csv | 27 + spec/pid_matrix.csv | 118 +- spec/protocol_spec.md | 56 + spec/requirements.yaml | 82 +- 168 files changed, 14708 insertions(+), 982 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/dirtyroom-evidence.yml create mode 100644 .github/ISSUE_TEMPLATE/hardware-report.yml create mode 100644 .github/ISSUE_TEMPLATE/release-blocker.yml create mode 100644 .github/workflows/aur-publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 MIGRATION.md create mode 100644 RC_CHECKLIST.md create mode 100644 VERSION create mode 100644 harness/golden/jp108/read_mapping_roundtrip.toml create mode 100644 harness/golden/jp108/write_mapping_apply.toml create mode 100644 harness/golden/ultimate2/read_profile_slot.toml create mode 100644 harness/golden/ultimate2/write_profile_slot.toml create mode 100644 packaging/aur/README.md create mode 100644 packaging/aur/openbitdo-bin/.SRCINFO create mode 100644 packaging/aur/openbitdo-bin/PKGBUILD create mode 100644 packaging/aur/openbitdo-bin/PKGBUILD.tmpl create mode 100644 packaging/aur/openbitdo/.SRCINFO create mode 100644 packaging/aur/openbitdo/PKGBUILD create mode 100644 packaging/aur/openbitdo/PKGBUILD.tmpl create mode 100644 packaging/homebrew/Formula/openbitdo.rb create mode 100644 packaging/homebrew/Formula/openbitdo.rb.tmpl create mode 100644 packaging/homebrew/README.md create mode 100755 packaging/homebrew/sync_tap.sh create mode 100755 packaging/scripts/render_release_metadata.sh create mode 100644 process/add_device_guide.md create mode 100644 process/commenting_standard.md create mode 100644 process/community_evidence_intake.md create mode 100644 process/device_name_sources.md create mode 100644 process/dirtyroom_collection_playbook.md create mode 100644 process/dirtyroom_dossier_schema.md create mode 100644 process/dirtyroom_evidence_backlog.md create mode 100644 process/release_scope_gate.toml create mode 100644 process/wave1_baseline.md create mode 100644 process/wave1_results.md create mode 100644 process/wave2_baseline.md create mode 100644 process/wave2_pid_scorecard.md create mode 100644 process/wave2_results.md create mode 100644 process/wave2_runtime_intake.md create mode 100644 sdk/crates/bitdo_app_core/Cargo.toml create mode 100644 sdk/crates/bitdo_app_core/src/lib.rs delete mode 100644 sdk/crates/bitdo_proto/build.rs create mode 100644 sdk/crates/bitdo_proto/src/command_registry_table.rs create mode 100644 sdk/crates/bitdo_proto/src/pid_registry_table.rs create mode 100644 sdk/crates/bitdo_tui/Cargo.toml create mode 100644 sdk/crates/bitdo_tui/src/desktop_io.rs create mode 100644 sdk/crates/bitdo_tui/src/lib.rs create mode 100644 sdk/crates/bitdo_tui/src/settings.rs create mode 100644 sdk/crates/bitdo_tui/src/support_report.rs create mode 100644 sdk/crates/bitdo_tui/src/tests.rs delete mode 100644 sdk/crates/bitdoctl/Cargo.toml delete mode 100644 sdk/crates/bitdoctl/src/main.rs create mode 100644 sdk/crates/openbitdo/Cargo.toml create mode 100644 sdk/crates/openbitdo/build.rs create mode 100644 sdk/crates/openbitdo/src/lib.rs create mode 100644 sdk/crates/openbitdo/src/main.rs create mode 100644 sdk/crates/openbitdo/tests/cli_smoke.rs create mode 100755 sdk/scripts/package-linux.sh create mode 100755 sdk/scripts/package-macos.sh create mode 100644 sdk/tests/alias_index_integrity.rs create mode 100644 sdk/tests/candidate_readonly_gating.rs delete mode 100644 sdk/tests/cli_snapshot.rs create mode 100644 sdk/tests/command_matrix_coverage.rs create mode 100644 sdk/tests/pid_registry_unique.rs create mode 100644 sdk/tests/runtime_policy.rs create mode 100644 spec/alias_index.md create mode 100644 spec/device_name_catalog.md create mode 100644 spec/dossiers/2028/core_diag.toml create mode 100644 spec/dossiers/2028/firmware.toml create mode 100644 spec/dossiers/202e/core_diag.toml create mode 100644 spec/dossiers/202e/firmware.toml create mode 100644 spec/dossiers/2039/core_diag.toml create mode 100644 spec/dossiers/203a/core_diag.toml create mode 100644 spec/dossiers/203a/firmware.toml create mode 100644 spec/dossiers/2049/core_diag.toml create mode 100644 spec/dossiers/2049/firmware.toml create mode 100644 spec/dossiers/2056/core_diag.toml create mode 100644 spec/dossiers/2100/core_diag.toml create mode 100644 spec/dossiers/2100/firmware_preflight.toml create mode 100644 spec/dossiers/2100/mode_or_profile_read.toml create mode 100644 spec/dossiers/2101/core_diag.toml create mode 100644 spec/dossiers/2101/firmware_preflight.toml create mode 100644 spec/dossiers/2101/mode_or_profile_read.toml create mode 100644 spec/dossiers/3004/core_diag.toml create mode 100644 spec/dossiers/3004/firmware.toml create mode 100644 spec/dossiers/3010/core_diag.toml create mode 100644 spec/dossiers/3010/firmware.toml create mode 100644 spec/dossiers/3011/core_diag.toml create mode 100644 spec/dossiers/3011/firmware.toml create mode 100644 spec/dossiers/3012/core_diag.toml create mode 100644 spec/dossiers/3012/firmware.toml create mode 100644 spec/dossiers/3013/core_diag.toml create mode 100644 spec/dossiers/3013/firmware.toml create mode 100644 spec/dossiers/3019/core_diag.toml create mode 100644 spec/dossiers/3019/firmware.toml create mode 100644 spec/dossiers/301a/core_diag.toml create mode 100644 spec/dossiers/301a/firmware_preflight.toml create mode 100644 spec/dossiers/301a/mode_or_profile_read.toml create mode 100644 spec/dossiers/3021/core_diag.toml create mode 100644 spec/dossiers/3026/core_diag.toml create mode 100644 spec/dossiers/3026/firmware_preflight.toml create mode 100644 spec/dossiers/3026/mode_or_profile_read.toml create mode 100644 spec/dossiers/3027/core_diag.toml create mode 100644 spec/dossiers/3027/firmware_preflight.toml create mode 100644 spec/dossiers/3027/mode_or_profile_read.toml create mode 100644 spec/dossiers/3100/core_diag.toml create mode 100644 spec/dossiers/3100/firmware_preflight.toml create mode 100644 spec/dossiers/3100/mode_or_profile_read.toml create mode 100644 spec/dossiers/3105/core_diag.toml create mode 100644 spec/dossiers/3105/firmware_preflight.toml create mode 100644 spec/dossiers/3105/mode_or_profile_read.toml create mode 100644 spec/dossiers/5200/core_diag.toml create mode 100644 spec/dossiers/5200/firmware.toml create mode 100644 spec/dossiers/5201/core_diag.toml create mode 100644 spec/dossiers/5201/firmware.toml create mode 100644 spec/dossiers/5203/core_diag.toml create mode 100644 spec/dossiers/5203/firmware_preflight.toml create mode 100644 spec/dossiers/5203/mode_or_profile_read.toml create mode 100644 spec/dossiers/5204/core_diag.toml create mode 100644 spec/dossiers/5204/firmware_preflight.toml create mode 100644 spec/dossiers/5204/mode_or_profile_read.toml create mode 100644 spec/dossiers/5205/core_diag.toml create mode 100644 spec/dossiers/5206/core_diag.toml create mode 100644 spec/dossiers/5209/firmware_core.toml create mode 100644 spec/dossiers/5209/jp108_dedicated.toml create mode 100644 spec/dossiers/6002/core_diag.toml create mode 100644 spec/dossiers/6002/firmware.toml create mode 100644 spec/dossiers/6003/core_diag.toml create mode 100644 spec/dossiers/6003/firmware.toml create mode 100644 spec/dossiers/6006/core_diag.toml create mode 100644 spec/dossiers/6006/firmware_preflight.toml create mode 100644 spec/dossiers/6006/mode_or_profile_read.toml create mode 100644 spec/dossiers/6012/firmware_core.toml create mode 100644 spec/dossiers/6012/u2_core.toml create mode 100644 spec/dossiers/901a/core_diag.toml create mode 100644 spec/dossiers/901a/firmware_preflight.toml create mode 100644 spec/dossiers/901a/mode_or_profile_read.toml create mode 100644 spec/dossiers/9028/core_diag.toml create mode 100644 spec/dossiers/9028/firmware_preflight.toml create mode 100644 spec/dossiers/9028/mode_or_profile_read.toml create mode 100644 spec/dossiers/global/core.toml create mode 100644 spec/evidence_index.csv diff --git a/.github/ISSUE_TEMPLATE/dirtyroom-evidence.yml b/.github/ISSUE_TEMPLATE/dirtyroom-evidence.yml new file mode 100644 index 0000000..2096196 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/dirtyroom-evidence.yml @@ -0,0 +1,59 @@ +name: Dirty-Room Evidence (Sanitized) +description: Submit sanitized protocol evidence for unconfirmed devices/operations. +title: "[dirtyroom] " +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. diff --git a/.github/ISSUE_TEMPLATE/hardware-report.yml b/.github/ISSUE_TEMPLATE/hardware-report.yml new file mode 100644 index 0000000..ee22d5b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/hardware-report.yml @@ -0,0 +1,54 @@ +name: Hardware Report +description: Report tested device behavior for OpenBitdo compatibility. +title: "[hardware] " +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 diff --git a/.github/ISSUE_TEMPLATE/release-blocker.yml b/.github/ISSUE_TEMPLATE/release-blocker.yml new file mode 100644 index 0000000..659a11e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release-blocker.yml @@ -0,0 +1,73 @@ +name: Release Blocker +description: Report a problem that must be resolved before public RC release. +title: "[release-blocker] " +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." diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml new file mode 100644 index 0000000..964a511 --- /dev/null +++ b/.github/workflows/aur-publish.yml @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e18722b..2a26b90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..94dda9c --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/.gitignore b/.gitignore index a1a4002..c585139 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6d486f5 --- /dev/null +++ b/CHANGELOG.md @@ -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`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9c669de --- /dev/null +++ b/LICENSE @@ -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. diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..c5adfc8 --- /dev/null +++ b/MIGRATION.md @@ -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) diff --git a/RC_CHECKLIST.md b/RC_CHECKLIST.md new file mode 100644 index 0000000..33d9c4e --- /dev/null +++ b/RC_CHECKLIST.md @@ -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. diff --git a/README.md b/README.md index 906e51c..a2b6673 100644 --- a/README.md +++ b/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)). diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..a444486 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v0.0.1-rc.1 \ No newline at end of file diff --git a/harness/golden/jp108/read_mapping_roundtrip.toml b/harness/golden/jp108/read_mapping_roundtrip.toml new file mode 100644 index 0000000..3879ec7 --- /dev/null +++ b/harness/golden/jp108/read_mapping_roundtrip.toml @@ -0,0 +1,4 @@ +operation = "jp108_read_dedicated_mapping" +pid = "0x5209" +status = "sanitized-placeholder" +notes = "Trace placeholder for clean-room replay harness" diff --git a/harness/golden/jp108/write_mapping_apply.toml b/harness/golden/jp108/write_mapping_apply.toml new file mode 100644 index 0000000..81ce946 --- /dev/null +++ b/harness/golden/jp108/write_mapping_apply.toml @@ -0,0 +1,4 @@ +operation = "jp108_write_dedicated_mapping" +pid = "0x520a" +status = "sanitized-placeholder" +notes = "Trace placeholder for clean-room replay harness" diff --git a/harness/golden/ultimate2/read_profile_slot.toml b/harness/golden/ultimate2/read_profile_slot.toml new file mode 100644 index 0000000..c6967dd --- /dev/null +++ b/harness/golden/ultimate2/read_profile_slot.toml @@ -0,0 +1,4 @@ +operation = "u2_read_core_profile" +pid = "0x6012" +status = "sanitized-placeholder" +notes = "Trace placeholder for clean-room replay harness" diff --git a/harness/golden/ultimate2/write_profile_slot.toml b/harness/golden/ultimate2/write_profile_slot.toml new file mode 100644 index 0000000..4ff031c --- /dev/null +++ b/harness/golden/ultimate2/write_profile_slot.toml @@ -0,0 +1,4 @@ +operation = "u2_write_core_profile" +pid = "0x6013" +status = "sanitized-placeholder" +notes = "Trace placeholder for clean-room replay harness" diff --git a/harness/lab/device_lab.yaml b/harness/lab/device_lab.yaml index 478b81a..3d43a07 100644 --- a/harness/lab/device_lab.yaml +++ b/harness/lab/device_lab.yaml @@ -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 diff --git a/packaging/aur/README.md b/packaging/aur/README.md new file mode 100644 index 0000000..11f3c6f --- /dev/null +++ b/packaging/aur/README.md @@ -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` diff --git a/packaging/aur/openbitdo-bin/.SRCINFO b/packaging/aur/openbitdo-bin/.SRCINFO new file mode 100644 index 0000000..b0d8a7f --- /dev/null +++ b/packaging/aur/openbitdo-bin/.SRCINFO @@ -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 diff --git a/packaging/aur/openbitdo-bin/PKGBUILD b/packaging/aur/openbitdo-bin/PKGBUILD new file mode 100644 index 0000000..a6c574a --- /dev/null +++ b/packaging/aur/openbitdo-bin/PKGBUILD @@ -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" +} diff --git a/packaging/aur/openbitdo-bin/PKGBUILD.tmpl b/packaging/aur/openbitdo-bin/PKGBUILD.tmpl new file mode 100644 index 0000000..5297480 --- /dev/null +++ b/packaging/aur/openbitdo-bin/PKGBUILD.tmpl @@ -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" +} diff --git a/packaging/aur/openbitdo/.SRCINFO b/packaging/aur/openbitdo/.SRCINFO new file mode 100644 index 0000000..1a608a7 --- /dev/null +++ b/packaging/aur/openbitdo/.SRCINFO @@ -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 diff --git a/packaging/aur/openbitdo/PKGBUILD b/packaging/aur/openbitdo/PKGBUILD new file mode 100644 index 0000000..51e03b6 --- /dev/null +++ b/packaging/aur/openbitdo/PKGBUILD @@ -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" +} diff --git a/packaging/aur/openbitdo/PKGBUILD.tmpl b/packaging/aur/openbitdo/PKGBUILD.tmpl new file mode 100644 index 0000000..122c7a8 --- /dev/null +++ b/packaging/aur/openbitdo/PKGBUILD.tmpl @@ -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" +} diff --git a/packaging/homebrew/Formula/openbitdo.rb b/packaging/homebrew/Formula/openbitdo.rb new file mode 100644 index 0000000..b1b04c6 --- /dev/null +++ b/packaging/homebrew/Formula/openbitdo.rb @@ -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 diff --git a/packaging/homebrew/Formula/openbitdo.rb.tmpl b/packaging/homebrew/Formula/openbitdo.rb.tmpl new file mode 100644 index 0000000..3c50dcf --- /dev/null +++ b/packaging/homebrew/Formula/openbitdo.rb.tmpl @@ -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 diff --git a/packaging/homebrew/README.md b/packaging/homebrew/README.md new file mode 100644 index 0000000..6bdf355 --- /dev/null +++ b/packaging/homebrew/README.md @@ -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`) diff --git a/packaging/homebrew/sync_tap.sh b/packaging/homebrew/sync_tap.sh new file mode 100755 index 0000000..0c4935e --- /dev/null +++ b/packaging/homebrew/sync_tap.sh @@ -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 diff --git a/packaging/scripts/render_release_metadata.sh b/packaging/scripts/render_release_metadata.sh new file mode 100755 index 0000000..e87aefb --- /dev/null +++ b/packaging/scripts/render_release_metadata.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + render_release_metadata.sh + +Inputs expected in : + openbitdo--source.tar.gz + openbitdo--linux-x86_64.tar.gz + openbitdo--linux-aarch64.tar.gz + openbitdo--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" < 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` diff --git a/process/commenting_standard.md b/process/commenting_standard.md new file mode 100644 index 0000000..05206ee --- /dev/null +++ b/process/commenting_standard.md @@ -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. diff --git a/process/community_evidence_intake.md b/process/community_evidence_intake.md new file mode 100644 index 0000000..df257d3 --- /dev/null +++ b/process/community_evidence_intake.md @@ -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//*.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. diff --git a/process/device_name_sources.md b/process/device_name_sources.md new file mode 100644 index 0000000..d1422f8 --- /dev/null +++ b/process/device_name_sources.md @@ -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_*)` diff --git a/process/dirtyroom_collection_playbook.md b/process/dirtyroom_collection_playbook.md new file mode 100644 index 0000000..6b2c2b4 --- /dev/null +++ b/process/dirtyroom_collection_playbook.md @@ -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//.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. diff --git a/process/dirtyroom_dossier_schema.md b/process/dirtyroom_dossier_schema.md new file mode 100644 index 0000000..5a401f5 --- /dev/null +++ b/process/dirtyroom_dossier_schema.md @@ -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"] +``` diff --git a/process/dirtyroom_evidence_backlog.md b/process/dirtyroom_evidence_backlog.md new file mode 100644 index 0000000..1629aa4 --- /dev/null +++ b/process/dirtyroom_evidence_backlog.md @@ -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. diff --git a/process/release_scope_gate.toml b/process/release_scope_gate.toml new file mode 100644 index 0000000..317145b --- /dev/null +++ b/process/release_scope_gate.toml @@ -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 diff --git a/process/wave1_baseline.md b/process/wave1_baseline.md new file mode 100644 index 0000000..570201f --- /dev/null +++ b/process/wave1_baseline.md @@ -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. diff --git a/process/wave1_results.md b/process/wave1_results.md new file mode 100644 index 0000000..f849b22 --- /dev/null +++ b/process/wave1_results.md @@ -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`. diff --git a/process/wave2_baseline.md b/process/wave2_baseline.md new file mode 100644 index 0000000..fd411d8 --- /dev/null +++ b/process/wave2_baseline.md @@ -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. diff --git a/process/wave2_pid_scorecard.md b/process/wave2_pid_scorecard.md new file mode 100644 index 0000000..a8662ac --- /dev/null +++ b/process/wave2_pid_scorecard.md @@ -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 | diff --git a/process/wave2_results.md b/process/wave2_results.md new file mode 100644 index 0000000..f3e49b7 --- /dev/null +++ b/process/wave2_results.md @@ -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. diff --git a/process/wave2_runtime_intake.md b/process/wave2_runtime_intake.md new file mode 100644 index 0000000..f6ab186 --- /dev/null +++ b/process/wave2_runtime_intake.md @@ -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). diff --git a/sdk/Cargo.lock b/sdk/Cargo.lock index bb990c9..489ac3c 100644 --- a/sdk/Cargo.lock +++ b/sdk/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -47,7 +62,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -58,7 +73,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -82,17 +97,54 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitdo_app_core" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "bitdo_proto", + "chrono", + "ed25519-dalek", + "hex", + "reqwest", + "serde", + "serde_json", + "sha2", + "thiserror", + "tokio", + "toml", + "uuid", +] + [[package]] name = "bitdo_proto" version = "0.1.0" dependencies = [ - "csv", "hex", "hidapi", "serde", @@ -101,16 +153,33 @@ dependencies = [ ] [[package]] -name = "bitdoctl" +name = "bitdo_tui" version = "0.1.0" dependencies = [ "anyhow", - "assert_cmd", + "bitdo_app_core", "bitdo_proto", - "clap", - "hex", - "predicates", - "serde_json", + "chrono", + "crossterm 0.29.0", + "ratatui", + "serde", + "tokio", + "toml", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", ] [[package]] @@ -124,6 +193,33 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.56" @@ -140,6 +236,26 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.60" @@ -187,24 +303,193 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "csv" -version = "1.4.0" +name = "compact_str" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" dependencies = [ - "csv-core", + "castaway", + "cfg-if", "itoa", + "rustversion", "ryu", - "serde_core", + "static_assertions", ] [[package]] -name = "csv-core" -version = "0.1.13" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "memchr", + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.4", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", ] [[package]] @@ -213,6 +498,88 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -228,6 +595,121 @@ dependencies = [ "num-traits", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" @@ -250,7 +732,295 @@ dependencies = [ "cfg-if", "libc", "pkg-config", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", ] [[package]] @@ -259,30 +1029,145 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -292,18 +1177,116 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openbitdo" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert_cmd", + "bitdo_app_core", + "bitdo_tui", + "clap", + "predicates", + "serde", + "tokio", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "predicates" version = "3.1.4" @@ -334,6 +1317,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -343,6 +1336,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.44" @@ -352,6 +1400,80 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.3" @@ -381,12 +1503,158 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -430,18 +1698,171 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -453,6 +1874,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "termtree" version = "0.5.1" @@ -479,18 +1920,330 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wait-timeout" version = "0.2.1" @@ -500,12 +2253,269 @@ dependencies = [ "libc", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -515,6 +2525,341 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 5494ceb..a0f5112 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -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"] } diff --git a/sdk/README.md b/sdk/README.md index 4765cab..26a262e 100644 --- a/sdk/README.md +++ b/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---.tar.gz` +- `openbitdo---` 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`) diff --git a/sdk/crates/bitdo_app_core/Cargo.toml b/sdk/crates/bitdo_app_core/Cargo.toml new file mode 100644 index 0000000..848e788 --- /dev/null +++ b/sdk/crates/bitdo_app_core/Cargo.toml @@ -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"] } diff --git a/sdk/crates/bitdo_app_core/src/lib.rs b/sdk/crates/bitdo_app_core/src/lib.rs new file mode 100644 index 0000000..debc5f0 --- /dev/null +++ b/sdk/crates/bitdo_app_core/src/lib.rs @@ -0,0 +1,2060 @@ +use base64::Engine; +use bitdo_proto::{ + device_profile_for, enumerate_hid_devices, BitdoErrorCode, DeviceSession, DiagProbeResult, + DiagSeverity, HidTransport, PidCapability, ProtocolFamily, SessionConfig, SupportEvidence, + SupportLevel, SupportTier, VidPid, +}; +use chrono::{DateTime, Utc}; +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use thiserror::Error; +use tokio::sync::{broadcast, Mutex, RwLock}; +use tokio::time::{sleep, Duration}; +use uuid::Uuid; + +const DEFAULT_MANIFEST_URL: &str = + "https://github.com/bybrooklyn/openbitdo/releases/latest/download/firmware-manifest.toml"; +const PINNED_ED25519_ACTIVE_PUBLIC_KEY_HEX: &str = + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"; +const PINNED_ED25519_NEXT_PUBLIC_KEY_HEX: &str = + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"; + +pub fn signing_key_fingerprint_active_sha256() -> String { + signing_key_fingerprint_sha256(PINNED_ED25519_ACTIVE_PUBLIC_KEY_HEX) +} + +pub fn signing_key_fingerprint_next_sha256() -> String { + signing_key_fingerprint_sha256(PINNED_ED25519_NEXT_PUBLIC_KEY_HEX) +} + +fn signing_key_fingerprint_sha256(public_key_hex: &str) -> String { + let bytes = match hex::decode(public_key_hex) { + Ok(bytes) => bytes, + Err(_) => return "unknown".to_owned(), + }; + sha256_hex(&bytes) +} + +#[derive(Clone, Debug)] +pub struct OpenBitdoCoreConfig { + pub mock_mode: bool, + pub advanced_mode: bool, + pub default_chunk_size: usize, + pub progress_interval_ms: u64, + pub firmware_manifest_url: String, +} + +impl Default for OpenBitdoCoreConfig { + fn default() -> Self { + Self { + mock_mode: false, + advanced_mode: false, + default_chunk_size: 56, + progress_interval_ms: 25, + firmware_manifest_url: DEFAULT_MANIFEST_URL.to_owned(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum DedicatedButtonId { + A, + B, + K1, + K2, + K3, + K4, + K5, + K6, + K7, + K8, +} + +impl DedicatedButtonId { + pub const ALL: [DedicatedButtonId; 10] = [ + DedicatedButtonId::A, + DedicatedButtonId::B, + DedicatedButtonId::K1, + DedicatedButtonId::K2, + DedicatedButtonId::K3, + DedicatedButtonId::K4, + DedicatedButtonId::K5, + DedicatedButtonId::K6, + DedicatedButtonId::K7, + DedicatedButtonId::K8, + ]; + + fn wire_index(self) -> u8 { + match self { + DedicatedButtonId::A => 0, + DedicatedButtonId::B => 1, + DedicatedButtonId::K1 => 2, + DedicatedButtonId::K2 => 3, + DedicatedButtonId::K3 => 4, + DedicatedButtonId::K4 => 5, + DedicatedButtonId::K5 => 6, + DedicatedButtonId::K6 => 7, + DedicatedButtonId::K7 => 8, + DedicatedButtonId::K8 => 9, + } + } + + fn from_wire_index(value: u8) -> Option { + Self::ALL + .iter() + .copied() + .find(|entry| entry.wire_index() == value) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum U2ButtonId { + A, + B, + K1, + K2, + K3, + K4, + K5, + K6, + K7, + K8, +} + +impl U2ButtonId { + pub const ALL: [U2ButtonId; 10] = [ + U2ButtonId::A, + U2ButtonId::B, + U2ButtonId::K1, + U2ButtonId::K2, + U2ButtonId::K3, + U2ButtonId::K4, + U2ButtonId::K5, + U2ButtonId::K6, + U2ButtonId::K7, + U2ButtonId::K8, + ]; + + fn wire_index(self) -> u8 { + match self { + U2ButtonId::A => 0, + U2ButtonId::B => 1, + U2ButtonId::K1 => 2, + U2ButtonId::K2 => 3, + U2ButtonId::K3 => 4, + U2ButtonId::K4 => 5, + U2ButtonId::K5 => 6, + U2ButtonId::K6 => 7, + U2ButtonId::K7 => 8, + U2ButtonId::K8 => 9, + } + } + + fn from_wire_index(value: u8) -> Option { + Self::ALL + .iter() + .copied() + .find(|entry| entry.wire_index() == value) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum U2SlotId { + Slot1, + Slot2, + Slot3, +} + +impl U2SlotId { + fn wire_value(self) -> u8 { + match self { + U2SlotId::Slot1 => 1, + U2SlotId::Slot2 => 2, + U2SlotId::Slot3 => 3, + } + } + + fn from_wire_value(value: u8) -> Self { + match value { + 2 => U2SlotId::Slot2, + 3 => U2SlotId::Slot3, + _ => U2SlotId::Slot1, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct ConfigBackupId(pub String); + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum DeviceKind { + Jp108, + Ultimate2, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct DedicatedButtonMapping { + pub button: DedicatedButtonId, + pub target_hid_usage: u16, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct U2ButtonMapping { + pub button: U2ButtonId, + pub target_hid_usage: u16, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct U2CoreProfile { + pub slot: U2SlotId, + pub mode: u8, + pub firmware_version: String, + pub l2_analog: f32, + pub r2_analog: f32, + pub supports_trigger_write: bool, + pub mappings: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct GuidedButtonTestResult { + pub device_kind: DeviceKind, + pub expected_inputs: Vec, + pub passed: bool, + pub guidance: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct ConfigBackup { + created_at: DateTime, + target: VidPid, + payload: ConfigBackupPayload, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +enum ConfigBackupPayload { + Jp108 { + mappings: Vec, + }, + U2 { + profile: U2CoreProfile, + config_blob: Vec, + }, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct WriteRecoveryReport { + pub backup_id: Option, + pub write_applied: bool, + pub rollback_attempted: bool, + pub rollback_succeeded: bool, + pub write_error: Option, + pub rollback_error: Option, +} + +impl WriteRecoveryReport { + pub fn rollback_failed(&self) -> bool { + self.rollback_attempted && !self.rollback_succeeded + } +} + +#[derive(Clone)] +pub struct OpenBitdoCore { + config: OpenBitdoCoreConfig, + advanced_mode: Arc, + sessions: Arc>>>, + backups: Arc>>, + http: reqwest::Client, +} + +impl OpenBitdoCore { + pub fn new(config: OpenBitdoCoreConfig) -> Self { + Self { + advanced_mode: Arc::new(AtomicBool::new(config.advanced_mode)), + config, + sessions: Arc::new(RwLock::new(HashMap::new())), + backups: Arc::new(RwLock::new(HashMap::new())), + http: reqwest::Client::new(), + } + } + + /// Advanced mode enables inferred SafeRead commands only. + /// Write/unsafe inferred commands remain blocked by runtime policy. + pub fn set_advanced_mode(&self, enabled: bool) { + self.advanced_mode.store(enabled, Ordering::Relaxed); + } + + pub fn advanced_mode(&self) -> bool { + self.advanced_mode.load(Ordering::Relaxed) + } + + pub async fn list_devices(&self) -> AppCoreResult> { + if self.config.mock_mode { + return Ok(vec![ + mock_device(VidPid::new(0x2dc8, 0x5209), true), + mock_device(VidPid::new(0x2dc8, 0x6012), true), + mock_device(VidPid::new(0x2dc8, 0x2100), false), + ]); + } + + let devices = enumerate_hid_devices().map_err(AppCoreError::Protocol)?; + let filtered = devices + .into_iter() + .filter(|d| d.vid_pid.vid == 0x2dc8) + .map(|d| { + let profile = device_profile_for(d.vid_pid); + AppDevice { + vid_pid: d.vid_pid, + name: profile.name, + support_level: profile.support_level, + support_tier: profile.support_tier, + protocol_family: profile.protocol_family, + capability: profile.capability, + evidence: profile.evidence, + serial: d.serial, + connected: true, + } + }) + .collect::>(); + Ok(filtered) + } + + pub async fn diag_probe(&self, target: VidPid) -> AppCoreResult { + if self.config.mock_mode { + return Ok(mock_diag_probe(target)); + } + + let mut session = DeviceSession::new( + HidTransport::new(), + target, + SessionConfig { + // Diagnostics always execute inferred SafeRead checks. Those + // checks are explicitly marked experimental in their result + // metadata so users can distinguish confidence levels. + experimental: true, + ..Default::default() + }, + ) + .map_err(AppCoreError::Protocol)?; + let diag = session.diag_probe(); + let _ = session.close(); + Ok(diag) + } + + pub fn beginner_diag_summary(&self, device: &AppDevice, diag: &DiagProbeResult) -> String { + let passed = diag.command_checks.iter().filter(|c| c.ok).count(); + let total = diag.command_checks.len(); + let experimental_total = diag + .command_checks + .iter() + .filter(|c| c.is_experimental) + .count(); + let experimental_ok = diag + .command_checks + .iter() + .filter(|c| c.is_experimental && c.ok) + .count(); + let needs_attention = diag + .command_checks + .iter() + .filter(|c| c.severity == DiagSeverity::NeedsAttention) + .count(); + let family_hint = match device.protocol_family { + ProtocolFamily::Standard64 => { + "Standard64 diagnostics are available. Read checks are safe while writes stay blocked until hardware confirmation." + } + ProtocolFamily::JpHandshake => { + "JP-handshake diagnostics are available. Handshake/version checks are the safe default path." + } + ProtocolFamily::DInput => { + "DInput diagnostics are available. Read checks are safe; write paths remain policy-gated." + } + ProtocolFamily::DS4Boot => { + "Boot-mode diagnostics are limited. Keep the device in normal mode for beginner-safe checks." + } + ProtocolFamily::Unknown => { + "Only basic identify diagnostics are available for unknown protocol family devices." + } + }; + + let status_hint = if needs_attention > 0 { + format!("Needs attention: {needs_attention} safety-critical diagnostic signal(s).") + } else { + "Needs attention: none.".to_owned() + }; + let experimental_hint = + format!("Experimental checks: {experimental_ok}/{experimental_total} passed."); + + match device.support_tier { + SupportTier::Full => format!( + "{passed}/{total} checks passed. {experimental_hint} {status_hint} {family_hint} This device is full-support." + ), + SupportTier::CandidateReadOnly => format!( + "{passed}/{total} checks passed. {experimental_hint} {status_hint} {family_hint} This device is candidate-readonly: update and mapping stay blocked until runtime + hardware confirmation." + ), + SupportTier::DetectOnly => format!( + "{passed}/{total} checks passed. {experimental_hint} {status_hint} {family_hint} This device is detect-only: use diagnostics only." + ), + } + } + + pub async fn jp108_read_dedicated_mapping( + &self, + vidpid: VidPid, + ) -> AppCoreResult> { + let profile = device_profile_for(vidpid); + if !profile.capability.supports_jp108_dedicated_map { + return Err(AppCoreError::PolicyDenied { + reason: AppPolicyGateReason::UnsupportedPid, + message: format!("JP108 dedicated mapping is not supported for {}", vidpid), + }); + } + + if self.config.mock_mode { + return Ok(default_jp108_mappings()); + } + + let mut session = self.open_session_for_ops(vidpid)?; + let mappings = session + .jp108_read_dedicated_mappings() + .map_err(AppCoreError::Protocol)? + .into_iter() + .filter_map(|(idx, usage)| { + DedicatedButtonId::from_wire_index(idx).map(|button| DedicatedButtonMapping { + button, + target_hid_usage: usage, + }) + }) + .collect::>(); + let _ = session.close(); + Ok(mappings) + } + + pub async fn jp108_apply_dedicated_mapping( + &self, + vidpid: VidPid, + changes: Vec, + backup: bool, + ) -> AppCoreResult> { + let report = self + .jp108_apply_dedicated_mapping_with_recovery(vidpid, changes, backup) + .await?; + if report.write_applied { + return Ok(report.backup_id); + } + if report.rollback_failed() { + return Err(AppCoreError::InvalidState( + report + .rollback_error + .unwrap_or_else(|| "write failed and rollback failed".to_owned()), + )); + } + Err(AppCoreError::InvalidState( + report + .write_error + .unwrap_or_else(|| "write failed; rollback restored previous state".to_owned()), + )) + } + + pub async fn jp108_apply_dedicated_mapping_with_recovery( + &self, + vidpid: VidPid, + changes: Vec, + backup: bool, + ) -> AppCoreResult { + let profile = device_profile_for(vidpid); + if !profile.capability.supports_jp108_dedicated_map { + return Err(AppCoreError::PolicyDenied { + reason: AppPolicyGateReason::UnsupportedPid, + message: format!("JP108 dedicated mapping is not supported for {}", vidpid), + }); + } + + if self.config.mock_mode { + let backup_id = if backup { + Some( + self.store_backup( + vidpid, + ConfigBackupPayload::Jp108 { + mappings: default_jp108_mappings(), + }, + ) + .await, + ) + } else { + None + }; + return Ok(WriteRecoveryReport { + backup_id, + write_applied: true, + rollback_attempted: false, + rollback_succeeded: false, + write_error: None, + rollback_error: None, + }); + } + + let backup_id = if backup { + let existing = self.jp108_read_dedicated_mapping(vidpid).await?; + Some( + self.store_backup(vidpid, ConfigBackupPayload::Jp108 { mappings: existing }) + .await, + ) + } else { + None + }; + + let mut session = self.open_session_for_ops(vidpid)?; + let apply_result: AppCoreResult<()> = (|| { + for change in &changes { + session + .jp108_write_dedicated_mapping( + change.button.wire_index(), + change.target_hid_usage, + ) + .map_err(AppCoreError::Protocol)?; + } + Ok(()) + })(); + let _ = session.close(); + + if let Err(err) = apply_result { + let write_error = err.to_string(); + if let Some(id) = backup_id.as_ref() { + match self.restore_backup(id.clone()).await { + Ok(_) => { + return Ok(WriteRecoveryReport { + backup_id, + write_applied: false, + rollback_attempted: true, + rollback_succeeded: true, + write_error: Some(write_error), + rollback_error: None, + }); + } + Err(rollback_err) => { + return Ok(WriteRecoveryReport { + backup_id, + write_applied: false, + rollback_attempted: true, + rollback_succeeded: false, + write_error: Some(write_error), + rollback_error: Some(rollback_err.to_string()), + }); + } + } + } + + return Ok(WriteRecoveryReport { + backup_id: None, + write_applied: false, + rollback_attempted: false, + rollback_succeeded: false, + write_error: Some(write_error), + rollback_error: None, + }); + } + + Ok(WriteRecoveryReport { + backup_id, + write_applied: true, + rollback_attempted: false, + rollback_succeeded: false, + write_error: None, + rollback_error: None, + }) + } + + pub async fn u2_read_core_profile( + &self, + vidpid: VidPid, + slot: U2SlotId, + ) -> AppCoreResult { + let profile = device_profile_for(vidpid); + if !(profile.capability.supports_u2_slot_config + && profile.capability.supports_u2_button_map) + { + return Err(AppCoreError::PolicyDenied { + reason: AppPolicyGateReason::UnsupportedPid, + message: format!("Ultimate2 core profile is not supported for {}", vidpid), + }); + } + + if self.config.mock_mode { + return Ok(U2CoreProfile { + slot, + mode: 0, + firmware_version: "mock-1.0.0".to_owned(), + l2_analog: 0.5, + r2_analog: 0.5, + supports_trigger_write: true, + mappings: default_u2_mappings(), + }); + } + + let mut session = self.open_session_for_ops(vidpid)?; + let active_slot = session + .u2_get_current_slot() + .map(U2SlotId::from_wire_value) + .unwrap_or(slot); + let mode = session.get_mode().map_err(AppCoreError::Protocol)?.mode; + let firmware_version = session + .send_command(bitdo_proto::CommandId::GetControllerVersion, None) + .ok() + .and_then(|resp| resp.parsed_fields.get("version_x100").copied()) + .map(|raw| format!("{:.2}", raw as f32 / 100.0)) + .unwrap_or_else(|| "unknown".to_owned()); + let config_blob = session + .u2_read_config_slot(active_slot.wire_value()) + .map_err(AppCoreError::Protocol)?; + let map = session + .u2_read_button_map(active_slot.wire_value()) + .map_err(AppCoreError::Protocol)? + .into_iter() + .filter_map(|(idx, usage)| { + U2ButtonId::from_wire_index(idx).map(|button| U2ButtonMapping { + button, + target_hid_usage: usage, + }) + }) + .collect::>(); + let _ = session.close(); + Ok(U2CoreProfile { + slot: active_slot, + mode, + firmware_version, + l2_analog: config_blob.get(6).map(|v| *v as f32 / 255.0).unwrap_or(0.0), + r2_analog: config_blob.get(7).map(|v| *v as f32 / 255.0).unwrap_or(0.0), + supports_trigger_write: profile.support_tier == SupportTier::Full, + mappings: map, + }) + } + + #[allow(clippy::too_many_arguments)] + pub async fn u2_apply_core_profile( + &self, + vidpid: VidPid, + slot: U2SlotId, + mode: u8, + map_changes: Vec, + l2_analog: f32, + r2_analog: f32, + backup: bool, + ) -> AppCoreResult> { + let report = self + .u2_apply_core_profile_with_recovery( + vidpid, + slot, + mode, + map_changes, + l2_analog, + r2_analog, + backup, + ) + .await?; + if report.write_applied { + return Ok(report.backup_id); + } + if report.rollback_failed() { + return Err(AppCoreError::InvalidState( + report + .rollback_error + .unwrap_or_else(|| "write failed and rollback failed".to_owned()), + )); + } + Err(AppCoreError::InvalidState( + report + .write_error + .unwrap_or_else(|| "write failed; rollback restored previous state".to_owned()), + )) + } + + #[allow(clippy::too_many_arguments)] + pub async fn u2_apply_core_profile_with_recovery( + &self, + vidpid: VidPid, + slot: U2SlotId, + mode: u8, + map_changes: Vec, + l2_analog: f32, + r2_analog: f32, + backup: bool, + ) -> AppCoreResult { + let profile = device_profile_for(vidpid); + if !(profile.capability.supports_u2_slot_config + && profile.capability.supports_u2_button_map) + { + return Err(AppCoreError::PolicyDenied { + reason: AppPolicyGateReason::UnsupportedPid, + message: format!("Ultimate2 core profile is not supported for {}", vidpid), + }); + } + + if self.config.mock_mode { + let backup_id = if backup { + Some( + self.store_backup( + vidpid, + ConfigBackupPayload::U2 { + profile: U2CoreProfile { + slot, + mode: 0, + firmware_version: "mock-1.0.0".to_owned(), + l2_analog: 0.5, + r2_analog: 0.5, + supports_trigger_write: true, + mappings: default_u2_mappings(), + }, + config_blob: vec![0; 32], + }, + ) + .await, + ) + } else { + None + }; + return Ok(WriteRecoveryReport { + backup_id, + write_applied: true, + rollback_attempted: false, + rollback_succeeded: false, + write_error: None, + rollback_error: None, + }); + } + + let backup_id = if backup { + let current = self.u2_read_core_profile(vidpid, slot).await?; + let mut session = self.open_session_for_ops(vidpid)?; + let config_blob = session + .u2_read_config_slot(slot.wire_value()) + .map_err(AppCoreError::Protocol)?; + let _ = session.close(); + Some( + self.store_backup( + vidpid, + ConfigBackupPayload::U2 { + profile: current, + config_blob, + }, + ) + .await, + ) + } else { + None + }; + + let mut session = self.open_session_for_ops(vidpid)?; + let apply_result: AppCoreResult<()> = (|| { + session.u2_set_mode(mode).map_err(AppCoreError::Protocol)?; + let wire_map = map_changes + .iter() + .map(|entry| (entry.button.wire_index(), entry.target_hid_usage)) + .collect::>(); + session + .u2_write_button_map(slot.wire_value(), &wire_map) + .map_err(AppCoreError::Protocol)?; + let mut config_blob = session + .u2_read_config_slot(slot.wire_value()) + .map_err(AppCoreError::Protocol)?; + if config_blob.is_empty() { + config_blob.resize(16, 0); + } + if config_blob.len() > 6 { + config_blob[4] = slot.wire_value(); + config_blob[5] = mode; + if config_blob.len() > 8 { + config_blob[6] = (l2_analog.clamp(0.0, 1.0) * 255.0).round() as u8; + config_blob[7] = (r2_analog.clamp(0.0, 1.0) * 255.0).round() as u8; + } + } + session + .u2_write_config_slot(slot.wire_value(), &config_blob) + .map_err(AppCoreError::Protocol)?; + Ok(()) + })(); + let _ = session.close(); + + if let Err(err) = apply_result { + let write_error = err.to_string(); + if let Some(id) = backup_id.as_ref() { + match self.restore_backup(id.clone()).await { + Ok(_) => { + return Ok(WriteRecoveryReport { + backup_id, + write_applied: false, + rollback_attempted: true, + rollback_succeeded: true, + write_error: Some(write_error), + rollback_error: None, + }); + } + Err(rollback_err) => { + return Ok(WriteRecoveryReport { + backup_id, + write_applied: false, + rollback_attempted: true, + rollback_succeeded: false, + write_error: Some(write_error), + rollback_error: Some(rollback_err.to_string()), + }); + } + } + } + + return Ok(WriteRecoveryReport { + backup_id: None, + write_applied: false, + rollback_attempted: false, + rollback_succeeded: false, + write_error: Some(write_error), + rollback_error: None, + }); + } + + Ok(WriteRecoveryReport { + backup_id, + write_applied: true, + rollback_attempted: false, + rollback_succeeded: false, + write_error: None, + rollback_error: None, + }) + } + + pub async fn restore_backup(&self, backup_id: ConfigBackupId) -> AppCoreResult<()> { + let backup = { + let backups = self.backups.read().await; + backups.get(&backup_id.0).cloned().ok_or_else(|| { + AppCoreError::NotFound(format!("unknown backup id: {}", backup_id.0)) + })? + }; + + if self.config.mock_mode { + return Ok(()); + } + + let mut session = self.open_session_for_ops(backup.target)?; + let restore_result: AppCoreResult<()> = (|| match backup.payload { + ConfigBackupPayload::Jp108 { mappings } => { + for entry in &mappings { + session + .jp108_write_dedicated_mapping( + entry.button.wire_index(), + entry.target_hid_usage, + ) + .map_err(AppCoreError::Protocol)?; + } + Ok(()) + } + ConfigBackupPayload::U2 { + profile, + config_blob, + } => { + session + .u2_set_mode(profile.mode) + .map_err(AppCoreError::Protocol)?; + let wire_map = profile + .mappings + .iter() + .map(|entry| (entry.button.wire_index(), entry.target_hid_usage)) + .collect::>(); + session + .u2_write_button_map(profile.slot.wire_value(), &wire_map) + .map_err(AppCoreError::Protocol)?; + session + .u2_write_config_slot(profile.slot.wire_value(), &config_blob) + .map_err(AppCoreError::Protocol)?; + Ok(()) + } + })(); + let _ = session.close(); + restore_result + } + + pub async fn guided_button_test( + &self, + device_kind: DeviceKind, + expected_inputs: Vec, + ) -> AppCoreResult { + let guidance = match device_kind { + DeviceKind::Jp108 => { + "Press each mapped JP108 dedicated key once and verify it matches the on-screen expected input." + } + DeviceKind::Ultimate2 => { + "Press each remapped Ultimate2 core button once and verify it matches the expected action." + } + }; + + Ok(GuidedButtonTestResult { + device_kind, + expected_inputs, + passed: true, + guidance: guidance.to_owned(), + }) + } + + pub async fn download_recommended_firmware( + &self, + target: VidPid, + ) -> AppCoreResult { + if self.config.mock_mode { + let path = std::env::temp_dir().join(format!( + "openbitdo-fw-mock-{:04x}-{}.bin", + target.pid, + Uuid::new_v4() + )); + let bytes = vec![0xAB; 4096]; + tokio::fs::write(&path, &bytes).await?; + let sha256 = sha256_hex(&bytes); + return Ok(FirmwareDownloadResult { + firmware_path: path, + version: "mock-1.0.0".to_owned(), + source_url: "mock://firmware".to_owned(), + sha256, + verified_signature: true, + }); + } + + let manifest_raw = self + .http + .get(&self.config.firmware_manifest_url) + .send() + .await + .map_err(|err| AppCoreError::Download(format!("manifest request failed: {err}")))? + .error_for_status() + .map_err(|err| AppCoreError::Download(format!("manifest download failed: {err}")))? + .text() + .await + .map_err(|err| AppCoreError::Download(format!("manifest read failed: {err}")))?; + + let manifest: FirmwareManifest = toml::from_str(&manifest_raw) + .map_err(|err| AppCoreError::Manifest(format!("invalid manifest TOML: {err}")))?; + + let profile = device_profile_for(target); + let artifact = manifest + .recommended_for(target, profile.protocol_family) + .ok_or_else(|| { + AppCoreError::Download(format!( + "no stable firmware artifact for pid={:#06x} family={:?}", + target.pid, profile.protocol_family + )) + })?; + + let artifact_bytes = self + .http + .get(&artifact.url) + .send() + .await + .map_err(|err| AppCoreError::Download(format!("artifact request failed: {err}")))? + .error_for_status() + .map_err(|err| AppCoreError::Download(format!("artifact download failed: {err}")))? + .bytes() + .await + .map_err(|err| AppCoreError::Download(format!("artifact read failed: {err}")))? + .to_vec(); + + let actual_hash = sha256_hex(&artifact_bytes); + if !actual_hash.eq_ignore_ascii_case(&artifact.sha256) { + return Err(AppCoreError::PolicyDenied { + reason: AppPolicyGateReason::ImageValidationFailed, + message: format!( + "downloaded firmware hash mismatch: expected={} actual={}", + artifact.sha256, actual_hash + ), + }); + } + + verify_artifact_signature(&self.http, artifact, &artifact_bytes).await?; + + let out = std::env::temp_dir().join(format!( + "openbitdo-fw-{:04x}-{}.bin", + artifact.pid, + Uuid::new_v4() + )); + tokio::fs::write(&out, &artifact_bytes).await?; + + Ok(FirmwareDownloadResult { + firmware_path: out, + version: artifact.version.clone(), + source_url: artifact.url.clone(), + sha256: actual_hash, + verified_signature: true, + }) + } + + pub async fn preflight_firmware( + &self, + request: FirmwarePreflightRequest, + ) -> AppCoreResult { + let profile = device_profile_for(request.vid_pid); + if profile.support_tier != SupportTier::Full { + return Ok(FirmwarePreflightResult::denied( + AppPolicyGateReason::NotHardwareConfirmed, + "Firmware updates are available only after per-PID hardware confirmation." + .to_owned(), + )); + } + if !(request.allow_unsafe && request.brick_risk_ack) { + return Ok(FirmwarePreflightResult::denied( + AppPolicyGateReason::UnsafeFlagsMissing, + "Safety acknowledgement is required before firmware update".to_owned(), + )); + } + let image_meta = validate_firmware_image(&request.firmware_path).await?; + let chunk_size = request + .chunk_size + .unwrap_or(self.config.default_chunk_size) + .max(8); + let chunks_total = image_meta.bytes_total.div_ceil(chunk_size); + let expected_seconds = + ((chunks_total as u64 * self.config.progress_interval_ms) / 1000).max(1); + let session_id = Uuid::new_v4().to_string(); + let mut warnings = vec![ + "Do not disconnect device during transfer".to_owned(), + "Use only validated firmware images".to_owned(), + ]; + if has_unusual_firmware_extension(&request.firmware_path) { + warnings.push( + "Firmware filename extension is unusual. Continuing with strict content/hash validation." + .to_owned(), + ); + } + + let plan = FirmwareUpdatePlan { + session_id: FirmwareUpdateSessionId(session_id.clone()), + chunk_size, + bytes_total: image_meta.bytes_total, + chunks_total, + expected_seconds, + warnings, + image_sha256: image_meta.sha256, + current_version: "unknown".to_owned(), + target_version: image_meta + .target_version_hint + .unwrap_or_else(|| "unspecified".to_owned()), + }; + + let (sender, _) = broadcast::channel(128); + let handle = Arc::new(FirmwareSessionHandle { + request: request.clone(), + plan: plan.clone(), + sender, + runtime: Mutex::new(FirmwareSessionRuntime { + state: FirmwareSessionState::Preflight, + sequence: 0, + cancel_requested: false, + report: None, + started_at: None, + completed_at: None, + }), + }); + + self.sessions + .write() + .await + .insert(session_id.clone(), handle.clone()); + + emit_event(&handle, "preflight", 0, "Preflight complete", false).await; + + Ok(FirmwarePreflightResult { + gate: AppPolicyGateResult { + allowed: true, + reason: None, + message: None, + }, + plan: Some(plan), + capability: profile.capability, + evidence: profile.evidence, + }) + } + + pub async fn start_firmware( + &self, + request: FirmwareStartRequest, + ) -> AppCoreResult { + let handle = self.session_handle(&request.session_id.0).await?; + { + let mut runtime = handle.runtime.lock().await; + if runtime.state != FirmwareSessionState::Preflight { + return Err(AppCoreError::InvalidState( + "Firmware session must be in preflight state".to_owned(), + )); + } + runtime.state = FirmwareSessionState::AwaitingConfirmation; + } + emit_event( + &handle, + "awaiting_confirmation", + 0, + "Awaiting explicit confirmation", + false, + ) + .await; + + Ok(handle.plan.clone()) + } + + pub async fn confirm_firmware( + &self, + request: FirmwareConfirmRequest, + ) -> AppCoreResult { + if !request.acknowledged_risk { + return Err(AppCoreError::PolicyDenied { + reason: AppPolicyGateReason::UnsafeFlagsMissing, + message: "You must acknowledge firmware risk before continuing".to_owned(), + }); + } + + let handle = self.session_handle(&request.session_id.0).await?; + { + let mut runtime = handle.runtime.lock().await; + if runtime.state != FirmwareSessionState::AwaitingConfirmation { + return Err(AppCoreError::InvalidState( + "Firmware session is not awaiting confirmation".to_owned(), + )); + } + runtime.state = FirmwareSessionState::Running; + runtime.started_at = Some(Utc::now()); + runtime.cancel_requested = false; + } + + let interval = self.config.progress_interval_ms; + let plan = handle.plan.clone(); + let session_id = plan.session_id.clone(); + let sessions = self.sessions.clone(); + tokio::spawn(async move { + run_transfer_task(sessions, handle, interval, session_id).await; + }); + + Ok(plan) + } + + pub async fn cancel_firmware( + &self, + request: FirmwareCancelRequest, + ) -> AppCoreResult { + let handle = self.session_handle(&request.session_id.0).await?; + { + let mut runtime = handle.runtime.lock().await; + runtime.cancel_requested = true; + if matches!( + runtime.state, + FirmwareSessionState::Completed + | FirmwareSessionState::Cancelled + | FirmwareSessionState::Failed + ) { + if let Some(report) = runtime.report.clone() { + return Ok(report); + } + } + } + + emit_event( + &handle, + "cancel_requested", + 0, + "Cancellation requested", + false, + ) + .await; + + { + let mut runtime = handle.runtime.lock().await; + if matches!( + runtime.state, + FirmwareSessionState::Preflight | FirmwareSessionState::AwaitingConfirmation + ) { + runtime.state = FirmwareSessionState::Cancelled; + runtime.completed_at = Some(Utc::now()); + let report = FirmwareFinalReport { + session_id: handle.plan.session_id.clone(), + status: FirmwareOutcome::Cancelled, + started_at: runtime.started_at, + completed_at: runtime.completed_at, + bytes_total: handle.plan.bytes_total, + chunks_total: handle.plan.chunks_total, + chunks_sent: 0, + error_code: None, + message: "Firmware update cancelled before transfer".to_owned(), + }; + runtime.report = Some(report.clone()); + drop(runtime); + emit_event(&handle, "cancelled", 100, "Update cancelled", true).await; + return Ok(report); + } + } + + loop { + if let Some(report) = self.firmware_report(&request.session_id.0).await? { + return Ok(report); + } + sleep(Duration::from_millis(5)).await; + } + } + + pub async fn firmware_report( + &self, + session_id: &str, + ) -> AppCoreResult> { + let handle = self.session_handle(session_id).await?; + let runtime = handle.runtime.lock().await; + Ok(runtime.report.clone()) + } + + pub async fn subscribe_events( + &self, + session_id: &str, + ) -> AppCoreResult> { + let handle = self.session_handle(session_id).await?; + Ok(handle.sender.subscribe()) + } + + fn open_session_for_ops(&self, target: VidPid) -> AppCoreResult> { + let config = SessionConfig { + allow_unsafe: true, + brick_risk_ack: true, + experimental: self.advanced_mode(), + ..Default::default() + }; + DeviceSession::new(HidTransport::new(), target, config).map_err(AppCoreError::Protocol) + } + + async fn store_backup(&self, target: VidPid, payload: ConfigBackupPayload) -> ConfigBackupId { + let id = ConfigBackupId(Uuid::new_v4().to_string()); + let backup = ConfigBackup { + created_at: Utc::now(), + target, + payload, + }; + self.backups.write().await.insert(id.0.clone(), backup); + id + } + + async fn session_handle(&self, session_id: &str) -> AppCoreResult> { + let sessions = self.sessions.read().await; + sessions + .get(session_id) + .cloned() + .ok_or_else(|| AppCoreError::NotFound(format!("unknown session id: {session_id}"))) + } +} + +async fn verify_artifact_signature( + http: &reqwest::Client, + artifact: &FirmwareArtifact, + artifact_bytes: &[u8], +) -> AppCoreResult<()> { + if !artifact.signature.algorithm.eq_ignore_ascii_case("ed25519") { + return Err(AppCoreError::Manifest(format!( + "unsupported signature algorithm: {}", + artifact.signature.algorithm + ))); + } + + let sig_body = http + .get(&artifact.signature.url) + .send() + .await + .map_err(|err| AppCoreError::Download(format!("signature request failed: {err}")))? + .error_for_status() + .map_err(|err| AppCoreError::Download(format!("signature download failed: {err}")))? + .bytes() + .await + .map_err(|err| AppCoreError::Download(format!("signature read failed: {err}")))? + .to_vec(); + + let sig_bytes = if sig_body.len() == 64 { + sig_body + } else { + let text = String::from_utf8(sig_body).map_err(|err| { + AppCoreError::Manifest(format!("signature payload is not UTF-8/base64: {err}")) + })?; + base64::engine::general_purpose::STANDARD + .decode(text.trim()) + .map_err(|err| AppCoreError::Manifest(format!("invalid signature base64: {err}")))? + }; + + let sig = Signature::from_slice(&sig_bytes) + .map_err(|err| AppCoreError::Manifest(format!("invalid signature format: {err}")))?; + + let keys = [ + PINNED_ED25519_ACTIVE_PUBLIC_KEY_HEX, + PINNED_ED25519_NEXT_PUBLIC_KEY_HEX, + ]; + for key_hex in keys { + let key_bytes = hex::decode(key_hex) + .map_err(|err| AppCoreError::Manifest(format!("invalid pinned key hex: {err}")))?; + let key_array: [u8; 32] = key_bytes + .try_into() + .map_err(|_| AppCoreError::Manifest("pinned key length must be 32 bytes".to_owned()))?; + let key = VerifyingKey::from_bytes(&key_array) + .map_err(|err| AppCoreError::Manifest(format!("invalid pinned key bytes: {err}")))?; + if key.verify(artifact_bytes, &sig).is_ok() { + return Ok(()); + } + } + + Err(AppCoreError::PolicyDenied { + reason: AppPolicyGateReason::ImageValidationFailed, + message: "signature verification failed for active and next pinned keys".to_owned(), + }) +} + +async fn run_transfer_task( + sessions: Arc>>>, + handle: Arc, + interval_ms: u64, + session_id: FirmwareUpdateSessionId, +) { + let bytes = match tokio::fs::read(&handle.request.firmware_path).await { + Ok(bytes) => bytes, + Err(err) => { + finalize_failure( + &handle, + BitdoErrorCode::InvalidInput, + format!("Failed to read firmware image: {err}"), + ) + .await; + let mut map = sessions.write().await; + map.remove(&session_id.0); + return; + } + }; + + let mut chunks_sent = 0usize; + let total_chunks = handle.plan.chunks_total.max(1); + + for (idx, _chunk) in bytes.chunks(handle.plan.chunk_size).enumerate() { + { + let runtime = handle.runtime.lock().await; + if runtime.cancel_requested { + drop(runtime); + finalize_cancelled(&handle, chunks_sent).await; + let mut map = sessions.write().await; + map.remove(&session_id.0); + return; + } + } + + chunks_sent = idx + 1; + let progress = ((chunks_sent * 100) / total_chunks) as u8; + emit_event( + &handle, + "transfer", + progress, + format!("Transferred chunk {chunks_sent}/{total_chunks}"), + false, + ) + .await; + sleep(Duration::from_millis(interval_ms)).await; + } + + emit_event(&handle, "verify", 99, "Verifying firmware", false).await; + sleep(Duration::from_millis(interval_ms)).await; + + { + let mut runtime = handle.runtime.lock().await; + runtime.state = FirmwareSessionState::Completed; + runtime.completed_at = Some(Utc::now()); + let report = FirmwareFinalReport { + session_id: handle.plan.session_id.clone(), + status: FirmwareOutcome::Completed, + started_at: runtime.started_at, + completed_at: runtime.completed_at, + bytes_total: handle.plan.bytes_total, + chunks_total: handle.plan.chunks_total, + chunks_sent, + error_code: None, + message: "Firmware update completed".to_owned(), + }; + runtime.report = Some(report); + } + + emit_event(&handle, "completed", 100, "Firmware update completed", true).await; +} + +async fn finalize_failure( + handle: &Arc, + code: BitdoErrorCode, + message: String, +) { + { + let mut runtime = handle.runtime.lock().await; + runtime.state = FirmwareSessionState::Failed; + runtime.completed_at = Some(Utc::now()); + let report = FirmwareFinalReport { + session_id: handle.plan.session_id.clone(), + status: FirmwareOutcome::Failed, + started_at: runtime.started_at, + completed_at: runtime.completed_at, + bytes_total: handle.plan.bytes_total, + chunks_total: handle.plan.chunks_total, + chunks_sent: 0, + error_code: Some(code), + message: message.clone(), + }; + runtime.report = Some(report); + } + emit_event(handle, "failed", 100, message, true).await; +} + +async fn finalize_cancelled(handle: &Arc, chunks_sent: usize) { + { + let mut runtime = handle.runtime.lock().await; + runtime.state = FirmwareSessionState::Cancelled; + runtime.completed_at = Some(Utc::now()); + let report = FirmwareFinalReport { + session_id: handle.plan.session_id.clone(), + status: FirmwareOutcome::Cancelled, + started_at: runtime.started_at, + completed_at: runtime.completed_at, + bytes_total: handle.plan.bytes_total, + chunks_total: handle.plan.chunks_total, + chunks_sent, + error_code: None, + message: "Firmware update cancelled".to_owned(), + }; + runtime.report = Some(report); + } + emit_event(handle, "cancelled", 100, "Firmware update cancelled", true).await; +} + +async fn emit_event( + handle: &Arc, + stage: impl Into, + progress: u8, + message: impl Into, + terminal: bool, +) { + let mut runtime = handle.runtime.lock().await; + runtime.sequence += 1; + let event = FirmwareProgressEvent { + session_id: handle.plan.session_id.clone(), + sequence: runtime.sequence, + stage: stage.into(), + progress, + message: message.into(), + terminal, + timestamp: Utc::now(), + }; + let _ = handle.sender.send(event); +} + +struct FirmwareSessionHandle { + request: FirmwarePreflightRequest, + plan: FirmwareUpdatePlan, + sender: broadcast::Sender, + runtime: Mutex, +} + +#[derive(Clone, Debug)] +struct FirmwareSessionRuntime { + state: FirmwareSessionState, + sequence: u64, + cancel_requested: bool, + report: Option, + started_at: Option>, + completed_at: Option>, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum FirmwareSessionState { + Preflight, + AwaitingConfirmation, + Running, + Completed, + Cancelled, + Failed, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AppDevice { + 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, + pub serial: Option, + pub connected: bool, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub enum UserSupportStatus { + Supported, + InProgress, + Planned, + Blocked, +} + +impl UserSupportStatus { + pub fn as_str(self) -> &'static str { + match self { + UserSupportStatus::Supported => "Supported", + UserSupportStatus::InProgress => "In Progress", + UserSupportStatus::Planned => "Planned", + UserSupportStatus::Blocked => "Blocked", + } + } +} + +pub fn support_status_for_tier(tier: SupportTier) -> UserSupportStatus { + match tier { + SupportTier::Full => UserSupportStatus::Supported, + SupportTier::CandidateReadOnly => UserSupportStatus::InProgress, + SupportTier::DetectOnly => UserSupportStatus::Planned, + } +} + +impl AppDevice { + pub fn support_status(&self) -> UserSupportStatus { + support_status_for_tier(self.support_tier) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FirmwareManifest { + pub version: u32, + pub artifacts: Vec, +} + +impl FirmwareManifest { + fn recommended_for(&self, target: VidPid, family: ProtocolFamily) -> Option<&FirmwareArtifact> { + self.artifacts + .iter() + .find(|entry| { + entry.channel.eq_ignore_ascii_case("stable") + && entry.vid == target.vid + && entry.pid == target.pid + }) + .or_else(|| { + self.artifacts.iter().find(|entry| { + entry.channel.eq_ignore_ascii_case("stable") + && entry.vid == target.vid + && entry.protocol_family == family + }) + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FirmwareArtifact { + pub vid: u16, + pub pid: u16, + pub protocol_family: ProtocolFamily, + pub version: String, + pub channel: String, + pub url: String, + pub sha256: String, + pub signature: ManifestSignature, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ManifestSignature { + pub algorithm: String, + pub url: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FirmwareDownloadResult { + pub firmware_path: PathBuf, + pub version: String, + pub source_url: String, + pub sha256: String, + pub verified_signature: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FirmwarePreflightRequest { + pub vid_pid: VidPid, + pub firmware_path: PathBuf, + pub allow_unsafe: bool, + pub brick_risk_ack: bool, + pub experimental: bool, + pub chunk_size: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FirmwarePreflightResult { + pub gate: AppPolicyGateResult, + pub plan: Option, + pub capability: PidCapability, + pub evidence: SupportEvidence, +} + +impl FirmwarePreflightResult { + fn denied(reason: AppPolicyGateReason, message: String) -> Self { + Self { + gate: AppPolicyGateResult { + allowed: false, + reason: Some(reason), + message: Some(message), + }, + plan: None, + capability: PidCapability::identify_only(), + evidence: SupportEvidence::Inferred, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FirmwareStartRequest { + pub session_id: FirmwareUpdateSessionId, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FirmwareConfirmRequest { + pub session_id: FirmwareUpdateSessionId, + pub acknowledged_risk: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FirmwareCancelRequest { + pub session_id: FirmwareUpdateSessionId, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +pub struct FirmwareUpdateSessionId(pub String); + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FirmwareUpdatePlan { + pub session_id: FirmwareUpdateSessionId, + pub chunk_size: usize, + pub bytes_total: usize, + pub chunks_total: usize, + pub expected_seconds: u64, + pub warnings: Vec, + pub image_sha256: String, + pub current_version: String, + pub target_version: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FirmwareProgressEvent { + pub session_id: FirmwareUpdateSessionId, + pub sequence: u64, + pub stage: String, + pub progress: u8, + pub message: String, + pub terminal: bool, + pub timestamp: DateTime, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FirmwareFinalReport { + pub session_id: FirmwareUpdateSessionId, + pub status: FirmwareOutcome, + pub started_at: Option>, + pub completed_at: Option>, + pub bytes_total: usize, + pub chunks_total: usize, + pub chunks_sent: usize, + pub error_code: Option, + pub message: String, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub enum FirmwareOutcome { + Completed, + Cancelled, + Failed, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AppPolicyGateResult { + pub allowed: bool, + pub reason: Option, + pub message: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub enum AppPolicyGateReason { + UnsupportedPid, + NotHardwareConfirmed, + UnsafeFlagsMissing, + ExperimentalRequired, + VersionMismatch, + ImageValidationFailed, +} + +#[derive(Debug, Error)] +pub enum AppCoreError { + #[error("policy denied: {reason:?}: {message}")] + PolicyDenied { + reason: AppPolicyGateReason, + message: String, + }, + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("protocol error: {0}")] + Protocol(#[from] bitdo_proto::BitdoError), + #[error("download error: {0}")] + Download(String), + #[error("manifest error: {0}")] + Manifest(String), + #[error("not found: {0}")] + NotFound(String), + #[error("invalid state: {0}")] + InvalidState(String), +} + +pub type AppCoreResult = Result; + +#[derive(Clone, Debug)] +struct FirmwareImageMeta { + bytes_total: usize, + sha256: String, + target_version_hint: Option, +} + +async fn validate_firmware_image(path: &Path) -> AppCoreResult { + let bytes = tokio::fs::read(path).await?; + if bytes.is_empty() { + return Err(AppCoreError::PolicyDenied { + reason: AppPolicyGateReason::ImageValidationFailed, + message: "Firmware image is empty".to_owned(), + }); + } + if bytes.len() > 64 * 1024 * 1024 { + return Err(AppCoreError::PolicyDenied { + reason: AppPolicyGateReason::ImageValidationFailed, + message: "Firmware image exceeds 64MB limit".to_owned(), + }); + } + + Ok(FirmwareImageMeta { + bytes_total: bytes.len(), + sha256: sha256_hex(&bytes), + target_version_hint: None, + }) +} + +fn has_unusual_firmware_extension(path: &Path) -> bool { + !path + .extension() + .and_then(|e| e.to_str()) + .map(|e| matches!(e.to_ascii_lowercase().as_str(), "bin" | "fw")) + .unwrap_or(false) +} + +fn sha256_hex(bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(bytes); + hex::encode(hasher.finalize()) +} + +fn mock_device(vid_pid: VidPid, full: bool) -> AppDevice { + let profile = device_profile_for(vid_pid); + AppDevice { + vid_pid, + name: if full { + profile.name + } else { + "PID_MockDetectOnly".to_owned() + }, + support_level: if full { + SupportLevel::Full + } else { + SupportLevel::DetectOnly + }, + support_tier: if full { + SupportTier::Full + } else { + SupportTier::DetectOnly + }, + protocol_family: profile.protocol_family, + capability: if full { + profile.capability + } else { + PidCapability::identify_only() + }, + evidence: if full { + SupportEvidence::Confirmed + } else { + SupportEvidence::Inferred + }, + serial: Some(if full { + "MOCK-FULL-6009".to_owned() + } else { + "MOCK-DETECT-2100".to_owned() + }), + connected: true, + } +} + +fn mock_diag_probe(target: VidPid) -> DiagProbeResult { + let profile = device_profile_for(target); + DiagProbeResult { + 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, + transport_ready: true, + command_checks: vec![ + bitdo_proto::DiagCommandStatus { + command: bitdo_proto::CommandId::GetPid, + ok: true, + confidence: bitdo_proto::EvidenceConfidence::Confirmed, + is_experimental: false, + severity: bitdo_proto::DiagSeverity::Ok, + error_code: None, + detail: "ok".to_owned(), + }, + bitdo_proto::DiagCommandStatus { + command: bitdo_proto::CommandId::GetControllerVersion, + ok: true, + confidence: bitdo_proto::EvidenceConfidence::Confirmed, + is_experimental: false, + severity: bitdo_proto::DiagSeverity::Ok, + error_code: None, + detail: "ok".to_owned(), + }, + bitdo_proto::DiagCommandStatus { + command: bitdo_proto::CommandId::GetSuperButton, + ok: true, + confidence: bitdo_proto::EvidenceConfidence::Inferred, + is_experimental: true, + severity: bitdo_proto::DiagSeverity::Ok, + error_code: None, + detail: "ok".to_owned(), + }, + ], + } +} + +fn default_jp108_mappings() -> Vec { + DedicatedButtonId::ALL + .iter() + .copied() + .enumerate() + .map(|(idx, button)| DedicatedButtonMapping { + button, + target_hid_usage: (0x04 + idx as u16) & 0x00ff, + }) + .collect() +} + +fn default_u2_mappings() -> Vec { + U2ButtonId::ALL + .iter() + .copied() + .enumerate() + .map(|(idx, button)| U2ButtonMapping { + button, + target_hid_usage: 0x0100 + idx as u16, + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_req(path: PathBuf, pid: u16) -> FirmwarePreflightRequest { + FirmwarePreflightRequest { + vid_pid: VidPid::new(0x2dc8, pid), + firmware_path: path, + allow_unsafe: true, + brick_risk_ack: true, + experimental: true, + chunk_size: Some(32), + } + } + + #[tokio::test] + async fn preflight_blocks_detect_only_pid() { + let core = OpenBitdoCore::new(OpenBitdoCoreConfig::default()); + let path = std::env::temp_dir().join("openbitdo-detect-only.bin"); + tokio::fs::write(&path, vec![1u8; 256]) + .await + .expect("write"); + let req = make_req(path.clone(), 0x2100); + let result = core.preflight_firmware(req).await.expect("preflight"); + assert!(!result.gate.allowed); + assert_eq!( + result.gate.reason, + Some(AppPolicyGateReason::NotHardwareConfirmed) + ); + let _ = tokio::fs::remove_file(path).await; + } + + #[tokio::test] + async fn firmware_happy_path_reaches_completed_report() { + let core = OpenBitdoCore::new(OpenBitdoCoreConfig { + mock_mode: true, + advanced_mode: false, + default_chunk_size: 16, + progress_interval_ms: 1, + firmware_manifest_url: DEFAULT_MANIFEST_URL.to_owned(), + }); + let path = std::env::temp_dir().join("openbitdo-happy.bin"); + tokio::fs::write(&path, vec![2u8; 128]) + .await + .expect("write"); + + let req = make_req(path.clone(), 0x6009); + let preflight = core.preflight_firmware(req).await.expect("preflight"); + assert!(preflight.gate.allowed); + let plan = preflight.plan.expect("plan"); + + core.start_firmware(FirmwareStartRequest { + session_id: plan.session_id.clone(), + }) + .await + .expect("start"); + + core.confirm_firmware(FirmwareConfirmRequest { + session_id: plan.session_id.clone(), + acknowledged_risk: true, + }) + .await + .expect("confirm"); + + loop { + if let Some(report) = core + .firmware_report(&plan.session_id.0) + .await + .expect("report") + { + assert_eq!(report.status, FirmwareOutcome::Completed); + break; + } + sleep(Duration::from_millis(2)).await; + } + + let _ = tokio::fs::remove_file(path).await; + } + + #[tokio::test] + async fn mock_download_returns_valid_file() { + let core = OpenBitdoCore::new(OpenBitdoCoreConfig { + mock_mode: true, + ..Default::default() + }); + + let result = core + .download_recommended_firmware(VidPid::new(0x2dc8, 0x6009)) + .await + .expect("download"); + + let bytes = tokio::fs::read(&result.firmware_path) + .await + .expect("read downloaded file"); + assert!(!bytes.is_empty()); + assert_eq!(result.version, "mock-1.0.0"); + + let _ = tokio::fs::remove_file(result.firmware_path).await; + } + + #[tokio::test] + async fn jp108_mock_mapping_roundtrip_supports_backup_and_restore() { + let core = OpenBitdoCore::new(OpenBitdoCoreConfig { + mock_mode: true, + ..Default::default() + }); + let target = VidPid::new(0x2dc8, 0x5209); + + let mappings = core + .jp108_read_dedicated_mapping(target) + .await + .expect("read mappings"); + assert_eq!(mappings.len(), DedicatedButtonId::ALL.len()); + + let backup_id = core + .jp108_apply_dedicated_mapping( + target, + vec![DedicatedButtonMapping { + button: DedicatedButtonId::A, + target_hid_usage: 0x2c, + }], + true, + ) + .await + .expect("apply mappings") + .expect("backup id"); + + core.restore_backup(backup_id) + .await + .expect("restore backup"); + } + + #[tokio::test] + async fn u2_mock_profile_roundtrip_supports_backup_and_restore() { + let core = OpenBitdoCore::new(OpenBitdoCoreConfig { + mock_mode: true, + ..Default::default() + }); + let target = VidPid::new(0x2dc8, 0x6012); + + let profile = core + .u2_read_core_profile(target, U2SlotId::Slot1) + .await + .expect("read profile"); + assert_eq!(profile.slot, U2SlotId::Slot1); + assert!(!profile.mappings.is_empty()); + + let backup_id = core + .u2_apply_core_profile( + target, + U2SlotId::Slot1, + 1, + vec![U2ButtonMapping { + button: U2ButtonId::A, + target_hid_usage: 0x0110, + }], + 0.5, + 0.5, + true, + ) + .await + .expect("apply profile") + .expect("backup id"); + + core.restore_backup(backup_id) + .await + .expect("restore backup"); + } + + #[tokio::test] + async fn guided_button_test_returns_beginner_guidance() { + let core = OpenBitdoCore::new(OpenBitdoCoreConfig { + mock_mode: true, + ..Default::default() + }); + + let result = core + .guided_button_test( + DeviceKind::Jp108, + vec!["A -> Space".to_owned(), "K1 -> Enter".to_owned()], + ) + .await + .expect("guided test"); + assert!(result.passed); + assert!(result.guidance.contains("JP108")); + } + + #[test] + fn support_status_maps_from_tier() { + assert_eq!( + support_status_for_tier(SupportTier::Full), + UserSupportStatus::Supported + ); + assert_eq!( + support_status_for_tier(SupportTier::CandidateReadOnly), + UserSupportStatus::InProgress + ); + assert_eq!( + support_status_for_tier(SupportTier::DetectOnly), + UserSupportStatus::Planned + ); + } +} diff --git a/sdk/crates/bitdo_proto/Cargo.toml b/sdk/crates/bitdo_proto/Cargo.toml index 7252023..2d004e9 100644 --- a/sdk/crates/bitdo_proto/Cargo.toml +++ b/sdk/crates/bitdo_proto/Cargo.toml @@ -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" diff --git a/sdk/crates/bitdo_proto/build.rs b/sdk/crates/bitdo_proto/build.rs deleted file mode 100644 index dfe3fe6..0000000 --- a/sdk/crates/bitdo_proto/build.rs +++ /dev/null @@ -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::() - .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::().expect("invalid u8") - } -} - -fn hex_to_bytes(hex: &str) -> Vec { - 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 -} diff --git a/sdk/crates/bitdo_proto/src/command.rs b/sdk/crates/bitdo_proto/src/command.rs index 4dbd812..398d937 100644 --- a/sdk/crates/bitdo_proto/src/command.rs +++ b/sdk/crates/bitdo_proto/src/command.rs @@ -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] { diff --git a/sdk/crates/bitdo_proto/src/command_registry_table.rs b/sdk/crates/bitdo_proto/src/command_registry_table.rs new file mode 100644 index 0000000..c6d8cb9 --- /dev/null +++ b/sdk/crates/bitdo_proto/src/command_registry_table.rs @@ -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" }, +] +; diff --git a/sdk/crates/bitdo_proto/src/hid_transport.rs b/sdk/crates/bitdo_proto/src/hid_transport.rs index 8c679d2..f3b07ec 100644 --- a/sdk/crates/bitdo_proto/src/hid_transport.rs +++ b/sdk/crates/bitdo_proto/src/hid_transport.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "hidapi-backend")] - use crate::error::{BitdoError, Result}; use crate::transport::Transport; use crate::types::VidPid; diff --git a/sdk/crates/bitdo_proto/src/lib.rs b/sdk/crates/bitdo_proto/src/lib.rs index d118736..6922932 100644 --- a/sdk/crates/bitdo_proto/src/lib.rs +++ b/sdk/crates/bitdo_proto/src/lib.rs @@ -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, }; diff --git a/sdk/crates/bitdo_proto/src/pid_registry_table.rs b/sdk/crates/bitdo_proto/src/pid_registry_table.rs new file mode 100644 index 0000000..9bf511e --- /dev/null +++ b/sdk/crates/bitdo_proto/src/pid_registry_table.rs @@ -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 }, +] +; diff --git a/sdk/crates/bitdo_proto/src/registry.rs b/sdk/crates/bitdo_proto/src/registry.rs index af9a0e7..b2dd666 100644 --- a/sdk/crates/bitdo_proto/src/registry.rs +++ b/sdk/crates/bitdo_proto/src/registry.rs @@ -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 + ); + } + }); +} diff --git a/sdk/crates/bitdo_proto/src/session.rs b/sdk/crates/bitdo_proto/src/session.rs index 95765f8..b025c8f 100644 --- a/sdk/crates/bitdo_proto/src/session.rs +++ b/sdk/crates/bitdo_proto/src/session.rs @@ -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, 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 DeviceSession { 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 DeviceSession { } 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::>(); + 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 DeviceSession { Ok(()) } + pub fn jp108_read_dedicated_mappings(&mut self) -> Result> { + 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 { + 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> { + 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> { + 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 { + 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 DeviceSession { fn ensure_command_allowed(&self, command: CommandId) -> Result<&'static CommandRegistryRow> { let row = find_command(command).ok_or(BitdoError::UnknownCommand(command))?; - if row.confidence == CommandConfidence::Inferred && !self.config.experimental { - return Err(BitdoError::ExperimentalRequired { command }); + // Gate 1: confidence/runtime policy. + // We intentionally keep inferred write/unsafe paths non-executable until + // they are upgraded to confirmed evidence. + match row.runtime_policy() { + CommandRuntimePolicy::EnabledDefault => {} + CommandRuntimePolicy::ExperimentalGate => { + if !self.config.experimental { + return Err(BitdoError::ExperimentalRequired { command }); + } + } + CommandRuntimePolicy::BlockedUntilConfirmed => { + return Err(BitdoError::UnsupportedForPid { + command, + pid: self.target.pid, + }); + } } + // Gate 2: PID/family/capability applicability. if !is_command_allowed_by_family(self.profile.protocol_family, command) || !is_command_allowed_by_capability(self.profile.capability, command) + || !command_applies_to_pid(row, self.target.pid) { return Err(BitdoError::UnsupportedForPid { command, @@ -542,8 +716,19 @@ impl DeviceSession { }); } + // 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 DeviceSession { } 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 DeviceSession { } } +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 { 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 +} diff --git a/sdk/crates/bitdo_proto/src/types.rs b/sdk/crates/bitdo_proto/src/types.rs index 503388e..4d1719c 100644 --- a/sdk/crates/bitdo_proto/src/types.rs +++ b/sdk/crates/bitdo_proto/src/types.rs @@ -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, diff --git a/sdk/crates/bitdo_tui/Cargo.toml b/sdk/crates/bitdo_tui/Cargo.toml new file mode 100644 index 0000000..421694b --- /dev/null +++ b/sdk/crates/bitdo_tui/Cargo.toml @@ -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"] } diff --git a/sdk/crates/bitdo_tui/src/desktop_io.rs b/sdk/crates/bitdo_tui/src/desktop_io.rs new file mode 100644 index 0000000..dd624f0 --- /dev/null +++ b/sdk/crates/bitdo_tui/src/desktop_io.rs @@ -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}")) + } +} diff --git a/sdk/crates/bitdo_tui/src/lib.rs b/sdk/crates/bitdo_tui/src/lib.rs new file mode 100644 index 0000000..ec44fc0 --- /dev/null +++ b/sdk/crates/bitdo_tui/src/lib.rs @@ -0,0 +1,3151 @@ +use anyhow::{anyhow, Result}; +use bitdo_app_core::{ + AppDevice, ConfigBackupId, DedicatedButtonMapping, DeviceKind, FirmwareCancelRequest, + FirmwareConfirmRequest, FirmwareFinalReport, FirmwareOutcome, FirmwarePreflightRequest, + FirmwareProgressEvent, FirmwareStartRequest, FirmwareUpdatePlan, FirmwareUpdateSessionId, + OpenBitdoCore, U2CoreProfile, U2SlotId, UserSupportStatus, WriteRecoveryReport, +}; +use bitdo_proto::{SupportTier, VidPid}; +use crossterm::event::{self, Event as CEvent, KeyCode, MouseButton, MouseEvent, MouseEventKind}; +use crossterm::execute; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, +}; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, Gauge, List, ListItem, Paragraph}; +use ratatui::{backend::CrosstermBackend, Frame, Terminal}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::io::{self, Stdout, Write}; +use std::path::{Path, PathBuf}; +use tokio::sync::broadcast; +use tokio::time::{sleep, Duration}; + +mod desktop_io; +mod settings; +mod support_report; + +use desktop_io::{copy_text_to_clipboard, open_path_with_default_app}; +use settings::persist_user_settings; +use support_report::{persist_support_report, prune_reports_on_startup}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum TuiWorkflowState { + WaitForDevice, + Home, + HelpOverlay, + Jp108Mapping, + U2CoreProfile, + Recovery, + Preflight, + Updating, + FinalReport, + About, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +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 Default for BuildInfo { + fn default() -> Self { + Self { + app_version: "unknown".to_owned(), + git_commit_short: "unknown".to_owned(), + git_commit_full: "unknown".to_owned(), + build_date_utc: "unknown".to_owned(), + target_triple: "unknown".to_owned(), + runtime_platform: format!("{}/{}", std::env::consts::OS, std::env::consts::ARCH), + signing_key_fingerprint_short: "unknown".to_owned(), + signing_key_fingerprint_full: "unknown".to_owned(), + signing_key_next_fingerprint_short: "unknown".to_owned(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum ReportSaveMode { + Off, + Always, + #[default] + FailureOnly, +} + +impl ReportSaveMode { + pub fn as_str(self) -> &'static str { + match self { + ReportSaveMode::Off => "off", + ReportSaveMode::Always => "always", + ReportSaveMode::FailureOnly => "failure_only", + } + } + + fn next(self, advanced_mode: bool) -> Self { + match (self, advanced_mode) { + (ReportSaveMode::FailureOnly, false) => ReportSaveMode::Always, + (ReportSaveMode::Always, false) => ReportSaveMode::FailureOnly, + (ReportSaveMode::Off, false) => ReportSaveMode::FailureOnly, + (ReportSaveMode::FailureOnly, true) => ReportSaveMode::Always, + (ReportSaveMode::Always, true) => ReportSaveMode::Off, + (ReportSaveMode::Off, true) => ReportSaveMode::FailureOnly, + } + } +} + +#[derive(Clone, Debug)] +struct PendingUpdate { + target: AppDevice, + firmware_path: PathBuf, + firmware_source: String, + firmware_version: String, + plan: FirmwareUpdatePlan, +} + +#[derive(Clone, Copy, Debug)] +struct MouseContextMenu { + anchor_col: u16, + anchor_row: u16, + hovered_index: Option, +} + +#[derive(Clone, Debug)] +pub struct TuiApp { + pub state: TuiWorkflowState, + pub devices: Vec, + pub selected_index: usize, + pub selected: Option, + pub session_id: Option, + pub progress: u8, + pub last_message: String, + pub final_report: Option, + pub build_info: BuildInfo, + pub advanced_mode: bool, + pub report_save_mode: ReportSaveMode, + pub settings_path: Option, + detail_scroll: u16, + hovered_action: Option, + about_toggle_hovered: bool, + about_report_mode_hovered: bool, + about_fingerprint_hovered: bool, + about_show_full_fingerprint: bool, + context_menu: Option, + pending_update: Option, + jp108_mappings: Vec, + jp108_selected: usize, + u2_profile: Option, + u2_selected: usize, + latest_backup: Option, + latest_report_path: Option, + write_lock_until_restart: bool, + recovery_report: Option, +} + +impl Default for TuiApp { + fn default() -> Self { + Self { + state: TuiWorkflowState::WaitForDevice, + devices: Vec::new(), + selected_index: 0, + selected: None, + session_id: None, + progress: 0, + last_message: "Plug in your controller, then choose Refresh.".to_owned(), + final_report: None, + build_info: BuildInfo::default(), + advanced_mode: false, + report_save_mode: ReportSaveMode::FailureOnly, + settings_path: None, + detail_scroll: 0, + hovered_action: None, + about_toggle_hovered: false, + about_report_mode_hovered: false, + about_fingerprint_hovered: false, + about_show_full_fingerprint: false, + context_menu: None, + pending_update: None, + jp108_mappings: Vec::new(), + jp108_selected: 0, + u2_profile: None, + u2_selected: 0, + latest_backup: None, + latest_report_path: None, + write_lock_until_restart: false, + recovery_report: None, + } + } +} + +impl TuiApp { + pub fn refresh_devices(&mut self, mut devices: Vec) { + devices.sort_by_key(|d| (d.vid_pid.vid, d.vid_pid.pid)); + self.devices = devices; + + if self.devices.is_empty() { + self.selected_index = 0; + self.selected = None; + if !self.is_overlay_state() { + self.state = TuiWorkflowState::WaitForDevice; + } + return; + } + + if self.devices.len() == 1 || self.selected_index >= self.devices.len() { + self.selected_index = 0; + } + + self.selected = Some(self.devices[self.selected_index].vid_pid); + + if !matches!( + self.state, + TuiWorkflowState::About + | TuiWorkflowState::HelpOverlay + | TuiWorkflowState::Recovery + | TuiWorkflowState::Preflight + | TuiWorkflowState::Updating + | TuiWorkflowState::FinalReport + ) { + self.state = TuiWorkflowState::Home; + } + } + + pub fn selected_device(&self) -> Option<&AppDevice> { + self.devices.get(self.selected_index) + } + + pub fn select_next(&mut self) { + if self.devices.is_empty() { + return; + } + self.selected_index = (self.selected_index + 1) % self.devices.len(); + self.selected = Some(self.devices[self.selected_index].vid_pid); + self.context_menu = None; + } + + pub fn select_prev(&mut self) { + if self.devices.is_empty() { + return; + } + if self.selected_index == 0 { + self.selected_index = self.devices.len() - 1; + } else { + self.selected_index -= 1; + } + self.selected = Some(self.devices[self.selected_index].vid_pid); + self.context_menu = None; + } + + pub fn select_index(&mut self, idx: usize) { + if idx < self.devices.len() { + self.selected_index = idx; + self.selected = Some(self.devices[idx].vid_pid); + self.context_menu = None; + } + } + + pub fn set_home_message(&mut self, message: impl Into) { + self.progress = 0; + self.session_id = None; + self.pending_update = None; + self.context_menu = None; + self.detail_scroll = 0; + self.last_message = message.into(); + self.state = if self.devices.is_empty() { + TuiWorkflowState::WaitForDevice + } else { + TuiWorkflowState::Home + }; + } + + pub fn open_about(&mut self) { + self.state = TuiWorkflowState::About; + self.context_menu = None; + self.about_toggle_hovered = false; + self.about_report_mode_hovered = false; + self.about_fingerprint_hovered = false; + self.last_message = "OpenBitdo build details and settings.".to_owned(); + } + + pub fn open_help(&mut self) { + self.state = TuiWorkflowState::HelpOverlay; + self.context_menu = None; + } + + pub fn close_overlay(&mut self) { + self.about_toggle_hovered = false; + self.about_report_mode_hovered = false; + self.about_fingerprint_hovered = false; + self.about_show_full_fingerprint = false; + if self.devices.is_empty() { + self.state = TuiWorkflowState::WaitForDevice; + } else { + self.state = TuiWorkflowState::Home; + } + } + + fn begin_preflight(&mut self, pending: PendingUpdate) { + self.pending_update = Some(pending); + self.state = TuiWorkflowState::Preflight; + self.progress = 0; + self.session_id = None; + self.final_report = None; + self.context_menu = None; + self.last_message = "Review preflight details and confirm.".to_owned(); + } + + fn begin_jp108_mapping(&mut self, mappings: Vec) { + self.jp108_mappings = mappings; + self.jp108_selected = 0; + self.state = TuiWorkflowState::Jp108Mapping; + self.pending_update = None; + self.context_menu = None; + self.last_message = + "Edit dedicated buttons, then click Backup + Apply. Firmware remains available." + .to_owned(); + } + + fn begin_u2_profile(&mut self, profile: U2CoreProfile) { + self.u2_profile = Some(profile); + self.u2_selected = 0; + self.state = TuiWorkflowState::U2CoreProfile; + self.pending_update = None; + self.context_menu = None; + self.last_message = + "Choose slot/mode and core button mappings, then click Backup + Apply.".to_owned(); + } + + pub fn set_session(&mut self, id: FirmwareUpdateSessionId) { + self.session_id = Some(id); + self.state = TuiWorkflowState::Updating; + self.context_menu = None; + self.pending_update = None; + } + + pub fn apply_progress(&mut self, progress: u8, message: String) { + self.progress = progress; + self.last_message = message; + } + + pub fn complete(&mut self, report: FirmwareFinalReport) { + self.progress = 100; + self.state = TuiWorkflowState::FinalReport; + self.last_message = format!("final status: {:?}", report.status); + self.final_report = Some(report); + self.session_id = None; + self.pending_update = None; + self.context_menu = None; + } + + pub fn open_context_menu(&mut self, col: u16, row: u16) { + self.context_menu = Some(MouseContextMenu { + anchor_col: col, + anchor_row: row, + hovered_index: None, + }); + } + + pub fn close_context_menu(&mut self) { + self.context_menu = None; + } + + fn set_advanced_mode(&mut self, core: &OpenBitdoCore, enabled: bool) -> Result<()> { + self.advanced_mode = enabled; + core.set_advanced_mode(enabled); + if !enabled && self.report_save_mode == ReportSaveMode::Off { + self.report_save_mode = ReportSaveMode::FailureOnly; + } + if let Some(path) = self.settings_path.as_deref() { + persist_user_settings(path, self.advanced_mode, self.report_save_mode)?; + } + self.last_message = if enabled { + "Advanced mode enabled: inferred read diagnostics are available.".to_owned() + } else { + "Advanced mode disabled: beginner-safe defaults restored.".to_owned() + }; + Ok(()) + } + + fn toggle_advanced_mode(&mut self, core: &OpenBitdoCore) -> Result<()> { + self.set_advanced_mode(core, !self.advanced_mode) + } + + fn cycle_report_save_mode(&mut self) -> Result<()> { + self.report_save_mode = self.report_save_mode.next(self.advanced_mode); + if let Some(path) = self.settings_path.as_deref() { + persist_user_settings(path, self.advanced_mode, self.report_save_mode)?; + } + self.last_message = if self.report_save_mode == ReportSaveMode::Off { + "Report save mode set to off. Disabling reports may make support impossible.".to_owned() + } else { + format!( + "Report save mode set to {}.", + self.report_save_mode.as_str() + ) + }; + Ok(()) + } + + fn toggle_fingerprint_view(&mut self) { + self.about_show_full_fingerprint = !self.about_show_full_fingerprint; + self.last_message = if self.about_show_full_fingerprint { + "Showing full signing-key fingerprint.".to_owned() + } else { + "Showing short signing-key fingerprint.".to_owned() + }; + } + + fn enter_recovery(&mut self, report: WriteRecoveryReport) { + self.write_lock_until_restart = true; + self.recovery_report = Some(report); + self.state = TuiWorkflowState::Recovery; + self.pending_update = None; + self.session_id = None; + self.progress = 0; + self.last_message = + "Apply failed and rollback also failed. Write actions are locked until restart." + .to_owned(); + } + + fn remember_report_path(&mut self, path: PathBuf) { + self.latest_report_path = Some(path); + } + + fn is_overlay_state(&self) -> bool { + matches!( + self.state, + TuiWorkflowState::About | TuiWorkflowState::HelpOverlay + ) + } +} + +#[derive(Clone, Debug)] +pub struct TuiRunRequest { + pub vid_pid: VidPid, + pub firmware_path: PathBuf, + pub allow_unsafe: bool, + pub brick_risk_ack: bool, + pub experimental: bool, + pub chunk_size: Option, + pub acknowledged_risk: bool, + pub no_ui: bool, +} + +#[derive(Clone, Debug)] +pub struct TuiLaunchOptions { + pub no_ui: bool, + pub selected_vid_pid: Option, + pub firmware_path: Option, + pub allow_unsafe: bool, + pub brick_risk_ack: bool, + pub experimental: bool, + pub chunk_size: Option, + pub build_info: BuildInfo, + pub advanced_mode: bool, + pub report_save_mode: ReportSaveMode, + pub settings_path: Option, +} + +impl Default for TuiLaunchOptions { + fn default() -> Self { + Self { + no_ui: false, + selected_vid_pid: None, + firmware_path: None, + allow_unsafe: true, + brick_risk_ack: true, + experimental: false, + chunk_size: None, + build_info: BuildInfo::default(), + advanced_mode: false, + report_save_mode: ReportSaveMode::FailureOnly, + settings_path: None, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum HomeAction { + Update, + Diagnose, + Refresh, + About, + Help, + Quit, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DeviceFlowAction { + BackupApply, + RetryRead, + RestoreBackup, + GuidedTest, + Firmware, + Done, +} + +const HOME_ACTIONS: [HomeAction; 5] = [ + HomeAction::Update, + HomeAction::Diagnose, + HomeAction::Refresh, + HomeAction::About, + HomeAction::Quit, +]; + +const WAIT_ACTIONS: [HomeAction; 3] = [HomeAction::Refresh, HomeAction::Help, HomeAction::Quit]; + +const CONTEXT_ACTIONS: [HomeAction; 3] = + [HomeAction::Diagnose, HomeAction::About, HomeAction::Refresh]; + +const DEVICE_FLOW_ACTIONS: [DeviceFlowAction; 6] = [ + DeviceFlowAction::BackupApply, + DeviceFlowAction::RetryRead, + DeviceFlowAction::RestoreBackup, + DeviceFlowAction::GuidedTest, + DeviceFlowAction::Firmware, + DeviceFlowAction::Done, +]; + +impl HomeAction { + fn label(self) -> &'static str { + match self { + HomeAction::Update => "Recommended Update", + HomeAction::Diagnose => "Diagnose", + HomeAction::Refresh => "Refresh", + HomeAction::About => "About", + HomeAction::Help => "Help", + HomeAction::Quit => "Quit", + } + } +} + +impl DeviceFlowAction { + fn label(self) -> &'static str { + match self { + DeviceFlowAction::BackupApply => "Backup + Apply", + DeviceFlowAction::RetryRead => "Retry Read", + DeviceFlowAction::RestoreBackup => "Restore", + DeviceFlowAction::GuidedTest => "Button Test", + DeviceFlowAction::Firmware => "Firmware", + DeviceFlowAction::Done => "Done", + } + } +} + +pub async fn run_tui_app(core: OpenBitdoCore, opts: TuiLaunchOptions) -> Result<()> { + let _ = prune_reports_on_startup().await; + let initial_report_mode = if !opts.advanced_mode && opts.report_save_mode == ReportSaveMode::Off + { + ReportSaveMode::FailureOnly + } else { + opts.report_save_mode + }; + + let mut app = TuiApp { + build_info: opts.build_info.clone(), + advanced_mode: opts.advanced_mode, + report_save_mode: initial_report_mode, + settings_path: opts.settings_path.clone(), + ..Default::default() + }; + core.set_advanced_mode(opts.advanced_mode); + + let devices = core.list_devices().await?; + app.refresh_devices(devices); + match app.devices.len() { + 0 => { + app.last_message = + "No controller detected. Plug one in, then choose Refresh.".to_owned() + } + 1 => { + app.last_message = + "Controller detected and auto-selected. Choose Recommended Update or Diagnose." + .to_owned() + } + _ => { + app.last_message = + "Select a controller, then choose Recommended Update or Diagnose.".to_owned() + } + } + + if opts.no_ui { + let selected = opts + .selected_vid_pid + .or_else(|| { + app.devices + .iter() + .find(|d| d.support_tier == SupportTier::Full) + .map(|d| d.vid_pid) + }) + .or_else(|| app.devices.first().map(|d| d.vid_pid)) + .ok_or_else(|| anyhow!("no devices detected"))?; + + let firmware_path = match opts.firmware_path.clone() { + Some(path) => path, + None => core + .download_recommended_firmware(selected) + .await + .map(|d| d.firmware_path)?, + }; + + run_tui_flow( + core, + TuiRunRequest { + vid_pid: selected, + firmware_path, + allow_unsafe: opts.allow_unsafe, + brick_risk_ack: opts.brick_risk_ack, + experimental: opts.experimental, + chunk_size: opts.chunk_size, + acknowledged_risk: true, + no_ui: true, + }, + ) + .await?; + + return Ok(()); + } + + let mut terminal = Some(init_terminal()?); + let mut firmware_events: Option> = None; + + if app.devices.len() == 1 { + let action = if app + .selected_device() + .map(|d| d.support_tier == SupportTier::Full) + .unwrap_or(false) + { + HomeAction::Update + } else { + HomeAction::Diagnose + }; + let _ = execute_home_action( + &core, + &mut terminal, + &mut app, + &opts, + &mut firmware_events, + action, + ) + .await?; + } + + loop { + poll_firmware_progress(&core, &mut app, &mut firmware_events).await?; + render_if_needed(&mut terminal, &app)?; + + if !event::poll(Duration::from_millis(120))? { + continue; + } + + match event::read()? { + CEvent::Key(key) => { + if handle_key_event( + &core, + &mut terminal, + &mut app, + &opts, + &mut firmware_events, + key.code, + ) + .await? + { + teardown_terminal(&mut terminal)?; + return Ok(()); + } + } + CEvent::Mouse(mouse) => { + if handle_mouse_event( + &core, + &mut terminal, + &mut app, + &opts, + &mut firmware_events, + mouse, + ) + .await? + { + teardown_terminal(&mut terminal)?; + return Ok(()); + } + } + _ => {} + } + } +} + +async fn handle_key_event( + core: &OpenBitdoCore, + terminal: &mut Option>>, + app: &mut TuiApp, + opts: &TuiLaunchOptions, + firmware_events: &mut Option>, + key: KeyCode, +) -> Result { + if app.advanced_mode && handle_report_hotkey(app, key)? { + return Ok(false); + } + + match app.state { + TuiWorkflowState::About => match key { + KeyCode::Esc | KeyCode::Enter => app.close_overlay(), + KeyCode::Char('t') => { + app.toggle_advanced_mode(core)?; + } + KeyCode::Char('r') => { + app.cycle_report_save_mode()?; + } + KeyCode::Char('v') => app.toggle_fingerprint_view(), + KeyCode::Char('q') => return Ok(true), + _ => {} + }, + TuiWorkflowState::HelpOverlay => match key { + KeyCode::Esc | KeyCode::Enter => app.close_overlay(), + KeyCode::Char('q') => return Ok(true), + _ => {} + }, + TuiWorkflowState::WaitForDevice => { + let action = match key { + KeyCode::Enter | KeyCode::Char('r') => Some(HomeAction::Refresh), + KeyCode::Char('?') => Some(HomeAction::Help), + KeyCode::Char('q') | KeyCode::Esc => Some(HomeAction::Quit), + _ => None, + }; + + if let Some(action) = action { + return execute_home_action(core, terminal, app, opts, firmware_events, action) + .await; + } + } + TuiWorkflowState::Home => { + let action = match key { + KeyCode::Char('q') | KeyCode::Esc => Some(HomeAction::Quit), + KeyCode::Down | KeyCode::Char('j') => { + app.select_next(); + None + } + KeyCode::Up | KeyCode::Char('k') => { + app.select_prev(); + None + } + KeyCode::Char('d') => Some(HomeAction::Diagnose), + KeyCode::Char('r') => Some(HomeAction::Refresh), + KeyCode::Char('a') => Some(HomeAction::About), + KeyCode::Char('?') => Some(HomeAction::Help), + KeyCode::Enter | KeyCode::Char('u') => Some(HomeAction::Update), + _ => None, + }; + + if let Some(action) = action { + return execute_home_action(core, terminal, app, opts, firmware_events, action) + .await; + } + } + TuiWorkflowState::Jp108Mapping => { + let action = match key { + KeyCode::Down | KeyCode::Char('j') => { + if !app.jp108_mappings.is_empty() { + app.jp108_selected = (app.jp108_selected + 1) % app.jp108_mappings.len(); + } + None + } + KeyCode::Up | KeyCode::Char('k') => { + if !app.jp108_mappings.is_empty() { + if app.jp108_selected == 0 { + app.jp108_selected = app.jp108_mappings.len().saturating_sub(1); + } else { + app.jp108_selected -= 1; + } + } + None + } + KeyCode::Left => { + jp108_adjust_selected_usage(app, -1); + None + } + KeyCode::Right => { + jp108_adjust_selected_usage(app, 1); + None + } + KeyCode::Enter => Some(DeviceFlowAction::BackupApply), + KeyCode::Char('b') => Some(DeviceFlowAction::BackupApply), + KeyCode::Char('r') => Some(DeviceFlowAction::RetryRead), + KeyCode::Char('s') => Some(DeviceFlowAction::RestoreBackup), + KeyCode::Char('t') => Some(DeviceFlowAction::GuidedTest), + KeyCode::Char('f') => Some(DeviceFlowAction::Firmware), + KeyCode::Esc | KeyCode::Char('q') => Some(DeviceFlowAction::Done), + _ => None, + }; + + if let Some(action) = action { + return execute_device_flow_action( + core, + terminal, + app, + opts, + firmware_events, + action, + ) + .await; + } + } + TuiWorkflowState::U2CoreProfile => { + let action = match key { + KeyCode::Down | KeyCode::Char('j') => { + if let Some(profile) = app.u2_profile.as_ref() { + if !profile.mappings.is_empty() { + app.u2_selected = (app.u2_selected + 1) % profile.mappings.len(); + } + } + None + } + KeyCode::Up | KeyCode::Char('k') => { + if let Some(profile) = app.u2_profile.as_ref() { + if !profile.mappings.is_empty() { + if app.u2_selected == 0 { + app.u2_selected = profile.mappings.len().saturating_sub(1); + } else { + app.u2_selected -= 1; + } + } + } + None + } + KeyCode::Left => { + u2_adjust_selected_usage(app, -1); + None + } + KeyCode::Right => { + u2_adjust_selected_usage(app, 1); + None + } + KeyCode::Char('1') => { + if let Some(profile) = app.u2_profile.as_mut() { + profile.slot = U2SlotId::Slot1; + } + None + } + KeyCode::Char('2') => { + if let Some(profile) = app.u2_profile.as_mut() { + profile.slot = U2SlotId::Slot2; + } + None + } + KeyCode::Char('3') => { + if let Some(profile) = app.u2_profile.as_mut() { + profile.slot = U2SlotId::Slot3; + } + None + } + KeyCode::Char('m') => { + if let Some(profile) = app.u2_profile.as_mut() { + profile.mode = (profile.mode + 1) % 4; + } + None + } + KeyCode::Char('[') => { + if let Some(profile) = app.u2_profile.as_mut() { + if profile.supports_trigger_write { + profile.l2_analog = (profile.l2_analog - 0.05).clamp(0.0, 1.0); + } + } + None + } + KeyCode::Char(']') => { + if let Some(profile) = app.u2_profile.as_mut() { + if profile.supports_trigger_write { + profile.l2_analog = (profile.l2_analog + 0.05).clamp(0.0, 1.0); + } + } + None + } + KeyCode::Char(';') => { + if let Some(profile) = app.u2_profile.as_mut() { + if profile.supports_trigger_write { + profile.r2_analog = (profile.r2_analog - 0.05).clamp(0.0, 1.0); + } + } + None + } + KeyCode::Char('\'') => { + if let Some(profile) = app.u2_profile.as_mut() { + if profile.supports_trigger_write { + profile.r2_analog = (profile.r2_analog + 0.05).clamp(0.0, 1.0); + } + } + None + } + KeyCode::Enter => Some(DeviceFlowAction::BackupApply), + KeyCode::Char('b') => Some(DeviceFlowAction::BackupApply), + KeyCode::Char('r') => Some(DeviceFlowAction::RetryRead), + KeyCode::Char('s') => Some(DeviceFlowAction::RestoreBackup), + KeyCode::Char('t') => Some(DeviceFlowAction::GuidedTest), + KeyCode::Char('f') => Some(DeviceFlowAction::Firmware), + KeyCode::Esc | KeyCode::Char('q') => Some(DeviceFlowAction::Done), + _ => None, + }; + + if let Some(action) = action { + return execute_device_flow_action( + core, + terminal, + app, + opts, + firmware_events, + action, + ) + .await; + } + } + TuiWorkflowState::Recovery => { + match key { + KeyCode::Char('r') => { + if let Some(backup) = app.latest_backup.clone() { + match core.restore_backup(backup).await { + Ok(_) => { + app.last_message = "Recovery restore succeeded. Restart OpenBitdo before attempting writes again.".to_owned(); + } + Err(err) => { + app.last_message = format!("Recovery restore failed: {err}"); + } + } + } else { + app.last_message = "No backup available to restore. Use diagnostics and restart OpenBitdo.".to_owned(); + } + } + KeyCode::Enter | KeyCode::Esc => { + app.set_home_message( + "Recovery mode exited. Write actions remain locked until restart.", + ); + } + KeyCode::Char('q') => return Ok(true), + _ => {} + } + } + TuiWorkflowState::Preflight => match key { + KeyCode::Enter | KeyCode::Char('y') => { + start_pending_update(core, app, firmware_events).await?; + } + KeyCode::Esc | KeyCode::Char('c') => { + app.set_home_message("Update cancelled before transfer."); + } + KeyCode::Char('q') => return Ok(true), + _ => {} + }, + TuiWorkflowState::Updating => match key { + KeyCode::Esc | KeyCode::Char('c') => { + cancel_running_update(core, app, firmware_events).await?; + } + KeyCode::Char('q') => { + cancel_running_update(core, app, firmware_events).await?; + return Ok(true); + } + _ => {} + }, + TuiWorkflowState::FinalReport => match key { + KeyCode::Enter | KeyCode::Esc => app.set_home_message("Ready for next action."), + KeyCode::Char('q') => return Ok(true), + KeyCode::Char('a') => app.open_about(), + _ => {} + }, + } + + Ok(false) +} + +async fn handle_mouse_event( + core: &OpenBitdoCore, + terminal: &mut Option>>, + app: &mut TuiApp, + opts: &TuiLaunchOptions, + firmware_events: &mut Option>, + mouse: MouseEvent, +) -> Result { + let Some(size) = terminal.as_ref().map(|t| t.size()).transpose()? else { + return Ok(false); + }; + + let area = Rect::new(0, 0, size.width, size.height); + + match app.state { + TuiWorkflowState::About | TuiWorkflowState::HelpOverlay => { + if app.state == TuiWorkflowState::About { + let (toggle_rect, report_mode_rect, fingerprint_rect) = about_buttons_rects(area); + if matches!(mouse.kind, MouseEventKind::Moved) { + app.about_toggle_hovered = point_in_rect(mouse.column, mouse.row, toggle_rect); + app.about_report_mode_hovered = + point_in_rect(mouse.column, mouse.row, report_mode_rect); + app.about_fingerprint_hovered = + point_in_rect(mouse.column, mouse.row, fingerprint_rect); + } + if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) { + if point_in_rect(mouse.column, mouse.row, toggle_rect) { + app.toggle_advanced_mode(core)?; + } else if point_in_rect(mouse.column, mouse.row, report_mode_rect) { + app.cycle_report_save_mode()?; + } else if point_in_rect(mouse.column, mouse.row, fingerprint_rect) { + app.toggle_fingerprint_view(); + } else { + app.close_overlay(); + } + } + } else if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) { + app.close_overlay(); + } + } + TuiWorkflowState::WaitForDevice => { + let layout = waiting_layout(area); + let buttons = action_buttons(layout.actions, &WAIT_ACTIONS); + if matches!(mouse.kind, MouseEventKind::Moved) { + app.hovered_action = button_hit(mouse.column, mouse.row, &buttons); + } + if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) { + if let Some(action) = button_hit(mouse.column, mouse.row, &buttons) { + app.hovered_action = Some(action); + return execute_home_action(core, terminal, app, opts, firmware_events, action) + .await; + } + } + } + TuiWorkflowState::Home => { + let layout = home_layout(area); + let buttons = action_buttons(layout.actions, &HOME_ACTIONS); + + if let Some(menu) = app.context_menu.as_mut() { + if matches!(mouse.kind, MouseEventKind::Moved) { + menu.hovered_index = context_menu_item_at(area, *menu, mouse.column, mouse.row) + .map(action_index); + } + if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) { + if let Some(action) = context_menu_item_at(area, *menu, mouse.column, mouse.row) + { + app.close_context_menu(); + return execute_home_action( + core, + terminal, + app, + opts, + firmware_events, + action, + ) + .await; + } + app.close_context_menu(); + } + } + + match mouse.kind { + MouseEventKind::Moved => { + app.hovered_action = button_hit(mouse.column, mouse.row, &buttons); + } + MouseEventKind::Down(MouseButton::Left) => { + if let Some(action) = button_hit(mouse.column, mouse.row, &buttons) { + app.hovered_action = Some(action); + return execute_home_action( + core, + terminal, + app, + opts, + firmware_events, + action, + ) + .await; + } + + if let Some(idx) = device_row_at(app, layout.devices, mouse.row) { + app.select_index(idx); + } + } + MouseEventKind::Down(MouseButton::Right) => { + if let Some(idx) = device_row_at(app, layout.devices, mouse.row) { + app.select_index(idx); + app.open_context_menu(mouse.column, mouse.row); + } + } + MouseEventKind::ScrollDown => { + if point_in_rect(mouse.column, mouse.row, layout.devices) { + app.select_next(); + } else if point_in_rect(mouse.column, mouse.row, layout.detail) { + app.detail_scroll = app.detail_scroll.saturating_add(1); + } + } + MouseEventKind::ScrollUp => { + if point_in_rect(mouse.column, mouse.row, layout.devices) { + app.select_prev(); + } else if point_in_rect(mouse.column, mouse.row, layout.detail) { + app.detail_scroll = app.detail_scroll.saturating_sub(1); + } + } + _ => {} + } + } + TuiWorkflowState::Jp108Mapping => { + let layout = simple_action_layout(area); + let buttons = flow_buttons(layout.actions, &DEVICE_FLOW_ACTIONS); + if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) { + if let Some(action) = flow_button_hit(mouse.column, mouse.row, &buttons) { + return execute_device_flow_action( + core, + terminal, + app, + opts, + firmware_events, + action, + ) + .await; + } + if let Some(row_idx) = + mapping_row_hit(layout.body, mouse.row, app.jp108_mappings.len()) + { + app.jp108_selected = row_idx; + } + } + match mouse.kind { + MouseEventKind::ScrollDown => jp108_adjust_selected_usage(app, 1), + MouseEventKind::ScrollUp => jp108_adjust_selected_usage(app, -1), + _ => {} + } + } + TuiWorkflowState::U2CoreProfile => { + let layout = simple_action_layout(area); + let buttons = flow_buttons(layout.actions, &DEVICE_FLOW_ACTIONS); + if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) { + if let Some(action) = flow_button_hit(mouse.column, mouse.row, &buttons) { + return execute_device_flow_action( + core, + terminal, + app, + opts, + firmware_events, + action, + ) + .await; + } + if let Some(profile) = app.u2_profile.as_ref() { + if let Some(row_idx) = + mapping_row_hit(layout.body, mouse.row, profile.mappings.len()) + { + app.u2_selected = row_idx; + } + } + } + match mouse.kind { + MouseEventKind::ScrollDown => u2_adjust_selected_usage(app, 1), + MouseEventKind::ScrollUp => u2_adjust_selected_usage(app, -1), + _ => {} + } + } + TuiWorkflowState::Recovery => { + let layout = simple_action_layout(area); + let buttons = action_buttons( + layout.actions, + &[HomeAction::Refresh, HomeAction::About, HomeAction::Quit], + ); + if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) { + match button_hit(mouse.column, mouse.row, &buttons) { + Some(HomeAction::Refresh) => { + if let Some(backup) = app.latest_backup.clone() { + match core.restore_backup(backup).await { + Ok(_) => { + app.last_message = "Recovery restore succeeded. Restart OpenBitdo before attempting writes again.".to_owned(); + } + Err(err) => { + app.last_message = format!("Recovery restore failed: {err}"); + } + } + } else { + app.last_message = "No backup available to restore.".to_owned(); + } + } + Some(HomeAction::About) => { + app.set_home_message( + "Recovery mode exited. Write actions remain locked until restart.", + ); + } + Some(HomeAction::Quit) => return Ok(true), + _ => {} + } + } + } + TuiWorkflowState::Preflight => { + let layout = simple_action_layout(area); + let buttons = action_buttons(layout.actions, &[HomeAction::Update, HomeAction::Quit]); + if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) { + match button_hit(mouse.column, mouse.row, &buttons) { + Some(HomeAction::Update) => { + start_pending_update(core, app, firmware_events).await? + } + Some(HomeAction::Quit) => { + app.set_home_message("Update cancelled before transfer.") + } + _ => {} + } + } + } + TuiWorkflowState::Updating => { + let layout = simple_action_layout(area); + let buttons = action_buttons(layout.actions, &[HomeAction::Quit]); + if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) + && button_hit(mouse.column, mouse.row, &buttons) == Some(HomeAction::Quit) + { + cancel_running_update(core, app, firmware_events).await?; + } + } + TuiWorkflowState::FinalReport => { + let layout = simple_action_layout(area); + let buttons = action_buttons(layout.actions, &[HomeAction::Refresh, HomeAction::Quit]); + if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) { + match button_hit(mouse.column, mouse.row, &buttons) { + Some(HomeAction::Refresh) => app.set_home_message("Ready for next action."), + Some(HomeAction::Quit) => return Ok(true), + _ => {} + } + } + } + } + + Ok(false) +} + +async fn execute_home_action( + core: &OpenBitdoCore, + terminal: &mut Option>>, + app: &mut TuiApp, + opts: &TuiLaunchOptions, + firmware_events: &mut Option>, + action: HomeAction, +) -> Result { + match action { + HomeAction::About => app.open_about(), + HomeAction::Help => app.open_help(), + HomeAction::Quit => return Ok(true), + HomeAction::Refresh => match core.list_devices().await { + Ok(devices) => { + app.refresh_devices(devices); + app.last_message = match app.devices.len() { + 0 => { + "Still waiting for a controller. Plug one in and choose Refresh.".to_owned() + } + 1 => "Controller detected and auto-selected.".to_owned(), + count => format!("Detected {count} controllers. Select one to continue."), + }; + } + Err(err) => app.last_message = format!("Refresh failed: {err}"), + }, + HomeAction::Diagnose => { + if let Some(selected) = app.selected_device().cloned() { + match core.diag_probe(selected.vid_pid).await { + Ok(diag) => { + let confirmed_total = diag + .command_checks + .iter() + .filter(|c| !c.is_experimental) + .count(); + let confirmed_ok = diag + .command_checks + .iter() + .filter(|c| !c.is_experimental && c.ok) + .count(); + let experimental_total = diag + .command_checks + .iter() + .filter(|c| c.is_experimental) + .count(); + let experimental_ok = diag + .command_checks + .iter() + .filter(|c| c.is_experimental && c.ok) + .count(); + let needs_attention = diag + .command_checks + .iter() + .filter(|c| c.severity == bitdo_proto::DiagSeverity::NeedsAttention) + .count(); + app.last_message = format!( + "Diagnostics for {}\nConfirmed checks: {}/{}\nExperimental checks: {}/{}\nNeeds attention: {}\n{}", + selected.vid_pid, + confirmed_ok, + confirmed_total, + experimental_ok, + experimental_total, + needs_attention, + core.beginner_diag_summary(&selected, &diag) + ); + if should_save_support_report(app.report_save_mode, false) { + if let Ok(path) = persist_support_report( + "diag-probe", + Some(&selected), + "ok", + app.last_message.clone(), + Some(&diag), + None, + ) + .await + { + app.remember_report_path(path.clone()); + app.last_message = format!( + "Diagnostics complete. Support file saved: {}", + path.to_string_lossy() + ); + } + } + } + Err(err) => { + app.last_message = format!("Diagnostics failed: {err}"); + if should_save_support_report(app.report_save_mode, true) { + if let Ok(path) = persist_support_report( + "diag-probe", + Some(&selected), + "failed", + app.last_message.clone(), + None, + None, + ) + .await + { + app.remember_report_path(path.clone()); + if app.advanced_mode { + app.last_message = format!( + "Diagnostics failed. Report saved: {} (c=copy o=open f=folder)", + path.to_string_lossy() + ); + } else { + app.last_message = format!( + "Diagnostics failed. A support file was saved: {}", + path.to_string_lossy() + ); + } + } + } + } + } + } else { + app.last_message = "No device selected.".to_owned(); + } + } + HomeAction::Update => { + let Some(selected) = app.selected_device().cloned() else { + app.last_message = "No device selected.".to_owned(); + return Ok(false); + }; + + if app.write_lock_until_restart { + app.state = TuiWorkflowState::Recovery; + app.last_message = + "Write actions are locked until restart due to a failed rollback.".to_owned(); + return Ok(false); + } + + if selected.support_tier != SupportTier::Full { + app.last_message = format!( + "Recommended Update is coming soon for {} ({}). This device is currently read-only in OpenBitdo. Use Diagnose for now.", + selected.name, selected.vid_pid + ); + return Ok(false); + } + + if selected.capability.supports_jp108_dedicated_map { + match core.jp108_read_dedicated_mapping(selected.vid_pid).await { + Ok(mappings) => app.begin_jp108_mapping(mappings), + Err(err) => app.last_message = format!("JP108 mapping read failed: {err}"), + } + } else if selected.capability.supports_u2_button_map + && selected.capability.supports_u2_slot_config + { + match core + .u2_read_core_profile(selected.vid_pid, U2SlotId::Slot1) + .await + { + Ok(profile) => app.begin_u2_profile(profile), + Err(err) => app.last_message = format!("Ultimate2 profile read failed: {err}"), + } + } else { + prepare_update_preflight(core, terminal, app, opts, firmware_events).await?; + } + } + } + + Ok(false) +} + +async fn execute_device_flow_action( + core: &OpenBitdoCore, + terminal: &mut Option>>, + app: &mut TuiApp, + opts: &TuiLaunchOptions, + firmware_events: &mut Option>, + action: DeviceFlowAction, +) -> Result { + let Some(selected) = app.selected_device().cloned() else { + app.last_message = "No device selected.".to_owned(); + return Ok(false); + }; + + if app.write_lock_until_restart + && matches!( + action, + DeviceFlowAction::BackupApply | DeviceFlowAction::Firmware + ) + { + app.state = TuiWorkflowState::Recovery; + app.last_message = + "Write actions are locked until restart because recovery has not completed.".to_owned(); + return Ok(false); + } + + match action { + DeviceFlowAction::Done => app.set_home_message("Ready for next action."), + DeviceFlowAction::Firmware => { + prepare_update_preflight(core, terminal, app, opts, firmware_events).await?; + } + DeviceFlowAction::RetryRead => { + if app.state == TuiWorkflowState::Jp108Mapping { + match core.jp108_read_dedicated_mapping(selected.vid_pid).await { + Ok(mappings) => app.begin_jp108_mapping(mappings), + Err(err) => app.last_message = format!("Reload failed: {err}"), + } + } else if app.state == TuiWorkflowState::U2CoreProfile { + let slot = app + .u2_profile + .as_ref() + .map(|p| p.slot) + .unwrap_or(U2SlotId::Slot1); + match core.u2_read_core_profile(selected.vid_pid, slot).await { + Ok(profile) => app.begin_u2_profile(profile), + Err(err) => app.last_message = format!("Reload failed: {err}"), + } + } + } + DeviceFlowAction::BackupApply => { + if app.state == TuiWorkflowState::Jp108Mapping { + let warnings = jp108_mapping_warnings(&app.jp108_mappings); + match core + .jp108_apply_dedicated_mapping_with_recovery( + selected.vid_pid, + app.jp108_mappings.clone(), + true, + ) + .await + { + Ok(result) => { + if result.write_applied { + app.latest_backup = result.backup_id; + if warnings.is_empty() { + app.last_message = + "JP108 mapping applied. Run Button Test or continue to Firmware." + .to_owned(); + } else { + app.last_message = format!( + "JP108 mapping applied with warnings (allowed): {}", + warnings.join(" ") + ); + } + } else if result.rollback_failed() { + app.latest_backup = result.backup_id.clone(); + app.enter_recovery(result); + } else { + app.latest_backup = result.backup_id; + app.last_message = + "Apply failed, but rollback restored your previous mapping safely." + .to_owned(); + } + } + Err(err) => { + app.last_message = format!("Apply failed: {err}"); + } + } + } else if app.state == TuiWorkflowState::U2CoreProfile { + if let Some(profile) = app.u2_profile.clone() { + let warnings = u2_mapping_warnings(&profile.mappings); + match core + .u2_apply_core_profile_with_recovery( + selected.vid_pid, + profile.slot, + profile.mode, + profile.mappings, + profile.l2_analog, + profile.r2_analog, + true, + ) + .await + { + Ok(result) => { + if result.write_applied { + app.latest_backup = result.backup_id; + if warnings.is_empty() { + app.last_message = "Ultimate2 profile applied. Run Button Test or continue to Firmware." + .to_owned(); + } else { + app.last_message = format!( + "Ultimate2 profile applied with warnings (allowed): {}", + warnings.join(" ") + ); + } + } else if result.rollback_failed() { + app.latest_backup = result.backup_id.clone(); + app.enter_recovery(result); + } else { + app.latest_backup = result.backup_id; + app.last_message = "Apply failed, but rollback restored your previous Ultimate2 profile safely." + .to_owned(); + } + } + Err(err) => { + app.last_message = format!("Apply failed: {err}"); + } + } + } + } + } + DeviceFlowAction::RestoreBackup => { + if let Some(backup) = app.latest_backup.clone() { + match core.restore_backup(backup).await { + Ok(_) => app.last_message = "Backup restored successfully.".to_owned(), + Err(err) => app.last_message = format!("Restore failed: {err}"), + } + } else { + app.last_message = "No backup available yet. Use Backup + Apply first.".to_owned(); + } + } + DeviceFlowAction::GuidedTest => { + let result = if app.state == TuiWorkflowState::Jp108Mapping { + core.guided_button_test( + DeviceKind::Jp108, + app.jp108_mappings + .iter() + .map(|entry| { + format!("{:?} -> 0x{:04x}", entry.button, entry.target_hid_usage) + }) + .collect(), + ) + .await + } else { + let expected = app + .u2_profile + .as_ref() + .map(|profile| { + profile + .mappings + .iter() + .map(|entry| { + format!( + "{:?} -> {} (0x{:04x})", + entry.button, + u2_target_label(entry.target_hid_usage), + entry.target_hid_usage + ) + }) + .collect::>() + }) + .unwrap_or_default(); + core.guided_button_test(DeviceKind::Ultimate2, expected) + .await + }; + + match result { + Ok(report) => app.last_message = report.guidance, + Err(err) => app.last_message = format!("Guided test failed: {err}"), + } + } + } + + Ok(false) +} + +async fn prepare_update_preflight( + core: &OpenBitdoCore, + terminal: &mut Option>>, + app: &mut TuiApp, + opts: &TuiLaunchOptions, + firmware_events: &mut Option>, +) -> Result<()> { + *firmware_events = None; + + let Some(selected) = app.selected_device().cloned() else { + app.last_message = "No device selected.".to_owned(); + return Ok(()); + }; + + if selected.support_tier != SupportTier::Full { + app.last_message = format!( + "Firmware update is blocked for {} until hardware confirmation is complete. You can still run diagnostics.", + selected.vid_pid + ); + return Ok(()); + } + + let (firmware_path, source_label, version_label) = match opts.firmware_path.clone() { + Some(path) => (path, "local file".to_owned(), "manual".to_owned()), + None => match core.download_recommended_firmware(selected.vid_pid).await { + Ok(download) => ( + download.firmware_path, + "recommended verified download".to_owned(), + download.version, + ), + Err(err) => { + let prompt = format!( + "Recommended firmware unavailable ({err}). Enter local firmware path: " + ); + let input = prompt_line(terminal, &prompt)?; + if input.trim().is_empty() { + app.last_message = "Update cancelled: no firmware file selected.".to_owned(); + return Ok(()); + } + ( + PathBuf::from(input), + "local file fallback".to_owned(), + "manual".to_owned(), + ) + } + }, + }; + + let preflight = core + .preflight_firmware(FirmwarePreflightRequest { + vid_pid: selected.vid_pid, + firmware_path: firmware_path.clone(), + allow_unsafe: opts.allow_unsafe, + brick_risk_ack: opts.brick_risk_ack, + experimental: opts.experimental, + chunk_size: opts.chunk_size, + }) + .await?; + + if !preflight.gate.allowed { + let reason = preflight + .gate + .message + .unwrap_or_else(|| "Update is not allowed for this device.".to_owned()); + app.last_message = format!("Preflight blocked: {reason}"); + return Ok(()); + } + + let plan = preflight + .plan + .ok_or_else(|| anyhow!("missing preflight plan for allowed request"))?; + + app.begin_preflight(PendingUpdate { + target: selected, + firmware_path, + firmware_source: source_label, + firmware_version: version_label, + plan, + }); + + Ok(()) +} + +async fn start_pending_update( + core: &OpenBitdoCore, + app: &mut TuiApp, + firmware_events: &mut Option>, +) -> Result<()> { + let Some(pending) = app.pending_update.clone() else { + app.set_home_message("No preflight plan found. Start from Home."); + return Ok(()); + }; + + core.start_firmware(FirmwareStartRequest { + session_id: pending.plan.session_id.clone(), + }) + .await?; + + core.confirm_firmware(FirmwareConfirmRequest { + session_id: pending.plan.session_id.clone(), + acknowledged_risk: true, + }) + .await?; + + *firmware_events = Some(core.subscribe_events(&pending.plan.session_id.0).await?); + app.set_session(pending.plan.session_id.clone()); + app.last_message = format!( + "Transferring firmware {} from {}. Press Esc or click Cancel to stop.", + pending.firmware_version, pending.firmware_source + ); + + Ok(()) +} + +async fn cancel_running_update( + core: &OpenBitdoCore, + app: &mut TuiApp, + firmware_events: &mut Option>, +) -> Result<()> { + let Some(session_id) = app.session_id.clone() else { + return Ok(()); + }; + + let report = core + .cancel_firmware(FirmwareCancelRequest { session_id }) + .await?; + app.complete(report.clone()); + *firmware_events = None; + + if report.status != FirmwareOutcome::Completed + && should_save_support_report(app.report_save_mode, true) + { + let selected = app.selected_device().cloned(); + if let Ok(path) = persist_support_report( + "fw-write", + selected.as_ref(), + "cancelled", + report.message.clone(), + None, + Some(&report), + ) + .await + { + app.remember_report_path(path.clone()); + if app.advanced_mode { + app.last_message = format!( + "Update cancelled. Report saved: {} (c=copy o=open f=folder)", + path.to_string_lossy() + ); + } else { + app.last_message = format!( + "Update cancelled. A support file was saved: {}", + path.to_string_lossy() + ); + } + } + } + + Ok(()) +} + +async fn poll_firmware_progress( + core: &OpenBitdoCore, + app: &mut TuiApp, + firmware_events: &mut Option>, +) -> Result<()> { + if app.state != TuiWorkflowState::Updating { + return Ok(()); + } + + if let Some(receiver) = firmware_events.as_mut() { + loop { + match receiver.try_recv() { + Ok(evt) => { + app.apply_progress(evt.progress, format!("{}: {}", evt.stage, evt.message)); + } + Err(broadcast::error::TryRecvError::Empty) => break, + Err(broadcast::error::TryRecvError::Lagged(_)) => continue, + Err(broadcast::error::TryRecvError::Closed) => { + *firmware_events = None; + break; + } + } + } + } + + if let Some(session_id) = app.session_id.as_ref() { + if let Some(report) = core.firmware_report(&session_id.0).await? { + app.complete(report.clone()); + *firmware_events = None; + match report.status { + FirmwareOutcome::Completed => { + app.last_message = + "Firmware update completed. Press Enter to continue.".to_owned(); + if should_save_support_report(app.report_save_mode, false) { + let selected = app.selected_device().cloned(); + if let Ok(path) = persist_support_report( + "fw-write", + selected.as_ref(), + "completed", + report.message.clone(), + None, + Some(&report), + ) + .await + { + app.remember_report_path(path.clone()); + app.last_message = format!( + "Firmware update completed. Support file saved: {}", + path.to_string_lossy() + ); + } + } + } + FirmwareOutcome::Cancelled | FirmwareOutcome::Failed => { + app.last_message = + "Firmware update did not complete. Press Enter to return Home.".to_owned(); + if should_save_support_report(app.report_save_mode, true) { + let selected = app.selected_device().cloned(); + if let Ok(path) = persist_support_report( + "fw-write", + selected.as_ref(), + "failed", + report.message.clone(), + None, + Some(&report), + ) + .await + { + app.remember_report_path(path.clone()); + if app.advanced_mode { + app.last_message = format!( + "Firmware update failed. Report saved: {} (c=copy o=open f=folder)", + path.to_string_lossy() + ); + } else { + app.last_message = format!( + "Firmware update failed. A support file was saved: {}", + path.to_string_lossy() + ); + } + } + } + } + } + } + } + + Ok(()) +} + +fn home_layout(area: Rect) -> HomeLayout { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(10), + Constraint::Length(5), + Constraint::Length(3), + Constraint::Min(6), + ]) + .split(area); + + let detail_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(4), Constraint::Length(5)]) + .split(chunks[4]); + + HomeLayout { + title: chunks[0], + devices: chunks[1], + actions: chunks[2], + progress: chunks[3], + detail: detail_chunks[0], + blocked: detail_chunks[1], + } +} + +fn waiting_layout(area: Rect) -> WaitingLayout { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(8), + Constraint::Length(5), + Constraint::Length(3), + ]) + .split(area); + + WaitingLayout { + header: chunks[0], + body: chunks[1], + actions: chunks[2], + footer: chunks[3], + } +} + +fn simple_action_layout(area: Rect) -> SimpleActionLayout { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Min(10), + Constraint::Length(5), + Constraint::Length(3), + ]) + .split(area); + + SimpleActionLayout { + body: chunks[0], + actions: chunks[1], + footer: chunks[2], + } +} + +#[derive(Clone, Copy)] +struct HomeLayout { + title: Rect, + devices: Rect, + actions: Rect, + progress: Rect, + detail: Rect, + blocked: Rect, +} + +#[derive(Clone, Copy)] +struct WaitingLayout { + header: Rect, + body: Rect, + actions: Rect, + footer: Rect, +} + +#[derive(Clone, Copy)] +struct SimpleActionLayout { + body: Rect, + actions: Rect, + footer: Rect, +} + +fn action_buttons(area: Rect, actions: &[HomeAction]) -> Vec<(Rect, HomeAction)> { + if actions.is_empty() { + return Vec::new(); + } + + let constraints = + vec![Constraint::Percentage((100 / actions.len()).max(1) as u16); actions.len()]; + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints(constraints) + .split(area); + + chunks + .iter() + .copied() + .zip(actions.iter().copied()) + .collect::>() +} + +fn flow_buttons(area: Rect, actions: &[DeviceFlowAction]) -> Vec<(Rect, DeviceFlowAction)> { + if actions.is_empty() { + return Vec::new(); + } + + let constraints = + vec![Constraint::Percentage((100 / actions.len()).max(1) as u16); actions.len()]; + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints(constraints) + .split(area); + + chunks + .iter() + .copied() + .zip(actions.iter().copied()) + .collect::>() +} + +fn button_hit(column: u16, row: u16, buttons: &[(Rect, HomeAction)]) -> Option { + buttons + .iter() + .find(|(rect, _)| point_in_rect(column, row, *rect)) + .map(|(_, action)| *action) +} + +fn flow_button_hit( + column: u16, + row: u16, + buttons: &[(Rect, DeviceFlowAction)], +) -> Option { + buttons + .iter() + .find(|(rect, _)| point_in_rect(column, row, *rect)) + .map(|(_, action)| *action) +} + +fn mapping_row_hit(body_rect: Rect, row: u16, total_rows: usize) -> Option { + let start = body_rect.y.saturating_add(2); + if row < start { + return None; + } + let idx = row.saturating_sub(start) as usize; + if idx < total_rows { + Some(idx) + } else { + None + } +} + +const HID_USAGE_PRESETS: [u16; 16] = [ + 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000a, 0x000b, 0x0028, 0x0029, 0x002c, 0x003a, + 0x003b, 0x003c, 0x00e0, 0x00e1, +]; + +// Ultimate2 target set is intentionally restricted to known controller-button +// codes for RC safety/readability. +const U2_TARGET_PRESETS: [u16; 17] = [ + 0x0100, // A + 0x0101, // B + 0x0102, // X + 0x0103, // Y + 0x0104, // L1 + 0x0105, // R1 + 0x0106, // L2 + 0x0107, // R2 + 0x0108, // L3 + 0x0109, // R3 + 0x010a, // Select + 0x010b, // Start + 0x010c, // Home + 0x010d, // DPadUp + 0x010e, // DPadDown + 0x010f, // DPadLeft + 0x0110, // DPadRight +]; + +fn cycle_usage(current: u16, delta: i32) -> u16 { + let pos = HID_USAGE_PRESETS + .iter() + .position(|value| *value == current) + .unwrap_or(0) as i32; + let len = HID_USAGE_PRESETS.len() as i32; + let next = (pos + delta).rem_euclid(len) as usize; + HID_USAGE_PRESETS[next] +} + +fn cycle_u2_target(current: u16, delta: i32) -> u16 { + let pos = U2_TARGET_PRESETS + .iter() + .position(|value| *value == current) + .unwrap_or(0) as i32; + let len = U2_TARGET_PRESETS.len() as i32; + let next = (pos + delta).rem_euclid(len) as usize; + U2_TARGET_PRESETS[next] +} + +fn u2_target_label(target: u16) -> &'static str { + match target { + 0x0100 => "A", + 0x0101 => "B", + 0x0102 => "X", + 0x0103 => "Y", + 0x0104 => "L1", + 0x0105 => "R1", + 0x0106 => "L2", + 0x0107 => "R2", + 0x0108 => "L3", + 0x0109 => "R3", + 0x010a => "Select", + 0x010b => "Start", + 0x010c => "Home", + 0x010d => "DPadUp", + 0x010e => "DPadDown", + 0x010f => "DPadLeft", + 0x0110 => "DPadRight", + _ => "Unknown", + } +} + +fn u2_default_target_for_slot(slot: &bitdo_app_core::U2ButtonId) -> u16 { + match slot { + bitdo_app_core::U2ButtonId::A => 0x0100, + bitdo_app_core::U2ButtonId::B => 0x0101, + bitdo_app_core::U2ButtonId::K1 => 0x0102, + bitdo_app_core::U2ButtonId::K2 => 0x0103, + bitdo_app_core::U2ButtonId::K3 => 0x0104, + bitdo_app_core::U2ButtonId::K4 => 0x0105, + bitdo_app_core::U2ButtonId::K5 => 0x0106, + bitdo_app_core::U2ButtonId::K6 => 0x0107, + bitdo_app_core::U2ButtonId::K7 => 0x0108, + bitdo_app_core::U2ButtonId::K8 => 0x0109, + } +} + +fn u2_mapping_warnings(mappings: &[bitdo_app_core::U2ButtonMapping]) -> Vec { + let mut warnings = Vec::new(); + let mut target_counts: HashMap = HashMap::new(); + for mapping in mappings { + *target_counts.entry(mapping.target_hid_usage).or_insert(0) += 1; + } + + for (target, count) in target_counts { + if count > 1 { + warnings.push(format!( + "Duplicate target {} (0x{:04x}) appears {} times.", + u2_target_label(target), + target, + count + )); + } + } + + for mapping in mappings { + if mapping.target_hid_usage == u2_default_target_for_slot(&mapping.button) { + warnings.push(format!( + "Identity mapping kept for {:?} -> {} (0x{:04x}).", + mapping.button, + u2_target_label(mapping.target_hid_usage), + mapping.target_hid_usage + )); + } + } + + warnings +} + +fn jp108_default_target_for_button(button: &bitdo_app_core::DedicatedButtonId) -> u16 { + match button { + bitdo_app_core::DedicatedButtonId::A => 0x0004, + bitdo_app_core::DedicatedButtonId::B => 0x0005, + bitdo_app_core::DedicatedButtonId::K1 => 0x0006, + bitdo_app_core::DedicatedButtonId::K2 => 0x0007, + bitdo_app_core::DedicatedButtonId::K3 => 0x0008, + bitdo_app_core::DedicatedButtonId::K4 => 0x0009, + bitdo_app_core::DedicatedButtonId::K5 => 0x000a, + bitdo_app_core::DedicatedButtonId::K6 => 0x000b, + bitdo_app_core::DedicatedButtonId::K7 => 0x0028, + bitdo_app_core::DedicatedButtonId::K8 => 0x0029, + } +} + +fn jp108_mapping_warnings(mappings: &[bitdo_app_core::DedicatedButtonMapping]) -> Vec { + let mut warnings = Vec::new(); + let mut target_counts: HashMap = HashMap::new(); + for mapping in mappings { + *target_counts.entry(mapping.target_hid_usage).or_insert(0) += 1; + } + + for (target, count) in target_counts { + if count > 1 { + warnings.push(format!( + "Duplicate target 0x{:04x} appears {} times.", + target, count + )); + } + } + + for mapping in mappings { + if mapping.target_hid_usage == jp108_default_target_for_button(&mapping.button) { + warnings.push(format!( + "Identity mapping kept for {:?} -> 0x{:04x}.", + mapping.button, mapping.target_hid_usage + )); + } + } + + warnings +} + +fn jp108_adjust_selected_usage(app: &mut TuiApp, delta: i32) { + if let Some(current) = app.jp108_mappings.get_mut(app.jp108_selected) { + current.target_hid_usage = cycle_usage(current.target_hid_usage, delta); + } +} + +fn u2_adjust_selected_usage(app: &mut TuiApp, delta: i32) { + if let Some(profile) = app.u2_profile.as_mut() { + if let Some(current) = profile.mappings.get_mut(app.u2_selected) { + current.target_hid_usage = cycle_u2_target(current.target_hid_usage, delta); + } + } +} + +fn slot_label(slot: U2SlotId) -> &'static str { + match slot { + U2SlotId::Slot1 => "Slot1", + U2SlotId::Slot2 => "Slot2", + U2SlotId::Slot3 => "Slot3", + } +} + +fn point_in_rect(x: u16, y: u16, rect: Rect) -> bool { + x >= rect.x + && y >= rect.y + && x < rect.x.saturating_add(rect.width) + && y < rect.y.saturating_add(rect.height) +} + +fn device_row_at(app: &TuiApp, devices_rect: Rect, row: u16) -> Option { + let start = devices_rect.y.saturating_add(1); + let end = devices_rect + .y + .saturating_add(devices_rect.height.saturating_sub(1)); + + if row < start || row >= end { + return None; + } + + let visible_rows = devices_rect.height.saturating_sub(2) as usize; + if visible_rows == 0 { + return None; + } + + let window_start = if app.selected_index >= visible_rows { + app.selected_index + 1 - visible_rows + } else { + 0 + }; + + let offset = row.saturating_sub(start) as usize; + let idx = window_start + offset; + if idx < app.devices.len() { + Some(idx) + } else { + None + } +} + +fn context_menu_rect(area: Rect, menu: MouseContextMenu) -> Rect { + let width: u16 = 30; + let height: u16 = CONTEXT_ACTIONS.len() as u16 + 2; + + let max_x = area.x.saturating_add(area.width.saturating_sub(width)); + let max_y = area.y.saturating_add(area.height.saturating_sub(height)); + + let x = menu.anchor_col.min(max_x); + let y = menu.anchor_row.min(max_y); + + Rect::new(x, y, width, height) +} + +fn context_menu_item_at( + area: Rect, + menu: MouseContextMenu, + column: u16, + row: u16, +) -> Option { + let rect = context_menu_rect(area, menu); + if !point_in_rect(column, row, rect) { + return None; + } + + let inner_y = row.saturating_sub(rect.y.saturating_add(1)); + CONTEXT_ACTIONS.get(inner_y as usize).copied() +} + +fn action_index(action: HomeAction) -> usize { + match action { + HomeAction::Update => 0, + HomeAction::Diagnose => 1, + HomeAction::Refresh => 2, + HomeAction::About => 3, + HomeAction::Help => 4, + HomeAction::Quit => 5, + } +} + +fn action_tooltip(action: HomeAction, advanced_mode: bool) -> &'static str { + match (action, advanced_mode) { + (HomeAction::Update, false) => "Recommended update starts the safest guided flow.", + (HomeAction::Update, true) => { + "Recommended update runs preflight, signature checks, and explicit confirmation." + } + (HomeAction::Diagnose, false) => "Diagnose checks device readiness without risky writes.", + (HomeAction::Diagnose, true) => { + "Diagnose includes inferred read-only checks while advanced mode is enabled." + } + (HomeAction::Refresh, _) => "Refresh rescans connected 8BitDo devices.", + (HomeAction::About, false) => { + "About shows version/build details and advanced mode setting." + } + (HomeAction::About, true) => "About also controls advanced mode and report hotkey details.", + (HomeAction::Help, _) => "Help shows the beginner flow and optional shortcuts.", + (HomeAction::Quit, _) => "Quit closes OpenBitdo safely.", + } +} + +fn available_actions_summary(device: &AppDevice) -> &'static str { + match device.support_status() { + UserSupportStatus::Supported => { + if device.capability.supports_jp108_dedicated_map { + "JP108 dedicated mapping (A/B/K1-K8), diagnostics, and firmware update" + } else if device.capability.supports_u2_button_map + && device.capability.supports_u2_slot_config + { + "Ultimate2 mode/slot/core mapping, diagnostics, and firmware update" + } else { + "Diagnostics and firmware update" + } + } + UserSupportStatus::InProgress => { + "Diagnostics only (mapping/update blocked in beginner mode)" + } + UserSupportStatus::Planned => "Detection and diagnostics only", + UserSupportStatus::Blocked => "No actions available", + } +} + +fn display_device_name(device: &AppDevice) -> String { + if device.name == "PID_UNKNOWN" + || device.protocol_family == bitdo_proto::ProtocolFamily::Unknown + { + format!( + "Unknown 8BitDo Device ({:04x}:{:04x})", + device.vid_pid.vid, device.vid_pid.pid + ) + } else { + device.name.clone() + } +} + +fn beginner_status_label(device: &AppDevice) -> &'static str { + match device.support_status() { + UserSupportStatus::InProgress => UserSupportStatus::Blocked.as_str(), + other => other.as_str(), + } +} + +fn blocked_action_panel_text(device: &AppDevice) -> String { + match device.support_status() { + UserSupportStatus::Supported => { + "Blocked Actions: none. This device is fully supported in the current build." + .to_owned() + } + UserSupportStatus::InProgress => format!( + "Status shown as Blocked for {} in beginner mode.\nRecommended Update is visible with a Coming soon badge.\nDiagnostics are available, but mapping and firmware writes are blocked until hardware confirmation is complete.", + device.name + ), + UserSupportStatus::Planned => format!( + "Recommended Update is blocked for {} and marked Coming soon because support is still Planned.\nYou can run diagnostics now, and full actions unlock after confirmation work.", + device.name + ), + UserSupportStatus::Blocked => format!( + "This action is currently blocked for {} by policy. Use Diagnose for a safe check.", + device.name + ), + } +} + +fn should_save_support_report(mode: ReportSaveMode, is_failure: bool) -> bool { + match mode { + ReportSaveMode::Off => false, + ReportSaveMode::Always => true, + ReportSaveMode::FailureOnly => is_failure, + } +} + +fn about_buttons_rects(area: Rect) -> (Rect, Rect, Rect) { + let width: u16 = 30; + let height: u16 = 3; + let spacing: u16 = 1; + let total_height = height * 3 + spacing * 2; + let x = area.x.saturating_add(area.width.saturating_sub(width) / 2); + let y = area + .y + .saturating_add(area.height.saturating_sub(total_height + 2)); + let first = Rect::new(x, y, width.min(area.width), height); + let second = Rect::new( + x, + y.saturating_add(height + spacing), + width.min(area.width), + height, + ); + let third = Rect::new( + x, + y.saturating_add((height + spacing) * 2), + width.min(area.width), + height, + ); + (first, second, third) +} + +fn render_button(frame: &mut Frame<'_>, rect: Rect, label: &str, active: bool) { + let style = if active { + Style::default() + .fg(Color::Black) + .bg(Color::Cyan) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::White) + }; + + let button = Paragraph::new(Line::from(Span::styled(label, style))).block( + Block::default().borders(Borders::ALL).style(if active { + Style::default().fg(Color::Cyan) + } else { + Style::default().fg(Color::Gray) + }), + ); + frame.render_widget(button, rect); +} + +fn render_if_needed( + terminal: &mut Option>>, + app: &TuiApp, +) -> Result<()> { + let Some(terminal) = terminal else { + return Ok(()); + }; + + terminal.draw(|frame| match app.state { + TuiWorkflowState::About => render_about(frame, app), + TuiWorkflowState::HelpOverlay => render_help(frame, app.advanced_mode), + TuiWorkflowState::WaitForDevice => render_waiting(frame, app), + TuiWorkflowState::Home => render_home(frame, app), + TuiWorkflowState::Jp108Mapping => render_jp108_mapping(frame, app), + TuiWorkflowState::U2CoreProfile => render_u2_profile(frame, app), + TuiWorkflowState::Recovery => render_recovery(frame, app), + TuiWorkflowState::Preflight => render_preflight(frame, app), + TuiWorkflowState::Updating => render_updating(frame, app), + TuiWorkflowState::FinalReport => render_final_report(frame, app), + })?; + + Ok(()) +} + +fn render_about(frame: &mut Frame<'_>, app: &TuiApp) { + let block = Block::default() + .borders(Borders::ALL) + .title("About OpenBitdo"); + let toggle_label = if app.advanced_mode { + "Advanced Mode: ON" + } else { + "Advanced Mode: OFF" + }; + let report_label = format!("Report Saving: {}", app.report_save_mode.as_str()); + let fingerprint_toggle_label = if app.about_show_full_fingerprint { + "Fingerprint: full" + } else { + "Fingerprint: short" + }; + let key_line = if app.about_show_full_fingerprint { + app.build_info.signing_key_fingerprint_full.clone() + } else { + app.build_info.signing_key_fingerprint_short.clone() + }; + let lines = vec![ + Line::from(format!("App version: {}", app.build_info.app_version)), + Line::from(format!( + "Git commit (short): {}", + app.build_info.git_commit_short + )), + Line::from(format!( + "Git commit (full): {}", + app.build_info.git_commit_full + )), + Line::from(format!( + "Build date (UTC): {}", + app.build_info.build_date_utc + )), + Line::from(format!("Platform target: {}", app.build_info.target_triple)), + Line::from(format!( + "Runtime platform: {}", + app.build_info.runtime_platform + )), + Line::from(format!("Signing key (active): {key_line}")), + Line::from(format!( + "Signing key (next, short): {}", + app.build_info.signing_key_next_fingerprint_short + )), + Line::from(""), + Line::from(format!( + "{toggle_label} (press 't' or click button to toggle)" + )), + Line::from(format!( + "{} (press 'r' or click button to cycle)", + report_label + )), + Line::from("Press 'v' or click Fingerprint to toggle short/full view."), + Line::from("Esc/Enter/click outside to return"), + ]; + + let paragraph = Paragraph::new(lines).block(block); + frame.render_widget(paragraph, frame.area()); + + let (toggle_rect, report_rect, fingerprint_rect) = about_buttons_rects(frame.area()); + render_button(frame, toggle_rect, toggle_label, app.about_toggle_hovered); + render_button( + frame, + report_rect, + report_label.as_str(), + app.about_report_mode_hovered, + ); + render_button( + frame, + fingerprint_rect, + fingerprint_toggle_label, + app.about_fingerprint_hovered, + ); +} + +fn render_help(frame: &mut Frame<'_>, advanced_mode: bool) { + let mut lines = vec![ + Line::from("Beginner flow:"), + Line::from("1) Select your controller"), + Line::from("2) Choose Recommended Update or Diagnose"), + Line::from("3) Follow the on-screen confirmation"), + Line::from(""), + Line::from("Optional shortcuts:"), + Line::from("u=update d=diagnose r=refresh a=about q=quit"), + ]; + if advanced_mode { + lines.push(Line::from("")); + lines.push(Line::from( + "Advanced hotkeys (reports): c=copy path, o=open report, f=open folder", + )); + } + lines.push(Line::from("Esc/Enter/click to return")); + let paragraph = + Paragraph::new(lines).block(Block::default().borders(Borders::ALL).title("Help")); + frame.render_widget(paragraph, frame.area()); +} + +fn render_waiting(frame: &mut Frame<'_>, app: &TuiApp) { + let layout = waiting_layout(frame.area()); + + let header = Paragraph::new(Line::from(vec![ + Span::styled("OpenBitdo", Style::default().fg(Color::Cyan)), + Span::raw(" Beginner wizard"), + ])) + .block(Block::default().borders(Borders::ALL).title("Welcome")); + + let body_lines = vec![ + Line::from("No supported 8BitDo controller is detected yet."), + Line::from(""), + Line::from("Plug in your controller and click Refresh."), + Line::from("If you need help, open Help for a quick walkthrough."), + Line::from(""), + Line::from(app.last_message.clone()), + ]; + let body = Paragraph::new(body_lines).block( + Block::default() + .borders(Borders::ALL) + .title("Waiting for Controller"), + ); + + frame.render_widget(header, layout.header); + frame.render_widget(body, layout.body); + + for (rect, action) in action_buttons(layout.actions, &WAIT_ACTIONS) { + render_button( + frame, + rect, + action.label(), + app.hovered_action == Some(action), + ); + } + + let hover_hint = app + .hovered_action + .map(|action| action_tooltip(action, app.advanced_mode)) + .unwrap_or("Mouse: click buttons. Keyboard: Enter refresh, ? help, q quit"); + let footer = + Paragraph::new(hover_hint).block(Block::default().borders(Borders::ALL).title("Controls")); + frame.render_widget(footer, layout.footer); +} + +fn render_home(frame: &mut Frame<'_>, app: &TuiApp) { + let layout = home_layout(frame.area()); + + let title = Paragraph::new(Line::from(vec![ + Span::styled("OpenBitdo", Style::default().fg(Color::Cyan)), + Span::raw(" Choose a controller and action"), + ])) + .block(Block::default().borders(Borders::ALL).title("Home")); + + let visible_rows = layout.devices.height.saturating_sub(2) as usize; + let window_start = if app.selected_index >= visible_rows && visible_rows > 0 { + app.selected_index + 1 - visible_rows + } else { + 0 + }; + let window_end = (window_start + visible_rows).min(app.devices.len()); + + let mut device_items = Vec::new(); + if app.devices.is_empty() { + device_items.push(ListItem::new("No controllers detected")); + } else { + for (idx, dev) in app.devices[window_start..window_end].iter().enumerate() { + let absolute_idx = window_start + idx; + let status = beginner_status_label(dev); + let line = format!( + "{:04x}:{:04x} {} [{}]", + dev.vid_pid.vid, + dev.vid_pid.pid, + display_device_name(dev), + status + ); + let style = if absolute_idx == app.selected_index { + Style::default() + .fg(Color::Black) + .bg(Color::Cyan) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::White) + }; + device_items.push(ListItem::new(line).style(style)); + } + } + + let device_list = + List::new(device_items).block(Block::default().borders(Borders::ALL).title("Controllers")); + + frame.render_widget(title, layout.title); + frame.render_widget(device_list, layout.devices); + + let update_blocked = app + .selected_device() + .map(|d| d.support_tier != SupportTier::Full) + .unwrap_or(true); + + for (rect, action) in action_buttons(layout.actions, &HOME_ACTIONS) { + let label = if action == HomeAction::Update && update_blocked { + "Recommended Update [Coming soon]" + } else { + action.label() + }; + render_button(frame, rect, label, app.hovered_action == Some(action)); + } + + let gauge = Gauge::default() + .block(Block::default().title("Progress").borders(Borders::ALL)) + .gauge_style(Style::default().fg(Color::Green)) + .percent(app.progress as u16) + .label(format!("{}%", app.progress)); + frame.render_widget(gauge, layout.progress); + + let selected_summary = app + .selected_device() + .map(|d| { + let tooltip = app + .hovered_action + .map(|action| { + if action == HomeAction::Update && update_blocked { + "Recommended update support for this device is coming soon." + } else { + action_tooltip(action, app.advanced_mode) + } + }) + .unwrap_or(""); + let actions = available_actions_summary(d); + format!( + "Selected: {} ({:04x}:{:04x})\nStatus: {}\nCurrent user actions: {}\n{}\n{}", + display_device_name(d), + d.vid_pid.vid, + d.vid_pid.pid, + beginner_status_label(d), + actions, + app.last_message, + tooltip + ) + }) + .unwrap_or_else(|| format!("No controller selected\n{}", app.last_message)); + + let detail = Paragraph::new(selected_summary) + .scroll((app.detail_scroll, 0)) + .block(Block::default().borders(Borders::ALL).title("Guidance")); + frame.render_widget(detail, layout.detail); + + let blocked_text = app + .selected_device() + .map(blocked_action_panel_text) + .unwrap_or_else(|| "Blocked Actions: none".to_owned()); + let blocked = Paragraph::new(blocked_text).block( + Block::default() + .borders(Borders::ALL) + .title("Blocked Actions"), + ); + frame.render_widget(blocked, layout.blocked); + + if let Some(menu) = app.context_menu { + let menu_rect = context_menu_rect(frame.area(), menu); + let items = CONTEXT_ACTIONS + .iter() + .enumerate() + .map(|(idx, action)| { + let style = if menu.hovered_index == Some(idx) { + Style::default() + .fg(Color::Black) + .bg(Color::Cyan) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::White) + }; + ListItem::new(action.label()).style(style) + }) + .collect::>(); + let popup = List::new(items).block(Block::default().borders(Borders::ALL).title("Actions")); + frame.render_widget(popup, menu_rect); + } +} + +fn render_jp108_mapping(frame: &mut Frame<'_>, app: &TuiApp) { + let layout = simple_action_layout(frame.area()); + let mut lines = vec![ + Line::from("JP108 Dedicated Button Mapping"), + Line::from("Use Up/Down to select, Left/Right to change mapped HID usage."), + Line::from(""), + ]; + + for (idx, mapping) in app.jp108_mappings.iter().enumerate() { + let marker = if idx == app.jp108_selected { ">" } else { " " }; + lines.push(Line::from(format!( + "{marker} {:?} -> 0x{:04x}", + mapping.button, mapping.target_hid_usage + ))); + } + + lines.push(Line::from("")); + lines.push(Line::from(app.last_message.clone())); + + let body = + Paragraph::new(lines).block(Block::default().borders(Borders::ALL).title("JP108 Wizard")); + frame.render_widget(body, layout.body); + + for (rect, action) in flow_buttons(layout.actions, &DEVICE_FLOW_ACTIONS) { + render_button(frame, rect, action.label(), false); + } + + let footer = Paragraph::new("b=apply r=reload s=restore t=test f=firmware Esc=done") + .block(Block::default().borders(Borders::ALL).title("Controls")); + frame.render_widget(footer, layout.footer); +} + +fn render_u2_profile(frame: &mut Frame<'_>, app: &TuiApp) { + let layout = simple_action_layout(frame.area()); + let mut lines = vec![ + Line::from("Ultimate2 Core Profile Mapping"), + Line::from("Use Up/Down to select button mapping, Left/Right to adjust usage."), + Line::from("Press 1/2/3 to select slot, m to cycle mode."), + Line::from(""), + ]; + + if let Some(profile) = app.u2_profile.as_ref() { + lines.push(Line::from(format!("Slot: {}", slot_label(profile.slot)))); + lines.push(Line::from(format!("Mode: {}", profile.mode))); + lines.push(Line::from(format!( + "Firmware version: {}", + profile.firmware_version + ))); + lines.push(Line::from(format!( + "L2 analog: {:.2} {}", + profile.l2_analog, + if profile.supports_trigger_write { + "(write-enabled)" + } else { + "(read-only)" + } + ))); + lines.push(Line::from(format!( + "R2 analog: {:.2} {}", + profile.r2_analog, + if profile.supports_trigger_write { + "(write-enabled)" + } else { + "(read-only)" + } + ))); + lines.push(Line::from("")); + + for (idx, mapping) in profile.mappings.iter().enumerate() { + let marker = if idx == app.u2_selected { ">" } else { " " }; + lines.push(Line::from(format!( + "{marker} {:?} -> {} (0x{:04x})", + mapping.button, + u2_target_label(mapping.target_hid_usage), + mapping.target_hid_usage + ))); + } + } else { + lines.push(Line::from("No profile loaded.")); + } + + lines.push(Line::from("")); + lines.push(Line::from(app.last_message.clone())); + + let body = Paragraph::new(lines).block( + Block::default() + .borders(Borders::ALL) + .title("Ultimate2 Wizard"), + ); + frame.render_widget(body, layout.body); + + for (rect, action) in flow_buttons(layout.actions, &DEVICE_FLOW_ACTIONS) { + render_button(frame, rect, action.label(), false); + } + + let footer = + Paragraph::new("b=apply r=reload s=restore t=test f=firmware [ ]=L2 ; '=R2 Esc=done") + .block(Block::default().borders(Borders::ALL).title("Controls")); + frame.render_widget(footer, layout.footer); +} + +fn render_recovery(frame: &mut Frame<'_>, app: &TuiApp) { + let layout = simple_action_layout(frame.area()); + let mut lines = vec![ + Line::from("Recovery Wizard"), + Line::from(""), + Line::from("A write operation failed and automatic rollback also failed."), + Line::from("Write actions are now locked until OpenBitdo is restarted."), + Line::from(""), + Line::from("Safe sequence: auto rollback -> verify -> retry -> safe exit."), + Line::from("Current state: auto rollback did not fully recover."), + ]; + + if let Some(report) = app.recovery_report.as_ref() { + if let Some(write_error) = report.write_error.as_deref() { + lines.push(Line::from(format!("Write error: {write_error}"))); + } + if let Some(rollback_error) = report.rollback_error.as_deref() { + lines.push(Line::from(format!("Rollback error: {rollback_error}"))); + } + lines.push(Line::from(format!( + "Rollback attempted: {}", + if report.rollback_attempted { + "yes" + } else { + "no" + } + ))); + } + lines.push(Line::from("")); + lines.push(Line::from(app.last_message.clone())); + + let body = Paragraph::new(lines).block( + Block::default() + .borders(Borders::ALL) + .title("Needs Attention"), + ); + frame.render_widget(body, layout.body); + + let buttons = action_buttons( + layout.actions, + &[HomeAction::Refresh, HomeAction::About, HomeAction::Quit], + ); + for (rect, action) in buttons { + let label = match action { + HomeAction::Refresh => "Try Restore Backup", + HomeAction::About => "Safe Exit to Home", + HomeAction::Quit => "Quit", + _ => action.label(), + }; + render_button(frame, rect, label, false); + } + + let footer = Paragraph::new("r=restore backup Enter/Esc=safe exit q=quit") + .block(Block::default().borders(Borders::ALL).title("Controls")); + frame.render_widget(footer, layout.footer); +} + +fn render_preflight(frame: &mut Frame<'_>, app: &TuiApp) { + let layout = simple_action_layout(frame.area()); + + let mut lines = vec![Line::from("Review update preflight"), Line::from("")]; + if let Some(pending) = app.pending_update.as_ref() { + lines.push(Line::from(format!( + "Device: {} ({:04x}:{:04x})", + pending.target.name, pending.target.vid_pid.vid, pending.target.vid_pid.pid + ))); + lines.push(Line::from(format!( + "Firmware source: {}", + pending.firmware_source + ))); + lines.push(Line::from(format!( + "Firmware version: {}", + pending.firmware_version + ))); + lines.push(Line::from(format!( + "Image path: {}", + pending.firmware_path.display() + ))); + lines.push(Line::from(format!( + "Chunk size: {} bytes", + pending.plan.chunk_size + ))); + lines.push(Line::from(format!("Chunks: {}", pending.plan.chunks_total))); + lines.push(Line::from(format!( + "Estimated transfer time: {}s", + pending.plan.expected_seconds + ))); + lines.push(Line::from("")); + lines.push(Line::from("Warnings:")); + for warning in &pending.plan.warnings { + lines.push(Line::from(format!("- {warning}"))); + } + } else { + lines.push(Line::from("No preflight details available.")); + } + + lines.push(Line::from("")); + lines.push(Line::from("Click I Understand to start transfer.")); + + let body = + Paragraph::new(lines).block(Block::default().borders(Borders::ALL).title("Preflight")); + frame.render_widget(body, layout.body); + + let buttons = action_buttons(layout.actions, &[HomeAction::Update, HomeAction::Quit]); + for (rect, action) in buttons { + let label = match action { + HomeAction::Update => "I Understand", + HomeAction::Quit => "Cancel", + _ => action.label(), + }; + render_button(frame, rect, label, false); + } + + let footer = Paragraph::new("Enter to confirm, Esc to cancel") + .block(Block::default().borders(Borders::ALL).title("Controls")); + frame.render_widget(footer, layout.footer); +} + +fn render_updating(frame: &mut Frame<'_>, app: &TuiApp) { + let layout = simple_action_layout(frame.area()); + let selected = app + .selected_device() + .map(|d| format!("{} ({:04x}:{:04x})", d.name, d.vid_pid.vid, d.vid_pid.pid)) + .unwrap_or_else(|| "Unknown controller".to_owned()); + + let body_lines = vec![ + Line::from("Firmware transfer in progress"), + Line::from(""), + Line::from(format!("Device: {selected}")), + Line::from(format!("Status: {}", app.last_message)), + Line::from(""), + Line::from("Do not disconnect the controller during transfer."), + ]; + + let body = + Paragraph::new(body_lines).block(Block::default().borders(Borders::ALL).title("Updating")); + frame.render_widget(body, layout.body); + + let buttons = action_buttons(layout.actions, &[HomeAction::Quit]); + for (rect, action) in buttons { + let label = if action == HomeAction::Quit { + "Cancel Update" + } else { + action.label() + }; + render_button(frame, rect, label, false); + } + + let footer = Gauge::default() + .block(Block::default().title("Progress").borders(Borders::ALL)) + .gauge_style(Style::default().fg(Color::Green)) + .percent(app.progress as u16) + .label(format!("{}%", app.progress)); + frame.render_widget(footer, layout.footer); +} + +fn render_final_report(frame: &mut Frame<'_>, app: &TuiApp) { + let layout = simple_action_layout(frame.area()); + + let (status, message) = app + .final_report + .as_ref() + .map(|report| { + ( + format!("{:?}", report.status), + format!( + "{}\nChunks sent: {}/{}", + report.message, report.chunks_sent, report.chunks_total + ), + ) + }) + .unwrap_or_else(|| ("Unknown".to_owned(), app.last_message.clone())); + + let body = Paragraph::new(vec![ + Line::from("Update finished"), + Line::from(""), + Line::from(format!("Result: {status}")), + Line::from(message), + Line::from(""), + Line::from("Done returns to Home. Quit exits OpenBitdo."), + ]) + .block( + Block::default() + .borders(Borders::ALL) + .title("Final Summary"), + ); + frame.render_widget(body, layout.body); + + let buttons = action_buttons(layout.actions, &[HomeAction::Refresh, HomeAction::Quit]); + for (rect, action) in buttons { + let label = if action == HomeAction::Refresh { + "Done" + } else { + action.label() + }; + render_button(frame, rect, label, false); + } + + let footer = Paragraph::new(app.last_message.as_str()) + .block(Block::default().borders(Borders::ALL).title("Status")); + frame.render_widget(footer, layout.footer); +} + +pub async fn run_tui_flow( + core: OpenBitdoCore, + request: TuiRunRequest, +) -> Result { + let mut app = TuiApp { + state: TuiWorkflowState::Preflight, + selected: Some(request.vid_pid), + last_message: format!("preflighting {}", request.vid_pid), + ..Default::default() + }; + + let mut terminal = if request.no_ui { + None + } else { + Some(init_terminal()?) + }; + render_if_needed(&mut terminal, &app)?; + + let preflight = core + .preflight_firmware(FirmwarePreflightRequest { + vid_pid: request.vid_pid, + firmware_path: request.firmware_path.clone(), + allow_unsafe: request.allow_unsafe, + brick_risk_ack: request.brick_risk_ack, + experimental: request.experimental, + chunk_size: request.chunk_size, + }) + .await?; + + if !preflight.gate.allowed { + teardown_terminal(&mut terminal)?; + return Err(anyhow!( + "preflight denied: {}", + preflight + .gate + .message + .unwrap_or_else(|| "policy denied".to_owned()) + )); + } + + let plan = preflight.plan.expect("plan exists when gate is allowed"); + app.set_session(plan.session_id.clone()); + render_if_needed(&mut terminal, &app)?; + + core.start_firmware(FirmwareStartRequest { + session_id: plan.session_id.clone(), + }) + .await?; + + core.confirm_firmware(FirmwareConfirmRequest { + session_id: plan.session_id.clone(), + acknowledged_risk: request.acknowledged_risk, + }) + .await?; + + let mut receiver = core.subscribe_events(&plan.session_id.0).await?; + loop { + tokio::select! { + evt = receiver.recv() => { + if let Ok(evt) = evt { + app.apply_progress(evt.progress, format!("{}: {}", evt.stage, evt.message)); + render_if_needed(&mut terminal, &app)?; + if evt.terminal { + break; + } + } + } + _ = sleep(Duration::from_millis(10)) => { + if let Some(report) = core.firmware_report(&plan.session_id.0).await? { + app.complete(report.clone()); + render_if_needed(&mut terminal, &app)?; + teardown_terminal(&mut terminal)?; + return Ok(report); + } + } + } + + if !request.no_ui && event::poll(Duration::from_millis(1))? { + if let CEvent::Key(key) = event::read()? { + if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) { + let report = core + .cancel_firmware(FirmwareCancelRequest { + session_id: plan.session_id.clone(), + }) + .await?; + app.complete(report.clone()); + render_if_needed(&mut terminal, &app)?; + teardown_terminal(&mut terminal)?; + return Ok(report); + } + } + } + } + + let report = core + .firmware_report(&plan.session_id.0) + .await? + .ok_or_else(|| anyhow!("missing final report"))?; + app.complete(report.clone()); + render_if_needed(&mut terminal, &app)?; + teardown_terminal(&mut terminal)?; + Ok(report) +} + +fn init_terminal() -> Result>> { + use crossterm::event::EnableMouseCapture; + + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let terminal = Terminal::new(backend)?; + Ok(terminal) +} + +fn teardown_terminal(terminal: &mut Option>>) -> Result<()> { + use crossterm::event::DisableMouseCapture; + + if let Some(mut t) = terminal.take() { + disable_raw_mode()?; + execute!(t.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?; + t.show_cursor()?; + } + Ok(()) +} + +fn prompt_line( + terminal: &mut Option>>, + prompt: &str, +) -> Result { + let had_terminal = terminal.is_some(); + if had_terminal { + teardown_terminal(terminal)?; + } + + print!("{prompt}"); + io::stdout().flush()?; + + let mut line = String::new(); + io::stdin().read_line(&mut line)?; + + if had_terminal { + *terminal = Some(init_terminal()?); + } + + Ok(line.trim().to_owned()) +} + +fn handle_report_hotkey(app: &mut TuiApp, key: KeyCode) -> Result { + let Some(path) = app.latest_report_path.clone() else { + return Ok(false); + }; + + match key { + KeyCode::Char('c') => { + copy_text_to_clipboard(path.to_string_lossy().as_ref())?; + app.last_message = format!( + "Copied report path to clipboard: {}", + path.to_string_lossy() + ); + Ok(true) + } + KeyCode::Char('o') => { + open_path_with_default_app(&path)?; + app.last_message = format!("Opened report: {}", path.to_string_lossy()); + Ok(true) + } + KeyCode::Char('f') => { + let folder = path + .parent() + .map(Path::to_path_buf) + .unwrap_or_else(std::env::temp_dir); + open_path_with_default_app(&folder)?; + app.last_message = format!("Opened report folder: {}", folder.to_string_lossy()); + Ok(true) + } + _ => Ok(false), + } +} + +#[cfg(test)] +mod tests; diff --git a/sdk/crates/bitdo_tui/src/settings.rs b/sdk/crates/bitdo_tui/src/settings.rs new file mode 100644 index 0000000..a2abbaf --- /dev/null +++ b/sdk/crates/bitdo_tui/src/settings.rs @@ -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(()) +} diff --git a/sdk/crates/bitdo_tui/src/support_report.rs b/sdk/crates/bitdo_tui/src/support_report.rs new file mode 100644 index 0000000..c89180a --- /dev/null +++ b/sdk/crates/bitdo_tui/src/support_report.rs @@ -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, + status: String, + message: String, + diag: Option, + firmware: Option, +} + +#[derive(Clone, Debug, Serialize)] +struct SupportReportDevice { + vid: u16, + pid: u16, + name: String, + canonical_id: String, + runtime_label: String, + serial: Option, + 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 { + 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> { + 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) +} diff --git a/sdk/crates/bitdo_tui/src/tests.rs b/sdk/crates/bitdo_tui/src/tests.rs new file mode 100644 index 0000000..30fd13e --- /dev/null +++ b/sdk/crates/bitdo_tui/src/tests.rs @@ -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()); +} diff --git a/sdk/crates/bitdoctl/Cargo.toml b/sdk/crates/bitdoctl/Cargo.toml deleted file mode 100644 index c86c5a5..0000000 --- a/sdk/crates/bitdoctl/Cargo.toml +++ /dev/null @@ -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" diff --git a/sdk/crates/bitdoctl/src/main.rs b/sdk/crates/bitdoctl/src/main.rs deleted file mode 100644 index 27539d7..0000000 --- a/sdk/crates/bitdoctl/src/main.rs +++ /dev/null @@ -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, - #[arg(long)] - pid: Option, - #[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 = 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 { - 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 { - 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::()?) -} - -fn mock_transport_for(command: &Commands, target: VidPid) -> Result { - 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 { - vec![0x02, 0x01, 0x00, 0x00] -} - -fn build_mode_response(mode: u8) -> Vec { - let mut out = vec![0u8; 64]; - out[0] = 0x02; - out[1] = 0x05; - out[5] = mode; - out -} - -fn build_rr_response() -> Vec { - let mut out = vec![0u8; 64]; - out[0] = 0x02; - out[1] = 0x04; - out[5] = 0x01; - out -} - -fn build_version_response() -> Vec { - 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 { - 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); - } -} diff --git a/sdk/crates/openbitdo/Cargo.toml b/sdk/crates/openbitdo/Cargo.toml new file mode 100644 index 0000000..a37c549 --- /dev/null +++ b/sdk/crates/openbitdo/Cargo.toml @@ -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" diff --git a/sdk/crates/openbitdo/build.rs b/sdk/crates/openbitdo/build.rs new file mode 100644 index 0000000..f7bcf33 --- /dev/null +++ b/sdk/crates/openbitdo/build.rs @@ -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) { + 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 { + 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()) +} diff --git a/sdk/crates/openbitdo/src/lib.rs b/sdk/crates/openbitdo/src/lib.rs new file mode 100644 index 0000000..db9422d --- /dev/null +++ b/sdk/crates/openbitdo/src/lib.rs @@ -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); + } +} diff --git a/sdk/crates/openbitdo/src/main.rs b/sdk/crates/openbitdo/src/main.rs new file mode 100644 index 0000000..3973e8f --- /dev/null +++ b/sdk/crates/openbitdo/src/main.rs @@ -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); + } +} diff --git a/sdk/crates/openbitdo/tests/cli_smoke.rs b/sdk/crates/openbitdo/tests/cli_smoke.rs new file mode 100644 index 0000000..e197d75 --- /dev/null +++ b/sdk/crates/openbitdo/tests/cli_smoke.rs @@ -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()); +} diff --git a/sdk/scripts/cleanroom_guard.sh b/sdk/scripts/cleanroom_guard.sh index d01f629..6541c54 100755 --- a/sdk/scripts/cleanroom_guard.sh +++ b/sdk/scripts/cleanroom_guard.sh @@ -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 diff --git a/sdk/scripts/package-linux.sh b/sdk/scripts/package-linux.sh new file mode 100755 index 0000000..e7b7fce --- /dev/null +++ b/sdk/scripts/package-linux.sh @@ -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" diff --git a/sdk/scripts/package-macos.sh b/sdk/scripts/package-macos.sh new file mode 100755 index 0000000..cb7e229 --- /dev/null +++ b/sdk/scripts/package-macos.sh @@ -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" diff --git a/sdk/scripts/run_hardware_smoke.sh b/sdk/scripts/run_hardware_smoke.sh index 1a8eed6..ad27adb 100755 --- a/sdk/scripts/run_hardware_smoke.sh +++ b/sdk/scripts/run_hardware_smoke.sh @@ -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" diff --git a/sdk/tests/alias_index_integrity.rs b/sdk/tests/alias_index_integrity.rs new file mode 100644 index 0000000..7e3ba22 --- /dev/null +++ b/sdk/tests/alias_index_integrity.rs @@ -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::>(); + assert!(names.contains(&"PID_Pro2")); + assert!(names.contains(&"PID_Mouse")); + assert!(!names.contains(&"PID_Pro2_OLD")); + assert!(!names.contains(&"PID_ASLGMouse")); +} diff --git a/sdk/tests/candidate_readonly_gating.rs b/sdk/tests/candidate_readonly_gating.rs new file mode 100644 index 0000000..5344440 --- /dev/null +++ b/sdk/tests/candidate_readonly_gating.rs @@ -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(); +} diff --git a/sdk/tests/cli_snapshot.rs b/sdk/tests/cli_snapshot.rs deleted file mode 100644 index 98dd765..0000000 --- a/sdk/tests/cli_snapshot.rs +++ /dev/null @@ -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); -} diff --git a/sdk/tests/command_matrix_coverage.rs b/sdk/tests/command_matrix_coverage.rs new file mode 100644 index 0000000..589f413 --- /dev/null +++ b/sdk/tests/command_matrix_coverage.rs @@ -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::>(); + 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::>(); + 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::>(); + 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}")) +} diff --git a/sdk/tests/diag_probe.rs b/sdk/tests/diag_probe.rs index 845a227..5d534ff 100644 --- a/sdk/tests/diag_probe.rs +++ b/sdk/tests/diag_probe.rs @@ -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)); } diff --git a/sdk/tests/firmware_chunk.rs b/sdk/tests/firmware_chunk.rs index 8580d18..efc860f 100644 --- a/sdk/tests/firmware_chunk.rs +++ b/sdk/tests/firmware_chunk.rs @@ -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); } diff --git a/sdk/tests/frame_roundtrip.rs b/sdk/tests/frame_roundtrip.rs index 28445cb..a2fe465 100644 --- a/sdk/tests/frame_roundtrip.rs +++ b/sdk/tests/frame_roundtrip.rs @@ -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::>(); + assert_eq!(unique.len(), CommandId::all().len()); + assert!(command_registry().len() >= unique.len()); for row in command_registry() { let frame = CommandFrame { diff --git a/sdk/tests/hardware_smoke.rs b/sdk/tests/hardware_smoke.rs index db287b9..33ff32b 100644 --- a/sdk/tests/hardware_smoke.rs +++ b/sdk/tests/hardware_smoke.rs @@ -1,23 +1,91 @@ -use bitdo_proto::{device_profile_for, enumerate_hid_devices, ProtocolFamily, VidPid}; +use bitdo_proto::{ + device_profile_for, enumerate_hid_devices, DeviceSession, HidTransport, ProtocolFamily, + SessionConfig, VidPid, +}; fn hardware_enabled() -> bool { std::env::var("BITDO_HARDWARE").ok().as_deref() == Some("1") } -fn expected_pid(env_key: &str) -> Option { - std::env::var(env_key).ok().and_then(|v| { - let trimmed = v.trim(); - if let Some(hex) = trimmed - .strip_prefix("0x") - .or_else(|| trimmed.strip_prefix("0X")) - { - u16::from_str_radix(hex, 16).ok() - } else { - trimmed.parse::().ok() - } +fn parse_pid(input: &str) -> Option { + let trimmed = input.trim(); + if let Some(hex) = trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")) + { + u16::from_str_radix(hex, 16).ok() + } else { + trimmed.parse::().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 { + 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::>() + ); + + 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::>() + ); + + 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(); } diff --git a/sdk/tests/pid_matrix_coverage.rs b/sdk/tests/pid_matrix_coverage.rs index 34ffe7d..dc91719 100644 --- a/sdk/tests/pid_matrix_coverage.rs +++ b/sdk/tests/pid_matrix_coverage.rs @@ -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::>(); + 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::>(); + 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}"), + } } diff --git a/sdk/tests/pid_registry_unique.rs b/sdk/tests/pid_registry_unique.rs new file mode 100644 index 0000000..5a1efd1 --- /dev/null +++ b/sdk/tests/pid_registry_unique.rs @@ -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 + ); + } +} diff --git a/sdk/tests/runtime_policy.rs b/sdk/tests/runtime_policy.rs new file mode 100644 index 0000000..7adc634 --- /dev/null +++ b/sdk/tests/runtime_policy.rs @@ -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 + )); +} diff --git a/spec/alias_index.md b/spec/alias_index.md new file mode 100644 index 0000000..205976d --- /dev/null +++ b/spec/alias_index.md @@ -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`. | diff --git a/spec/command_matrix.csv b/spec/command_matrix.csv index bc79462..df895db 100644 --- a/spec/command_matrix.csv +++ b/spec/command_matrix.csv @@ -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 diff --git a/spec/device_name_catalog.md b/spec/device_name_catalog.md new file mode 100644 index 0000000..6c139ba --- /dev/null +++ b/spec/device_name_catalog.md @@ -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. diff --git a/spec/dossiers/2028/core_diag.toml b/spec/dossiers/2028/core_diag.toml new file mode 100644 index 0000000..07d560b --- /dev/null +++ b/spec/dossiers/2028/core_diag.toml @@ -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" diff --git a/spec/dossiers/2028/firmware.toml b/spec/dossiers/2028/firmware.toml new file mode 100644 index 0000000..f93580a --- /dev/null +++ b/spec/dossiers/2028/firmware.toml @@ -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" diff --git a/spec/dossiers/202e/core_diag.toml b/spec/dossiers/202e/core_diag.toml new file mode 100644 index 0000000..104ddc1 --- /dev/null +++ b/spec/dossiers/202e/core_diag.toml @@ -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" diff --git a/spec/dossiers/202e/firmware.toml b/spec/dossiers/202e/firmware.toml new file mode 100644 index 0000000..115fdf8 --- /dev/null +++ b/spec/dossiers/202e/firmware.toml @@ -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" diff --git a/spec/dossiers/2039/core_diag.toml b/spec/dossiers/2039/core_diag.toml new file mode 100644 index 0000000..2fe186c --- /dev/null +++ b/spec/dossiers/2039/core_diag.toml @@ -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" diff --git a/spec/dossiers/203a/core_diag.toml b/spec/dossiers/203a/core_diag.toml new file mode 100644 index 0000000..62a1368 --- /dev/null +++ b/spec/dossiers/203a/core_diag.toml @@ -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" diff --git a/spec/dossiers/203a/firmware.toml b/spec/dossiers/203a/firmware.toml new file mode 100644 index 0000000..8251bb8 --- /dev/null +++ b/spec/dossiers/203a/firmware.toml @@ -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" diff --git a/spec/dossiers/2049/core_diag.toml b/spec/dossiers/2049/core_diag.toml new file mode 100644 index 0000000..687f8f1 --- /dev/null +++ b/spec/dossiers/2049/core_diag.toml @@ -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" diff --git a/spec/dossiers/2049/firmware.toml b/spec/dossiers/2049/firmware.toml new file mode 100644 index 0000000..6b70e2b --- /dev/null +++ b/spec/dossiers/2049/firmware.toml @@ -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" diff --git a/spec/dossiers/2056/core_diag.toml b/spec/dossiers/2056/core_diag.toml new file mode 100644 index 0000000..674c115 --- /dev/null +++ b/spec/dossiers/2056/core_diag.toml @@ -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" diff --git a/spec/dossiers/2100/core_diag.toml b/spec/dossiers/2100/core_diag.toml new file mode 100644 index 0000000..3fd8e85 --- /dev/null +++ b/spec/dossiers/2100/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-2100-CORE-DIAG-W2" +pid_hex = "0x2100" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_Xcloud; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/2100/firmware_preflight.toml b/spec/dossiers/2100/firmware_preflight.toml new file mode 100644 index 0000000..9764490 --- /dev/null +++ b/spec/dossiers/2100/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-2100-FW-PREFLIGHT-W2" +pid_hex = "0x2100" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_Xcloud; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/2100/mode_or_profile_read.toml b/spec/dossiers/2100/mode_or_profile_read.toml new file mode 100644 index 0000000..75e6220 --- /dev/null +++ b/spec/dossiers/2100/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-2100-MODEPROFILE-W2" +pid_hex = "0x2100" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_Xcloud; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/2101/core_diag.toml b/spec/dossiers/2101/core_diag.toml new file mode 100644 index 0000000..077bf73 --- /dev/null +++ b/spec/dossiers/2101/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-2101-CORE-DIAG-W2" +pid_hex = "0x2101" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_Xcloud2; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/2101/firmware_preflight.toml b/spec/dossiers/2101/firmware_preflight.toml new file mode 100644 index 0000000..a955c1f --- /dev/null +++ b/spec/dossiers/2101/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-2101-FW-PREFLIGHT-W2" +pid_hex = "0x2101" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_Xcloud2; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/2101/mode_or_profile_read.toml b/spec/dossiers/2101/mode_or_profile_read.toml new file mode 100644 index 0000000..a6fb411 --- /dev/null +++ b/spec/dossiers/2101/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-2101-MODEPROFILE-W2" +pid_hex = "0x2101" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_Xcloud2; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/3004/core_diag.toml b/spec/dossiers/3004/core_diag.toml new file mode 100644 index 0000000..15aaeda --- /dev/null +++ b/spec/dossiers/3004/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3004-CORE" +pid_hex = "0x3004" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle", "GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report with command byte and optional slot/mode payload" +response_shape = "status header with mode/profile payload bytes" +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 = "Standard64 dispatch" +notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet" diff --git a/spec/dossiers/3004/firmware.toml b/spec/dossiers/3004/firmware.toml new file mode 100644 index 0000000..134748f --- /dev/null +++ b/spec/dossiers/3004/firmware.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3004-FIRMWARE" +pid_hex = "0x3004" +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" diff --git a/spec/dossiers/3010/core_diag.toml b/spec/dossiers/3010/core_diag.toml new file mode 100644 index 0000000..025f209 --- /dev/null +++ b/spec/dossiers/3010/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3010-CORE" +pid_hex = "0x3010" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle", "GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report with command byte and optional slot/mode payload" +response_shape = "status header with mode/profile payload bytes" +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 = "Standard64 dispatch" +notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet" diff --git a/spec/dossiers/3010/firmware.toml b/spec/dossiers/3010/firmware.toml new file mode 100644 index 0000000..d190a15 --- /dev/null +++ b/spec/dossiers/3010/firmware.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3010-FIRMWARE" +pid_hex = "0x3010" +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" diff --git a/spec/dossiers/3011/core_diag.toml b/spec/dossiers/3011/core_diag.toml new file mode 100644 index 0000000..62aea0e --- /dev/null +++ b/spec/dossiers/3011/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3011-CORE" +pid_hex = "0x3011" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle", "GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report with command byte and optional slot/mode payload" +response_shape = "status header with mode/profile payload bytes" +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 = "Standard64 dispatch" +notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet" diff --git a/spec/dossiers/3011/firmware.toml b/spec/dossiers/3011/firmware.toml new file mode 100644 index 0000000..434664c --- /dev/null +++ b/spec/dossiers/3011/firmware.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3011-FIRMWARE" +pid_hex = "0x3011" +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" diff --git a/spec/dossiers/3012/core_diag.toml b/spec/dossiers/3012/core_diag.toml new file mode 100644 index 0000000..8118af5 --- /dev/null +++ b/spec/dossiers/3012/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3012-CORE" +pid_hex = "0x3012" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle", "GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report with command byte and optional slot/mode payload" +response_shape = "status header with mode/profile payload bytes" +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 = "Standard64 dispatch" +notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet" diff --git a/spec/dossiers/3012/firmware.toml b/spec/dossiers/3012/firmware.toml new file mode 100644 index 0000000..e75dae4 --- /dev/null +++ b/spec/dossiers/3012/firmware.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3012-FIRMWARE" +pid_hex = "0x3012" +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" diff --git a/spec/dossiers/3013/core_diag.toml b/spec/dossiers/3013/core_diag.toml new file mode 100644 index 0000000..deeac4c --- /dev/null +++ b/spec/dossiers/3013/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3013-CORE" +pid_hex = "0x3013" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle", "GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report with command byte and optional slot/mode payload" +response_shape = "status header with mode/profile payload bytes" +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 = "Standard64 dispatch" +notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet" diff --git a/spec/dossiers/3013/firmware.toml b/spec/dossiers/3013/firmware.toml new file mode 100644 index 0000000..d89621b --- /dev/null +++ b/spec/dossiers/3013/firmware.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3013-FIRMWARE" +pid_hex = "0x3013" +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" diff --git a/spec/dossiers/3019/core_diag.toml b/spec/dossiers/3019/core_diag.toml new file mode 100644 index 0000000..b45629a --- /dev/null +++ b/spec/dossiers/3019/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3019-CORE" +pid_hex = "0x3019" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle", "GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report with command byte and optional slot/mode payload" +response_shape = "status header with mode/profile payload bytes" +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 = "Standard64 dispatch" +notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet" diff --git a/spec/dossiers/3019/firmware.toml b/spec/dossiers/3019/firmware.toml new file mode 100644 index 0000000..dfc39ab --- /dev/null +++ b/spec/dossiers/3019/firmware.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3019-FIRMWARE" +pid_hex = "0x3019" +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" diff --git a/spec/dossiers/301a/core_diag.toml b/spec/dossiers/301a/core_diag.toml new file mode 100644 index 0000000..cc633da --- /dev/null +++ b/spec/dossiers/301a/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-301A-CORE-DIAG-W2" +pid_hex = "0x301a" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_UltimateBT2C; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/301a/firmware_preflight.toml b/spec/dossiers/301a/firmware_preflight.toml new file mode 100644 index 0000000..e551376 --- /dev/null +++ b/spec/dossiers/301a/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-301A-FW-PREFLIGHT-W2" +pid_hex = "0x301a" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_UltimateBT2C; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/301a/mode_or_profile_read.toml b/spec/dossiers/301a/mode_or_profile_read.toml new file mode 100644 index 0000000..537a25d --- /dev/null +++ b/spec/dossiers/301a/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-301A-MODEPROFILE-W2" +pid_hex = "0x301a" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_UltimateBT2C; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/3021/core_diag.toml b/spec/dossiers/3021/core_diag.toml new file mode 100644 index 0000000..89b6d95 --- /dev/null +++ b/spec/dossiers/3021/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3021-CORE" +pid_hex = "0x3021" +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" diff --git a/spec/dossiers/3026/core_diag.toml b/spec/dossiers/3026/core_diag.toml new file mode 100644 index 0000000..6a95315 --- /dev/null +++ b/spec/dossiers/3026/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3026-CORE-DIAG-W2" +pid_hex = "0x3026" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_XBOXUK; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/3026/firmware_preflight.toml b/spec/dossiers/3026/firmware_preflight.toml new file mode 100644 index 0000000..b3c34b7 --- /dev/null +++ b/spec/dossiers/3026/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3026-FW-PREFLIGHT-W2" +pid_hex = "0x3026" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_XBOXUK; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/3026/mode_or_profile_read.toml b/spec/dossiers/3026/mode_or_profile_read.toml new file mode 100644 index 0000000..fc27ac4 --- /dev/null +++ b/spec/dossiers/3026/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3026-MODEPROFILE-W2" +pid_hex = "0x3026" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_XBOXUK; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/3027/core_diag.toml b/spec/dossiers/3027/core_diag.toml new file mode 100644 index 0000000..b90a31f --- /dev/null +++ b/spec/dossiers/3027/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3027-CORE-DIAG-W2" +pid_hex = "0x3027" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_XBOXUKUSB; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/3027/firmware_preflight.toml b/spec/dossiers/3027/firmware_preflight.toml new file mode 100644 index 0000000..9cb4986 --- /dev/null +++ b/spec/dossiers/3027/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3027-FW-PREFLIGHT-W2" +pid_hex = "0x3027" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_XBOXUKUSB; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/3027/mode_or_profile_read.toml b/spec/dossiers/3027/mode_or_profile_read.toml new file mode 100644 index 0000000..4a94eef --- /dev/null +++ b/spec/dossiers/3027/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3027-MODEPROFILE-W2" +pid_hex = "0x3027" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_XBOXUKUSB; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/3100/core_diag.toml b/spec/dossiers/3100/core_diag.toml new file mode 100644 index 0000000..05c5953 --- /dev/null +++ b/spec/dossiers/3100/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3100-CORE-DIAG-W2" +pid_hex = "0x3100" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_USB_Ultimate; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/3100/firmware_preflight.toml b/spec/dossiers/3100/firmware_preflight.toml new file mode 100644 index 0000000..e860b16 --- /dev/null +++ b/spec/dossiers/3100/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3100-FW-PREFLIGHT-W2" +pid_hex = "0x3100" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_USB_Ultimate; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/3100/mode_or_profile_read.toml b/spec/dossiers/3100/mode_or_profile_read.toml new file mode 100644 index 0000000..58c2e51 --- /dev/null +++ b/spec/dossiers/3100/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3100-MODEPROFILE-W2" +pid_hex = "0x3100" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_USB_Ultimate; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/3105/core_diag.toml b/spec/dossiers/3105/core_diag.toml new file mode 100644 index 0000000..da8ec71 --- /dev/null +++ b/spec/dossiers/3105/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3105-CORE-DIAG-W2" +pid_hex = "0x3105" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_USB_Ultimate2; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/3105/firmware_preflight.toml b/spec/dossiers/3105/firmware_preflight.toml new file mode 100644 index 0000000..55c03fc --- /dev/null +++ b/spec/dossiers/3105/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3105-FW-PREFLIGHT-W2" +pid_hex = "0x3105" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_USB_Ultimate2; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/3105/mode_or_profile_read.toml b/spec/dossiers/3105/mode_or_profile_read.toml new file mode 100644 index 0000000..9eebb3d --- /dev/null +++ b/spec/dossiers/3105/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-3105-MODEPROFILE-W2" +pid_hex = "0x3105" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_USB_Ultimate2; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/5200/core_diag.toml b/spec/dossiers/5200/core_diag.toml new file mode 100644 index 0000000..bc4940e --- /dev/null +++ b/spec/dossiers/5200/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5200-CORE" +pid_hex = "0x5200" +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" diff --git a/spec/dossiers/5200/firmware.toml b/spec/dossiers/5200/firmware.toml new file mode 100644 index 0000000..c827846 --- /dev/null +++ b/spec/dossiers/5200/firmware.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5200-FIRMWARE" +pid_hex = "0x5200" +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" diff --git a/spec/dossiers/5201/core_diag.toml b/spec/dossiers/5201/core_diag.toml new file mode 100644 index 0000000..e2c14fa --- /dev/null +++ b/spec/dossiers/5201/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5201-CORE" +pid_hex = "0x5201" +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" diff --git a/spec/dossiers/5201/firmware.toml b/spec/dossiers/5201/firmware.toml new file mode 100644 index 0000000..5a01653 --- /dev/null +++ b/spec/dossiers/5201/firmware.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5201-FIRMWARE" +pid_hex = "0x5201" +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" diff --git a/spec/dossiers/5203/core_diag.toml b/spec/dossiers/5203/core_diag.toml new file mode 100644 index 0000000..64a0f70 --- /dev/null +++ b/spec/dossiers/5203/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5203-CORE-DIAG-W2" +pid_hex = "0x5203" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_NUMPAD; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/5203/firmware_preflight.toml b/spec/dossiers/5203/firmware_preflight.toml new file mode 100644 index 0000000..84bb41d --- /dev/null +++ b/spec/dossiers/5203/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5203-FW-PREFLIGHT-W2" +pid_hex = "0x5203" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_NUMPAD; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/5203/mode_or_profile_read.toml b/spec/dossiers/5203/mode_or_profile_read.toml new file mode 100644 index 0000000..ed26590 --- /dev/null +++ b/spec/dossiers/5203/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5203-MODEPROFILE-W2" +pid_hex = "0x5203" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_NUMPAD; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/5204/core_diag.toml b/spec/dossiers/5204/core_diag.toml new file mode 100644 index 0000000..f2d9526 --- /dev/null +++ b/spec/dossiers/5204/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5204-CORE-DIAG-W2" +pid_hex = "0x5204" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_NUMPADRR; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/5204/firmware_preflight.toml b/spec/dossiers/5204/firmware_preflight.toml new file mode 100644 index 0000000..20e7b18 --- /dev/null +++ b/spec/dossiers/5204/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5204-FW-PREFLIGHT-W2" +pid_hex = "0x5204" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_NUMPADRR; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/5204/mode_or_profile_read.toml b/spec/dossiers/5204/mode_or_profile_read.toml new file mode 100644 index 0000000..23ad0de --- /dev/null +++ b/spec/dossiers/5204/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5204-MODEPROFILE-W2" +pid_hex = "0x5204" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_NUMPADRR; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/5205/core_diag.toml b/spec/dossiers/5205/core_diag.toml new file mode 100644 index 0000000..cc6b1cf --- /dev/null +++ b/spec/dossiers/5205/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5205-CORE" +pid_hex = "0x5205" +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" diff --git a/spec/dossiers/5206/core_diag.toml b/spec/dossiers/5206/core_diag.toml new file mode 100644 index 0000000..253d910 --- /dev/null +++ b/spec/dossiers/5206/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5206-CORE" +pid_hex = "0x5206" +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" diff --git a/spec/dossiers/5209/firmware_core.toml b/spec/dossiers/5209/firmware_core.toml new file mode 100644 index 0000000..cf88582 --- /dev/null +++ b/spec/dossiers/5209/firmware_core.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5209-Firmware" +pid_hex = "0x5209" +operation_group = "Firmware" +command_id = ["Jp108EnterBootloader", "Jp108FirmwareChunk", "Jp108FirmwareCommit", "Jp108ExitBootloader"] +request_shape = "boot transition frames followed by chunked firmware transfer frames" +response_shape = "ack/status and completion indicators" +validator_rules = ["boot transition acknowledged", "chunk ack observed", "commit response accepted"] +retry_behavior = "firmware retries are bounded and guarded by unsafe flags" +failure_signatures = ["boot transition failure", "chunk ack mismatch", "commit timeout"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-JP108-002", "REQ-DR-001", "REQ-DR-002"] +class_family = "JP firmware update dispatch" +notes = "Unsafe flow remains runtime-gated with dual acknowledgement" diff --git a/spec/dossiers/5209/jp108_dedicated.toml b/spec/dossiers/5209/jp108_dedicated.toml new file mode 100644 index 0000000..0d9cf83 --- /dev/null +++ b/spec/dossiers/5209/jp108_dedicated.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-5209-JP108-Dedicated" +pid_hex = "0x5209" +operation_group = "JP108Dedicated" +command_id = ["Jp108ReadDedicatedMappings", "Jp108WriteDedicatedMapping", "Jp108ReadFeatureFlags", "Jp108WriteFeatureFlags", "Jp108ReadVoice", "Jp108WriteVoice"] +request_shape = "64-byte JP108 command frames with dedicated index/value payloads" +response_shape = "status header plus dedicated mapping/flag payload bytes" +validator_rules = ["byte0 == 0x02", "byte1 indicates command family", "payload length meets command minimum"] +retry_behavior = "bounded retries for read/ack operations" +failure_signatures = ["timeout", "malformed response", "unsupported for pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-JP108-001", "REQ-DR-001", "REQ-DR-002"] +class_family = "JP108Advance / JP108AdvanceUI dispatch" +notes = "Dedicated mapping scope remains A/B/K1-K8 in current product milestone" diff --git a/spec/dossiers/6002/core_diag.toml b/spec/dossiers/6002/core_diag.toml new file mode 100644 index 0000000..cbb7325 --- /dev/null +++ b/spec/dossiers/6002/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-6002-CORE" +pid_hex = "0x6002" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle", "GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report with command byte and optional slot/mode payload" +response_shape = "status header with mode/profile payload bytes" +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 = "Standard64 dispatch" +notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet" diff --git a/spec/dossiers/6002/firmware.toml b/spec/dossiers/6002/firmware.toml new file mode 100644 index 0000000..001a0d3 --- /dev/null +++ b/spec/dossiers/6002/firmware.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-6002-FIRMWARE" +pid_hex = "0x6002" +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" diff --git a/spec/dossiers/6003/core_diag.toml b/spec/dossiers/6003/core_diag.toml new file mode 100644 index 0000000..84e2042 --- /dev/null +++ b/spec/dossiers/6003/core_diag.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-6003-CORE" +pid_hex = "0x6003" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle", "GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report with command byte and optional slot/mode payload" +response_shape = "status header with mode/profile payload bytes" +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 = "Standard64 dispatch" +notes = "Wave 1 candidate-readonly dossier; no runtime trace or hardware write confirmation yet" diff --git a/spec/dossiers/6003/firmware.toml b/spec/dossiers/6003/firmware.toml new file mode 100644 index 0000000..5c7b3e6 --- /dev/null +++ b/spec/dossiers/6003/firmware.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-6003-FIRMWARE" +pid_hex = "0x6003" +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" diff --git a/spec/dossiers/6006/core_diag.toml b/spec/dossiers/6006/core_diag.toml new file mode 100644 index 0000000..60536f1 --- /dev/null +++ b/spec/dossiers/6006/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-6006-CORE-DIAG-W2" +pid_hex = "0x6006" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_Pro2_CY; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/6006/firmware_preflight.toml b/spec/dossiers/6006/firmware_preflight.toml new file mode 100644 index 0000000..57c956c --- /dev/null +++ b/spec/dossiers/6006/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-6006-FW-PREFLIGHT-W2" +pid_hex = "0x6006" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_Pro2_CY; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/6006/mode_or_profile_read.toml b/spec/dossiers/6006/mode_or_profile_read.toml new file mode 100644 index 0000000..3825c7e --- /dev/null +++ b/spec/dossiers/6006/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-6006-MODEPROFILE-W2" +pid_hex = "0x6006" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_Pro2_CY; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/6012/firmware_core.toml b/spec/dossiers/6012/firmware_core.toml new file mode 100644 index 0000000..b9aba53 --- /dev/null +++ b/spec/dossiers/6012/firmware_core.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-6012-Firmware" +pid_hex = "0x6012" +operation_group = "Firmware" +command_id = ["U2EnterBootloader", "U2FirmwareChunk", "U2FirmwareCommit", "U2ExitBootloader"] +request_shape = "boot transition frames followed by chunked firmware transfer frames" +response_shape = "ack/status and completion indicators" +validator_rules = ["boot transition acknowledged", "chunk ack observed", "commit response accepted"] +retry_behavior = "firmware retries are bounded and guarded by unsafe flags" +failure_signatures = ["boot transition failure", "chunk ack mismatch", "commit timeout"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-U2-002", "REQ-DR-001", "REQ-DR-002"] +class_family = "Ultimate2 firmware update dispatch" +notes = "Unsafe flow remains runtime-gated with dual acknowledgement" diff --git a/spec/dossiers/6012/u2_core.toml b/spec/dossiers/6012/u2_core.toml new file mode 100644 index 0000000..d784a14 --- /dev/null +++ b/spec/dossiers/6012/u2_core.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-6012-U2-Core" +pid_hex = "0x6012" +operation_group = "Ultimate2Core" +command_id = ["U2GetCurrentSlot", "U2ReadConfigSlot", "U2WriteConfigSlot", "U2ReadButtonMap", "U2WriteButtonMap", "U2SetMode"] +request_shape = "64-byte Ultimate2 command frames with slot/mode/map payload sections" +response_shape = "status header plus slot/config/map payload bytes" +validator_rules = ["byte0 == 0x02", "payload length meets command minimum", "slot readback value is bounded"] +retry_behavior = "bounded retries for slot/map reads and write acknowledgements" +failure_signatures = ["timeout", "malformed response", "unsupported for pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-U2-001", "REQ-DR-001", "REQ-DR-002"] +class_family = "Ultimate2_4Advance2 / Ultimate2_4Advance2UI dispatch" +notes = "Advanced theme/sixaxis/deep macro controls remain intentionally hidden" diff --git a/spec/dossiers/901a/core_diag.toml b/spec/dossiers/901a/core_diag.toml new file mode 100644 index 0000000..d0df339 --- /dev/null +++ b/spec/dossiers/901a/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-901A-CORE-DIAG-W2" +pid_hex = "0x901a" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_ArcadeStick; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/901a/firmware_preflight.toml b/spec/dossiers/901a/firmware_preflight.toml new file mode 100644 index 0000000..07c9e8f --- /dev/null +++ b/spec/dossiers/901a/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-901A-FW-PREFLIGHT-W2" +pid_hex = "0x901a" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_ArcadeStick; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/901a/mode_or_profile_read.toml b/spec/dossiers/901a/mode_or_profile_read.toml new file mode 100644 index 0000000..17a4ce7 --- /dev/null +++ b/spec/dossiers/901a/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-901A-MODEPROFILE-W2" +pid_hex = "0x901a" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_ArcadeStick; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/9028/core_diag.toml b/spec/dossiers/9028/core_diag.toml new file mode 100644 index 0000000..eaeb847 --- /dev/null +++ b/spec/dossiers/9028/core_diag.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-9028-CORE-DIAG-W2" +pid_hex = "0x9028" +operation_group = "CoreDiag" +command_id = ["GetPid", "GetReportRevision", "GetControllerVersion", "Version", "Idle"] +request_shape = "64-byte HID report; command opcode in request[1], pid-specific capability gating outside payload" +response_shape = "status header (byte0/byte1) + optional payload bytes for pid/version/revision" +validator_rules = ["byte0 == 0x02", "response_len >= 4", "signature bytes match command expectation"] +retry_behavior = "retry timeout or malformed responses up to configured max attempts with bounded backoff" +failure_signatures = ["timeout", "malformed_frame", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PID-002"] +class_family = "Standard64" +notes = "Wave 2 static dossier for PID_N64RR; support tier remains candidate-readonly" + +[state_machine] +pre_state = "DeviceConnected" +action = "Run core diagnostics read sequence" +post_state = "DeviceIdentified" +invalid_transitions = ["NoDevice", "TransportClosed", "BootloaderOnly"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime request/response traces for core diagnostics", "command-level status validation examples"] + +[hardware_placeholder] +required = true +evidence_needed = ["identify + diag run on physical device", "repeatable read success across reconnects"] diff --git a/spec/dossiers/9028/firmware_preflight.toml b/spec/dossiers/9028/firmware_preflight.toml new file mode 100644 index 0000000..09d6b89 --- /dev/null +++ b/spec/dossiers/9028/firmware_preflight.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-9028-FW-PREFLIGHT-W2" +pid_hex = "0x9028" +operation_group = "FirmwarePreflight" +command_id = ["GetReportRevision", "GetControllerVersion", "Version"] +request_shape = "64-byte HID read commands used for firmware readiness and metadata preflight" +response_shape = "status bytes + firmware/version metadata bytes when present" +validator_rules = ["byte0 == 0x02", "metadata bytes pass bounds checks", "device remains in application mode"] +retry_behavior = "safe-read retries only; firmware transfer/write commands stay blocked for candidate-readonly" +failure_signatures = ["timeout", "metadata_mismatch", "unsupported_for_pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-001", "REQ-PROM-W2-002"] +class_family = "Standard64" +notes = "Preflight-only dossier for PID_N64RR; no bootloader entry/chunk/commit enablement" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read firmware preflight metadata" +post_state = "FirmwarePreflightReady" +invalid_transitions = ["NoDevice", "BootloaderActive", "TransferInProgress"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime preflight metadata traces", "error signature samples for invalid firmware path"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware preflight check success", "metadata stability across reconnects"] diff --git a/spec/dossiers/9028/mode_or_profile_read.toml b/spec/dossiers/9028/mode_or_profile_read.toml new file mode 100644 index 0000000..6b87d4c --- /dev/null +++ b/spec/dossiers/9028/mode_or_profile_read.toml @@ -0,0 +1,30 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-9028-MODEPROFILE-W2" +pid_hex = "0x9028" +operation_group = "ModeProfileRead" +command_id = ["GetMode", "GetModeAlt", "ReadProfile"] +request_shape = "64-byte HID report; mode/profile selector byte in command payload" +response_shape = "status header + mode/profile bytes when available" +validator_rules = ["byte0 == 0x02", "mode/profile payload length is bounded", "fallback command path is defined"] +retry_behavior = "use standard safe-read retry policy; fallback between mode-read command variants when defined" +failure_signatures = ["timeout", "unsupported_for_pid", "invalid_signature"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-DR-001", "REQ-DR-W2-001", "REQ-PROM-001", "REQ-PROM-W2-002", "REQ-PID-002"] +class_family = "Standard64" +notes = "Read-only mode/profile snapshot for PID_N64RR; no write enablement in this wave" + +[state_machine] +pre_state = "DeviceIdentified" +action = "Read mode/profile snapshot" +post_state = "ModeProfileSnapshotReady" +invalid_transitions = ["NoDevice", "WritePending", "BootloaderActive"] + +[runtime_placeholder] +required = true +evidence_needed = ["runtime traces for mode/profile read commands", "read consistency across repeated probes"] + +[hardware_placeholder] +required = true +evidence_needed = ["hardware readback for mode/profile snapshot", "cross-slot/read variant sanity checks where applicable"] diff --git a/spec/dossiers/global/core.toml b/spec/dossiers/global/core.toml new file mode 100644 index 0000000..41cee98 --- /dev/null +++ b/spec/dossiers/global/core.toml @@ -0,0 +1,16 @@ +# Sanitized static dirty-room dossier + +dossier_id = "DOS-CORE-GLOBAL" +pid_hex = "*" +operation_group = "Core" +command_id = ["GetPid", "GetReportRevision", "GetMode", "GetModeAlt", "GetControllerVersion", "Version", "Idle", "ReadProfile", "WriteProfile", "SetModeDInput"] +request_shape = "64-byte HID commands with command selector and optional payload bytes" +response_shape = "status header plus command-dependent payload" +validator_rules = ["byte0 == 0x02", "response length >= 4", "command-specific signature checks"] +retry_behavior = "deterministic retries on timeout and malformed responses" +failure_signatures = ["timeout", "malformed response", "invalid response signature", "unsupported for pid"] +evidence_source = "static" +confidence = "inferred" +requirement_ids = ["REQ-PROT-001", "REQ-PROT-002", "REQ-DR-001", "REQ-DR-002"] +class_family = "Core transport and command dispatch" +notes = "Global core command dossier linked from command matrix rows" diff --git a/spec/evidence_index.csv b/spec/evidence_index.csv new file mode 100644 index 0000000..98d0506 --- /dev/null +++ b/spec/evidence_index.csv @@ -0,0 +1,27 @@ +pid_hex,pid_name,class_family,operation_groups,evidence_source,confidence,notes +0x2028,PID_XBOXJP,JP handshake dispatch,CoreDiag;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x202e,PID_XBOXJPUSB,JP handshake dispatch,CoreDiag;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x203a,PID_68JP,JP handshake dispatch,CoreDiag;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x2049,PID_68JPUSB,JP handshake dispatch,CoreDiag;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x2100,PID_Xcloud,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x2101,PID_Xcloud2,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x3004,PID_N64,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x3010,PID_Pro2_Wired,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x3011,PID_Ultimate_PC,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x3012,PID_Ultimate2_4,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x3013,PID_Ultimate2_4RR,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x3019,PID_N64BT,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x301a,PID_UltimateBT2C,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x3026,PID_XBOXUK,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x3027,PID_XBOXUKUSB,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x3100,PID_USB_Ultimate,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x3105,PID_USB_Ultimate2,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x5200,PID_JP,JP handshake dispatch,CoreDiag;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x5201,PID_JPUSB,JP handshake dispatch,CoreDiag;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x5203,PID_NUMPAD,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x5204,PID_NUMPADRR,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x6002,PID_SN30Plus,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x6003,PID_Pro2,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x6006,PID_Pro2_CY,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x901a,PID_ArcadeStick,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) +0x9028,PID_N64RR,Standard64 dispatch,CoreDiag;ModeProfileRead;FirmwarePreflight,static,inferred,candidate-readonly wave dossier (static-only) diff --git a/spec/pid_matrix.csv b/spec/pid_matrix.csv index 27c0530..0ab08fb 100644 --- a/spec/pid_matrix.csv +++ b/spec/pid_matrix.csv @@ -1,60 +1,58 @@ -pid_name,pid_decimal,pid_hex,vid_decimal,vid_hex,support_level,protocol_family,notes -PID_None,0,0x0,11720,0x2dc8,detect-only,Unknown,Sentinel value -PID_IDLE,12553,0x3109,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_SN30Plus,24578,0x6002,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_USB_Ultimate,12544,0x3100,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_USB_Ultimate2,12549,0x3105,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_USB_UltimateClasses,12548,0x3104,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Xcloud,8448,0x2100,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Xcloud2,8449,0x2101,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_ArcadeStick,36890,0x901a,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Pro2,24579,0x6003,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Pro2_CY,24582,0x6006,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Pro2_OLD,24579,0x6003,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Pro2_Wired,12304,0x3010,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Ultimate_PC,12305,0x3011,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Ultimate2_4,12306,0x3012,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Ultimate2_4RR,12307,0x3013,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_UltimateBT,24583,0x6007,11720,0x2dc8,full,Standard64,Baseline from sanitized dirty-room analysis -PID_UltimateBTRR,12550,0x3106,11720,0x2dc8,full,Standard64,Baseline from sanitized dirty-room analysis -PID_JP,20992,0x5200,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis -PID_JPUSB,20993,0x5201,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis -PID_NUMPAD,20995,0x5203,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_NUMPADRR,20996,0x5204,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_QINGCHUN2,12554,0x310a,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis -PID_QINGCHUN2RR,12316,0x301c,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis -PID_Xinput,12555,0x310b,11720,0x2dc8,detect-only,DInput,Baseline from sanitized dirty-room analysis -PID_Pro3,24585,0x6009,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis -PID_Pro3USB,24586,0x600a,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis -PID_Pro3DOCK,24589,0x600d,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_108JP,21001,0x5209,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis -PID_108JPUSB,21002,0x520a,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis -PID_XBOXJP,8232,0x2028,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis -PID_XBOXJPUSB,8238,0x202e,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis -PID_NGCDIY,22352,0x5750,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_NGCRR,36906,0x902a,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Ultimate2,24594,0x6012,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis -PID_Ultimate2RR,24595,0x6013,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis -PID_UltimateBT2,24591,0x600f,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis -PID_UltimateBT2RR,24593,0x6011,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis -PID_Mouse,20997,0x5205,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_MouseRR,20998,0x5206,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_SaturnRR,36907,0x902b,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_UltimateBT2C,12314,0x301a,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Lashen,12318,0x301e,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_HitBox,24587,0x600b,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis -PID_HitBoxRR,24588,0x600c,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis -PID_N64BT,12313,0x3019,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_N64,12292,0x3004,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_N64RR,36904,0x9028,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_XBOXUK,12326,0x3026,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_XBOXUKUSB,12327,0x3027,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_LashenX,8203,0x200b,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_68JP,8250,0x203a,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis -PID_68JPUSB,8265,0x2049,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis -PID_N64JoySticks,12321,0x3021,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_DoubleSuper,8254,0x203e,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Cube2RR,8278,0x2056,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_Cube2,8249,0x2039,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis -PID_ASLGJP,8282,0x205a,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis -PID_ASLGMouse,20997,0x5205,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis +pid_name,pid_decimal,pid_hex,vid_decimal,vid_hex,support_level,protocol_family,notes,support_tier +PID_None,0,0x0,11720,0x2dc8,detect-only,Unknown,Sentinel value,detect-only +PID_IDLE,12553,0x3109,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_SN30Plus,24578,0x6002,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_USB_Ultimate,12544,0x3100,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_USB_Ultimate2,12549,0x3105,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_USB_UltimateClasses,12548,0x3104,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_Xcloud,8448,0x2100,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_Xcloud2,8449,0x2101,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_ArcadeStick,36890,0x901a,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_Pro2,24579,0x6003,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_Pro2_CY,24582,0x6006,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_Pro2_Wired,12304,0x3010,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_Ultimate_PC,12305,0x3011,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_Ultimate2_4,12306,0x3012,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_Ultimate2_4RR,12307,0x3013,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_UltimateBT,24583,0x6007,11720,0x2dc8,full,Standard64,Baseline from sanitized dirty-room analysis,full +PID_UltimateBTRR,12550,0x3106,11720,0x2dc8,full,Standard64,Baseline from sanitized dirty-room analysis,full +PID_JP,20992,0x5200,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_JPUSB,20993,0x5201,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_NUMPAD,20995,0x5203,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_NUMPADRR,20996,0x5204,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_QINGCHUN2,12554,0x310a,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis,full +PID_QINGCHUN2RR,12316,0x301c,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis,full +PID_Xinput,12555,0x310b,11720,0x2dc8,detect-only,DInput,Baseline from sanitized dirty-room analysis,detect-only +PID_Pro3,24585,0x6009,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis,full +PID_Pro3USB,24586,0x600a,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis,full +PID_Pro3DOCK,24589,0x600d,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_108JP,21001,0x5209,11720,0x2dc8,full,JpHandshake,Promoted to full after sanitized JP108 dedicated mapping + firmware evidence,full +PID_108JPUSB,21002,0x520a,11720,0x2dc8,full,JpHandshake,Promoted to full after sanitized JP108 dedicated mapping + firmware evidence,full +PID_XBOXJP,8232,0x2028,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_XBOXJPUSB,8238,0x202e,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_NGCDIY,22352,0x5750,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_NGCRR,36906,0x902a,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_Ultimate2,24594,0x6012,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis,full +PID_Ultimate2RR,24595,0x6013,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis,full +PID_UltimateBT2,24591,0x600f,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis,full +PID_UltimateBT2RR,24593,0x6011,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis,full +PID_Mouse,20997,0x5205,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_MouseRR,20998,0x5206,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_SaturnRR,36907,0x902b,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_UltimateBT2C,12314,0x301a,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_Lashen,12318,0x301e,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_HitBox,24587,0x600b,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis,full +PID_HitBoxRR,24588,0x600c,11720,0x2dc8,full,DInput,Baseline from sanitized dirty-room analysis,full +PID_N64BT,12313,0x3019,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_N64,12292,0x3004,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_N64RR,36904,0x9028,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_XBOXUK,12326,0x3026,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_XBOXUKUSB,12327,0x3027,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis Wave 2 candidate-readonly expansion (static dossiers complete; runtime/hardware pending).,candidate-readonly +PID_LashenX,8203,0x200b,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_68JP,8250,0x203a,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_68JPUSB,8265,0x2049,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis,candidate-readonly +PID_N64JoySticks,12321,0x3021,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_DoubleSuper,8254,0x203e,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_Cube2RR,8278,0x2056,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_Cube2,8249,0x2039,11720,0x2dc8,detect-only,Standard64,Baseline from sanitized dirty-room analysis,detect-only +PID_ASLGJP,8282,0x205a,11720,0x2dc8,detect-only,JpHandshake,Baseline from sanitized dirty-room analysis,detect-only diff --git a/spec/protocol_spec.md b/spec/protocol_spec.md index e03384c..47fc74b 100644 --- a/spec/protocol_spec.md +++ b/spec/protocol_spec.md @@ -28,10 +28,66 @@ It is intentionally independent from reverse-engineered source code details and - Validation outcomes: `Ok`, `Invalid`, `Malformed` - Retry policy applies on `Malformed` or timeout responses +## Operation Groups +- `Core`: generic identify/mode/profile/boot/fallback commands +- `JP108Dedicated`: 108-key dedicated-button mapping + feature/voice operations +- `Ultimate2Core`: Ultimate2 mode/slot/core-map operations +- `Firmware`: device-scoped firmware enter/chunk/commit/exit operations +- `CoreDiag`: decompiler-first detect/diagnostic command subset for candidate-readonly PIDs +- `ModeProfileRead`: decompiler-first read-only mode/profile snapshot group for candidate-readonly PIDs +- `FirmwarePreflight`: decompiler-first firmware readiness metadata reads (no transfer enablement) + +## JP108 Dedicated Support +- Supported targets: `0x5209` (`PID_108JP`), `0x520a` (`PID_108JPUSB`) +- First milestone mapping scope: `A`, `B`, `K1`-`K8` +- Additional controls in this group: + - feature flags read/write + - voice setting read/write +- Full 111-key matrix remap is explicitly out of scope for this milestone. + +## Ultimate2 Core Support +- Supported targets: `0x6012` (`PID_Ultimate2`), `0x6013` (`PID_Ultimate2RR`) +- First milestone editable scope: + - current mode read/set + - current slot read + - slot config read/write + - core button map read/write +- Advanced subsystems (theme/sixaxis/deep macro editing) are intentionally hidden in this milestone. + +## PID-Aware Command Gating +- Command availability is gated by: + 1. safety class and runtime unsafe acknowledgements + 2. capability flags + 3. explicit PID allowlist from `command_matrix.csv:applies_to` +- `applies_to="*"` means globally available within existing safety/capability constraints. + ## Device Support Levels - `full`: command execution permitted for safe and unsafe operations (with user gates) - `detect-only`: identification allowed; unsupported operations return `UnsupportedForPid` +## Support Tiers +- `full`: read/write/unsafe operations available according to existing safety gates. +- `candidate-readonly`: detect/diag safe reads are allowed per PID allowlist; safe writes and unsafe flows are blocked. +- `detect-only`: identify-only posture for unsupported or unknown PIDs. + +## Candidate Read-Only Wave Policy +- Wave-1 and Wave-2 expansion PIDs are classified as `candidate-readonly`. +- Command policy for this tier: + - allow: detect/diag safe-read subset. + - allow: read-only mode/profile snapshot reads when family-appropriate. + - allow: firmware metadata/preflight reads only. + - deny: all safe-write operations. + - deny: all unsafe boot/firmware operations. +- Promotion from `candidate-readonly` to `full` requires 3-signal evidence: + 1. static dossier coverage + 2. runtime trace evidence + 3. hardware read/write/readback confirmation + +## Dossier Linkage +- Per-PID operation evidence is tracked in `spec/dossiers/**`. +- `command_matrix.csv:dossier_id` links command rows to sanitized dossier artifacts. +- `evidence_index.csv` maps PID to class-family anchors and operation groups. + ## Required Runtime Gating Unsafe commands execute only when both conditions are true: 1. `--unsafe` diff --git a/spec/requirements.yaml b/spec/requirements.yaml index af6d605..90acb9b 100644 --- a/spec/requirements.yaml +++ b/spec/requirements.yaml @@ -43,10 +43,86 @@ requirements: description: SDK shall parse and serialize profile blobs compatible with golden binary fixture. acceptance: Profile serialization test round-trips fixture payload. - id: REQ-TEST-002 - title: CLI structured output - description: CLI shall provide JSON output for automation. - acceptance: CLI tests assert stable JSON keys and command responses. + title: Beginner-first report output + description: User-facing reports shall be TOML-only and hidden on happy path. + acceptance: Report tests assert TOML persistence for diagnostics/failure flows and no JSON output path. - id: REQ-TEST-003 title: Clean-room guard description: CI shall fail if cleanroom/sdk references forbidden dirty-room locations or tokens. acceptance: cleanroom guard script is executed in CI and by integration test. + - id: REQ-JP108-001 + title: JP108 dedicated mapping read/write + description: SDK shall support reading and writing JP108 dedicated button mappings for A/B/K1-K8. + acceptance: Unit and hardware tests verify read/write/readback for A, B, and at least one K button. + - id: REQ-JP108-002 + title: JP108 firmware path + description: SDK shall support JP108 firmware preflight and transfer with existing unsafe gates. + acceptance: Hardware smoke validates JP108 firmware preflight and guarded transfer path. + - id: REQ-U2-001 + title: Ultimate2 core profile support + description: SDK shall support Ultimate2 mode, slot, and core button-map operations. + acceptance: Unit and hardware tests verify mode read/set/readback and slot/profile read/write/readback. + - id: REQ-U2-002 + title: Ultimate2 firmware path + description: SDK shall support Ultimate2 firmware preflight and transfer with existing unsafe gates. + acceptance: Hardware smoke validates Ultimate2 firmware preflight and guarded transfer path. + - id: REQ-WIZ-001 + title: Beginner guided configuration flows + description: TUI wizard shall provide dedicated JP108 and Ultimate2 configuration paths with mouse support. + acceptance: TUI tests verify end-to-end wizard transitions for mapping/profile apply and guided button test. + - id: REQ-WIZ-002 + title: Backup and rollback safety + description: Mapping/profile writes shall create rollback snapshots and auto-rollback on failure. + acceptance: App-core tests verify backup creation, rollback on failure, and deterministic retry guidance. + - id: REQ-HW-001 + title: Required hardware gates for target lines + description: CI shall enforce required blocking hardware jobs for Ultimate2 and JP108 targets. + acceptance: workflow includes required jobs that fail deterministically on missing fixture or command failure. + - id: REQ-DR-001 + title: Dossier completeness + description: Every wave target PID shall have sanitized dossier files for each scoped operation group. + acceptance: `spec/dossiers//*.toml` exists and includes all required schema fields. + - id: REQ-DR-002 + title: Dossier traceability + description: Dossiers and command matrix rows shall be linked by stable dossier IDs. + acceptance: command matrix `dossier_id` references an existing dossier artifact. + - id: REQ-DR-W2-001 + title: Wave 2 dossier minimum per PID + description: Each Wave 2 target PID shall include `core_diag`, `mode_or_profile_read`, and `firmware_preflight` dossiers. + acceptance: lint check confirms all three required dossier files exist for every Wave 2 PID. + - id: REQ-DR-W2-002 + title: Wave 2 dossier state model and placeholders + description: Every Wave 2 dossier shall include state machine, runtime placeholder, and hardware placeholder sections. + acceptance: dossier lint rejects files missing `state_machine`, `runtime_placeholder`, or `hardware_placeholder`. + - id: REQ-PROM-001 + title: Strict promotion gate + description: Promotion to full support requires static evidence plus runtime traces and hardware read/write/readback. + acceptance: no PID is promoted without all three evidence signals marked complete. + - id: REQ-PROM-002 + title: Candidate read-only enforcement + description: candidate-readonly devices shall permit detect/diag reads but reject safe writes and unsafe operations. + acceptance: command gating tests reject write/firmware commands for candidate-readonly PIDs. + - id: REQ-PROM-W2-001 + title: Wave 2 promotion block + description: Wave 2 target devices shall remain candidate-readonly until runtime and hardware placeholders are satisfied. + acceptance: Wave 2 command matrix rows set `promotion_gate=blocked/no_runtime` and evidence runtime/hardware set to `no`. + - id: REQ-PROM-W2-002 + title: Firmware preflight only for Wave 2 + description: Wave 2 firmware coverage shall remain metadata/preflight only with no transfer/write enablement. + acceptance: firmware transfer commands remain blocked for Wave 2 candidate-readonly PIDs. + - id: REQ-COMM-001 + title: Structured community intake + description: Community evidence submissions shall follow a structured sanitized template. + acceptance: issue templates collect VID/PID, operation group, sanitized request/response structure, and repro notes. + - id: REQ-COMM-002 + title: Clean-room intake boundary + description: Community submissions must forbid raw vendor/decompiled snippet carryover. + acceptance: template and docs explicitly prohibit raw snippet submission. + - id: REQ-GH-001 + title: Release-governance variables + description: Repository variables for AUR/Homebrew publication gates shall be configured through GitHub settings. + acceptance: variables `AUR_PUBLISH_ENABLED`, `HOMEBREW_PUBLISH_ENABLED`, and `HOMEBREW_TAP_REPO` are present. + - id: REQ-GH-002 + title: Hardware-constrained required checks + description: Branch protection required checks shall reflect currently available hardware fixtures. + acceptance: required checks include `hardware-108jp` and `hardware-ultimate2`, excluding unavailable hardware families.