diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..04e10b0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Unified diffs contain significant one-character context prefixes. +*.patch -whitespace diff --git a/.github/workflows/full-verification.yml b/.github/workflows/full-verification.yml index 21c2497..2e58b21 100644 --- a/.github/workflows/full-verification.yml +++ b/.github/workflows/full-verification.yml @@ -24,16 +24,21 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 persist-credentials: false - - name: Restore verified software caches + - name: Restore exact upstream sources uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | .hfx/upstreams + key: hfx-upstreams-${{ runner.os }}-${{ hashFiles('integrations/catalog.json') }} + - name: Restore verified build caches + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | .hfx/cargo target - key: hfx-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'toolchains/development-environment.json', 'integrations/catalog.json') }} + key: hfx-build-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'toolchains/development-environment.json') }} restore-keys: | - hfx-${{ runner.os }}- + hfx-build-${{ runner.os }}- - name: Set up Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Build the pinned development environment diff --git a/.github/workflows/verification.yml b/.github/workflows/verification.yml index f04cac9..d7cbaf5 100644 --- a/.github/workflows/verification.yml +++ b/.github/workflows/verification.yml @@ -25,16 +25,21 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 persist-credentials: false - - name: Restore verified software caches + - name: Restore exact upstream sources uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | .hfx/upstreams + key: hfx-upstreams-${{ runner.os }}-${{ hashFiles('integrations/catalog.json') }} + - name: Restore verified build caches + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | .hfx/cargo target - key: hfx-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'toolchains/development-environment.json', 'integrations/catalog.json') }} + key: hfx-build-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'toolchains/development-environment.json') }} restore-keys: | - hfx-${{ runner.os }}- + hfx-build-${{ runner.os }}- - name: Set up Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Build the pinned development environment diff --git a/Cargo.lock b/Cargo.lock index 8ddecc4..3a01cff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,7 @@ dependencies = [ "rustix", "serde", "serde_json", + "sha2", ] [[package]] @@ -159,6 +160,7 @@ version = "0.0.0-dev.1" dependencies = [ "hfx-domain", "hfx-errors", + "hfx-integration-model", "hfx-profiles", "hfx-protocol", "hfx-runtime", @@ -217,6 +219,24 @@ dependencies = [ "sha2", ] +[[package]] +name = "hfx-uhid-relay" +version = "0.0.0-dev.1" +dependencies = [ + "hfx-daemon", + "hfx-domain", + "hfx-kernel-transport", + "hfx-profiles", + "hfx-protocol", + "hfx-runtime", + "hfx-sdk", + "rustix", + "serde", + "serde_json", + "sha2", + "signal-hook", +] + [[package]] name = "hybrid-array" version = "0.4.10" diff --git a/Cargo.toml b/Cargo.toml index de4345c..2a92e1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/hfx-bridge", "crates/hfx-core", "crates/hfx-daemon", "crates/hfx-domain", "crates/hfx-errors", "crates/hfx-integration-model", "crates/hfx-kernel-transport", "crates/hfx-ops", "crates/hfx-profiles", "crates/hfx-protocol", "crates/hfx-runtime", "crates/hfx-sdk", "crates/hfx-sim"] +members = ["crates/hfx-bridge", "crates/hfx-core", "crates/hfx-daemon", "crates/hfx-domain", "crates/hfx-errors", "crates/hfx-integration-model", "crates/hfx-kernel-transport", "crates/hfx-ops", "crates/hfx-profiles", "crates/hfx-protocol", "crates/hfx-runtime", "crates/hfx-sdk", "crates/hfx-sim", "crates/hfx-uhid-relay"] resolver = "3" [workspace.package] @@ -22,3 +22,17 @@ unsafe_code = "deny" [workspace.lints.clippy] all = "deny" pedantic = "deny" + +[profile.operations] +inherits = "release" +codegen-units = 1 +lto = "thin" +panic = "abort" +strip = "symbols" + +[profile.relay] +inherits = "release" +codegen-units = 1 +lto = "thin" +panic = "abort" +strip = "symbols" diff --git a/README.md b/README.md index b34dfc3..376cba7 100644 --- a/README.md +++ b/README.md @@ -27,16 +27,41 @@ ```mermaid flowchart LR - Applications["Applications"] --> SDK["Versioned SDK"] - SDK --> Bridge["Bridge and policy authority"] - Bridge --> Kernel["Minimal HID transport"] + Applications["OpenRazer and OpenRGB"] --> SDK["Versioned native-route SDK"] + SDK --> Bridge["Route and exchange authority"] + Bridge --> Kernel["Bounded receiver transport"] Kernel --> Receiver["HyperFlux receiver and paired devices"] ``` -Applications own user interaction, layouts, and effects. The SDK owns the typed -application boundary. The bridge is the sole userspace writer and owns policy, -qualification, scheduling, restoration, and outcomes. The kernel preserves -ordinary HID input and transports bounded generation-bound envelopes. +Applications own model commands, settings, layouts, effects, profiles, and +persistence. The SDK carries qualified route discovery and exact 90-byte Razer +Feature exchanges. The bridge is the sole userspace receiver writer and owns +route qualification, generation safety, NativeControl arbitration, bounded +dispatch, availability, outcome certainty, and diagnostics. The kernel +preserves ordinary HID input and transports generation-bound exchanges without +interpreting device semantics. + +## OpenRazer Route + +HyperFlux applies a narrow transport patch to official +[OpenRazer 3.12.4](https://github.com/openrazer/openrazer/commit/1ef8a91712906a89fd332a2ed62bbd105315d6d1). +The upstream daemon, Python client, device classes, ordinary USB tables, and +direct-device behavior remain OpenRazer-owned. The patched DKMS modules accept +only trusted, feature-only HyperFlux UHID routes and advertise +`hyperflux_native_transport=razer-feature-report-v1`. + +| Qualified receiver-backed controller | Native route | Direct USB precedence | +| --- | --- | --- | +| Razer Basilisk V3 Pro 35K (Wireless) | `1532:00cd` | `1532:00cc` | +| Razer DeathStalker V2 Pro TKL (Wireless) | `1532:0296` | `1532:0298` | + +Distribution release suffixes do not affect compatibility. A newer OpenRazer +source line requires a reviewed rebase and fresh verification; until then, +`hyperflux-next-openrazer-relay.service` remains stopped. See the +[deployment contract](docs/generated/openrazer-native-deployment.md), +[installation contract](docs/generated/installation.md), and +[hardware boundary](docs/generated/supported-hardware.md). Other catalog +devices remain research-only and non-writable. ## Current Readiness diff --git a/apps/device-qualification/assets/app.css b/apps/device-qualification/assets/app.css index 364b2d9..7bdadf1 100644 --- a/apps/device-qualification/assets/app.css +++ b/apps/device-qualification/assets/app.css @@ -699,7 +699,7 @@ code, text-align: center; } -@media (max-width: 1080px) { +@media (max-width: 1200px) { .qualification-layout { grid-template-columns: 1fr; } .inspector { position: static; } .identity-strip { grid-template-columns: 1fr 1fr; row-gap: 15px; } diff --git a/apps/device-qualification/assets/app.js b/apps/device-qualification/assets/app.js index f0995e0..6af1acc 100644 --- a/apps/device-qualification/assets/app.js +++ b/apps/device-qualification/assets/app.js @@ -9,7 +9,7 @@ const refreshButton = document.querySelector("#refresh-view"); const state = { view: null, - selectedDeviceId: null, + selectedDeviceKey: null, selectedStageId: null, busyActionId: null, loading: true, @@ -20,9 +20,9 @@ const state = { refreshButton.addEventListener("click", () => refreshView(true)); document.addEventListener("click", (event) => { - const deviceButton = event.target.closest("[data-device-id]"); + const deviceButton = event.target.closest("[data-device-key]"); if (deviceButton) { - state.selectedDeviceId = deviceButton.dataset.deviceId; + state.selectedDeviceKey = deviceButton.dataset.deviceKey; state.selectedStageId = null; state.hasDraft = false; render(); @@ -78,6 +78,8 @@ async function refreshView(visible) { reconcileSelection(); if (value.companion.state === "legacy-v2-detected") { renderConnection("warning", "HyperFlux V2 detected"); + } else if (value.companion.state === "lifecycle-transition") { + renderConnection("warning", "Lifecycle transition"); } else { renderConnection("ready", value.companion.state === "ready" ? "Connected" : "Needs attention"); } @@ -149,7 +151,7 @@ async function invokeStage(form) { async function fetchWithTimeout(path, options) { const controller = new AbortController(); - const timeout = window.setTimeout(() => controller.abort(), 6_000); + const timeout = window.setTimeout(() => controller.abort(), 15_000); try { return await fetch(path, { credentials: "same-origin", @@ -163,8 +165,8 @@ async function fetchWithTimeout(path, options) { function reconcileSelection() { const contexts = deviceContexts(); - if (!contexts.some(({ device }) => device.device_id === state.selectedDeviceId)) { - state.selectedDeviceId = contexts[0]?.device.device_id ?? null; + if (!contexts.some((context) => context.key === state.selectedDeviceKey)) { + state.selectedDeviceKey = contexts[0]?.key ?? null; state.selectedStageId = null; state.hasDraft = false; } @@ -182,7 +184,7 @@ function render() { return; } if (!state.view) return; - if (state.view.companion.state !== "ready") { + if (!["ready", "lifecycle-transition"].includes(state.view.companion.state)) { app.innerHTML = systemGate(state.view.companion.state); return; } @@ -208,10 +210,10 @@ function renderDeviceRail(contexts) {