diff --git a/build/check-bundle-size.ts b/build/check-bundle-size.ts index 03a3968f..cad65857 100644 --- a/build/check-bundle-size.ts +++ b/build/check-bundle-size.ts @@ -39,7 +39,10 @@ const BUDGET_BYTES: Record = { // replaced. 765 kB leaves ~15 kB of headroom over the measured aggregate. // Raised to 790 kB for the MCHOSE A7 V2 mouse and MagDock driver support: // the measured aggregate is 779.1 kB, leaving ~11 kB of headroom. - ".js": 790_000, + // Raised to 800 kB for the ATK ZERO driver (AtkCards.tsx, device/atk.ts) + // and the mouse-reported lift-off range plumbing: the measured aggregate is + // 790.6 kB, leaving ~9 kB of headroom. + ".js": 800_000, }; const ASSETS = join("dist", "assets"); diff --git a/src/app/cards/AdvancedCards.tsx b/src/app/cards/AdvancedCards.tsx index e201ada9..aac2245c 100644 --- a/src/app/cards/AdvancedCards.tsx +++ b/src/app/cards/AdvancedCards.tsx @@ -304,6 +304,24 @@ export function ProcessingCard({ snapshot }: { snapshot: ControlSnapshot }): Rea ) : null} + {status.sensorMode != null && !traits.teevolution ? ( +
+ Sensor sampling mode + +
+ ) : null} + + ) : status.liftOffScale ? ( + ) : (
{ + if (scale) setCode(scale.value); + }, [scale?.value]); + + if (!scale) return null; + + const span = Math.max(1, scale.max - scale.min); + const millimetres = scale.minMillimetres + + ((code - scale.min) * (scale.maxMillimetres - scale.minMillimetres)) / span; + + return ( +
+ +
+ ); +} + function BunnyHop({ snapshot }: { snapshot: ControlSnapshot }): ReactNode { const entry = snapshot.profile.entry; if (!snapshot.profile.bunnyHopSupported || !entry) return null; diff --git a/src/app/cards/availability.ts b/src/app/cards/availability.ts index 7267836b..89b73f41 100644 --- a/src/app/cards/availability.ts +++ b/src/app/cards/availability.ts @@ -99,6 +99,7 @@ export function cardAvailability(snapshot: ControlSnapshot): CardAvailability { || status.angleTuning != null || status.longRangeMode != null || status.sensorMode != null || status.performanceDuration != null + || status.longRangeMode != null ); const eggs = host && traits.eggControls; diff --git a/src/device/controller.ts b/src/device/controller.ts index 690f427f..abb00121 100644 --- a/src/device/controller.ts +++ b/src/device/controller.ts @@ -54,6 +54,7 @@ import { isEggWeClient, type EggWeHidClient, } from "@openmouse/protocol/drivers/endgame/egg-we-control"; +import { AtkBitmouseHidClient } from "@openmouse/protocol/drivers/atk/bitmouse-hid"; import { AtkHidClient } from "@openmouse/protocol/drivers/atk/hid"; import { LamzuHidClient } from "@openmouse/protocol/drivers/lamzu/hid"; import { @@ -191,7 +192,7 @@ function activeAs(...classes: ClientClass[]): T | null { return null; } -const DM_CLASSES = [WLMouseHidClient, LamzuHidClient, AtkHidClient, NinjutsoHidClient] as const; +const DM_CLASSES = [WLMouseHidClient, LamzuHidClient, AtkHidClient, AtkBitmouseHidClient, NinjutsoHidClient] as const; const RAZER_CLASSES = [RazerHidClient, RazerViperMiniHidClient, RazerViperHidClient, RazerCobraHidClient] as const; const NEEDS_OPEN = [TeevolutionHidClient, VgnF2HidClient, KeychronNapeHidClient, KeychronM6HidClient, ModdoHidClient, ZaunkoenigHidClient, FantechHidClient, WallhackMouseHidClient, WallhackKeyboardHidClient, GloriousHidClient, GloriousClassicHidClient, MchoseHidClient, MchoseDockHidClient] as const; const PULSAR_CLASSES = [PulsarHidClient, PulsarProHidClient, PulsarXs1HidClient] as const; @@ -200,8 +201,8 @@ const logitechClient = (): LogitechHidppClient | null => activeAs(LogitechHidppC const eggClient = (): EggOp1HidClient | null => activeAs(EggOp1HidClient); const eggWeClient = (): EggWeHidClient | null => active !== null && isEggWeClient(active) ? active : null; -const dmClient = (): WLMouseHidClient | LamzuHidClient | AtkHidClient | NinjutsoHidClient | null => - activeAs(...DM_CLASSES); +const dmClient = (): WLMouseHidClient | LamzuHidClient | AtkHidClient | AtkBitmouseHidClient | NinjutsoHidClient | null => + activeAs(...DM_CLASSES); const razerClient = (): RazerHidClient | RazerViperMiniHidClient | RazerViperHidClient | RazerCobraHidClient | null => activeAs(...RAZER_CLASSES); const viperClient = (): RazerViperV4ProHidClient | null => activeAs(RazerViperV4ProHidClient); @@ -3330,6 +3331,51 @@ export function applyTeevolutionSensorMode(mode: NonNullable scale.max) return; + const millimetres = scale.minMillimetres + + ((code - scale.min) * (scale.maxMillimetres - scale.minMillimetres)) / Math.max(1, scale.max - scale.min); + const label = `Lift-off ${millimetres.toFixed(1)} mm`; + stageChange({ + key: "lift-off-scale", + label, + command: `Set lift-off to ${millimetres.toFixed(1)} mm`, + progress: `Setting lift-off to ${millimetres.toFixed(1)} mm…`, + preview: (status) => { + if (!status.liftOffScale) return; + status.liftOffScale = { ...status.liftOffScale, value: code, millimetres }; + }, + apply: async () => { + await requireClientMethod("setLiftOffScale", "lift-off distance").setLiftOffScale(code); + }, + }); +} + +export function applySensorMode(mode: NonNullable): void { + if (!hasActiveClient()) return; + stageChange({ + key: "sensor-mode", + label: `Sensor mode ${mode}`, + command: `Set sensor mode to ${mode}`, + progress: `Setting sensor mode to ${mode}…`, + preview: (status) => { + status.sensorMode = mode; + }, + apply: async () => { + await requireClientMethod("setSensorMode", "the sensor mode").setSensorMode(mode); + }, + }); +} export function applyTeevolutionPerformanceDuration(duration: number): void { if (!teevolutionClient()) return; const label = sleepLabel(duration * 10); diff --git a/src/device/traits.ts b/src/device/traits.ts index 3cff61c0..21b17a55 100644 --- a/src/device/traits.ts +++ b/src/device/traits.ts @@ -44,6 +44,7 @@ const BY_FAMILY: Readonly>> = { "attack-shark": DIRECT_MODE, crdrako: DIRECT_MODE, atk: DIRECT_MODE, + "atk-bitmouse": DIRECT_MODE, ninjutso: { ...DIRECT_MODE, ninjutso: true }, "keychron-nape": { advancedSection: true, sleep: true, directMode: true }, fantech: { advancedSection: true, sleep: true, directMode: true }, diff --git a/src/supported-mice.test.ts b/src/supported-mice.test.ts index 697ff0d4..934bd229 100644 --- a/src/supported-mice.test.ts +++ b/src/supported-mice.test.ts @@ -4,6 +4,7 @@ import test from "node:test"; import { DEVICE_DRIVERS } from "@openmouse/protocol/drivers/registry"; import { ATK_COMPX_PRODUCT_IDS } from "@openmouse/protocol/drivers/atk/products"; import { WLMOUSE_PRODUCTS, GLORIOUS_PRODUCTS, GLORIOUS_CLASSIC_PRODUCTS } from "@openmouse/protocol/drivers/vendors"; +import { BITMOUSE_PRODUCT_IDS } from "@openmouse/protocol/bitmouse"; import { EGG_DEVICE_PROFILES } from "@openmouse/protocol/endgame-gear-op1"; import { KEYCHRON_NAPE_PRODUCTS } from "@openmouse/protocol/keychron"; import { LAMZU_PRODUCTS } from "@openmouse/protocol/lamzu"; @@ -80,6 +81,7 @@ test("supported / PR / quickwin claims require a registered driver brand", () => // is exempt. The moment the protocol pins those PIDs, the row is flipped to // "supported" and this check proves the PIDs are real. const PID_UNIVERSE = new Set([ + ...BITMOUSE_PRODUCT_IDS, ...WLMOUSE_PRODUCTS.keys(), ...LAMZU_PRODUCTS.keys(), ...LOGITECH_DIRECT_PRODUCT_IDS, diff --git a/src/supported-mice.ts b/src/supported-mice.ts index ba18e3ae..4c33d424 100644 --- a/src/supported-mice.ts +++ b/src/supported-mice.ts @@ -432,8 +432,11 @@ export const MICE: Mouse[] = [ note: "ATK driver (0x373b) likely covers — needs hardware test" }, { brand: "ATK", model: "VXE Mad R Major", status: "likely", req: 1, note: "ATK driver (0x373b) likely covers — needs hardware test" }, + { brand: "ATK", model: "ZERO", status: "supported", req: 1, + pids: [0x1154, 0x1155], + note: "BITMOUSE driver — wired 0x1154, 8K receiver 0x1155, usagePage 0xff05. Verified on hardware: DPI, poll rate, debounce, sleep, motion sync, ripple. Lift-off not decoded" }, { brand: "ATK", model: "VXE Zero Normal", status: "likely", req: 1, - note: "ATK driver (0x373b) likely covers — needs hardware test" }, + note: "The 0xff02 ATK driver does not reach the ZERO family — it uses the BITMOUSE channel on 0xff05; needs its own PID confirmed on hardware" }, { brand: "VGN", model: "Dragonfly F2 Master+", status: "supported", req: 1, pids: [0xfb56, 0xfb57], note: "VGN F2 driver — PIDs 0xfb56/0xfb57" }, diff --git a/src/ui/device-images.ts b/src/ui/device-images.ts index 34eb4bd1..70c27f3a 100644 --- a/src/ui/device-images.ts +++ b/src/ui/device-images.ts @@ -55,6 +55,9 @@ const DEVICE_IMAGES: ReadonlyMap = new Map([ ["373e:006a", "crdrako-ko-one.png"], ["373e:006b", "crdrako-ko-one.png"], // Attack Shark R5 Ultra wired and wireless transports share the same shell. + // ATK ZERO wired and its 8K receiver are the same shell. + ["373b:1154", "atk-zero.png"], + ["373b:1155", "atk-zero.png"], ["373e:0046", "attackshark-r5-ultra.png"], ["373e:0047", "attackshark-r5-ultra.png"], // OP1 8K, Purple Frost, and v2. XM2 models use different shells.