+
- DPI
+ DPI {status.dpiStageColors ? Color : null}
{stages.map((dpi, index) => {
const isActive = index === active;
@@ -225,6 +225,15 @@ function DpiStages({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
disabled={disabled}
onChange={(event) => control.applyDpiStageValue(index, Number(event.currentTarget.value))}
/>
+ {status.dpiStageColors ? (
+
control.applyDpiStageColor(index, event.currentTarget.value)}
+ />
+ ) : null}
);
})}
diff --git a/src/app/cards/availability.test.ts b/src/app/cards/availability.test.ts
index 137044ed..170e2f39 100644
--- a/src/app/cards/availability.test.ts
+++ b/src/app/cards/availability.test.ts
@@ -107,6 +107,44 @@ test("Pulsar keeps the shared advanced cards", () => {
assert.equal(has.eggFilter, false);
});
+test("ATK exposes its processing and DPI-lighting cards from reported controls", () => {
+ const has = cardAvailability(snapshot({
+ status: {
+ brand: "VXE",
+ ui: {
+ family: "atk",
+ showAdvancedSection: true,
+ dpiLighting: { modes: [0, 1, 2], brightness: [0, 1, 2], speed: [0, 1, 2] },
+ },
+ longRangeMode: false,
+ },
+ }));
+ assert.equal(has.advancedHost, true);
+ assert.equal(has.processing, true);
+ assert.equal(has.teevolutionDpiLighting, true);
+});
+
+test("ATK inspection cards require data actually read from the device", () => {
+ const empty = cardAvailability(snapshot({ status: { brand: "VXE", ui: { family: "atk" } } }));
+ assert.equal(empty.atkButtons, false);
+ assert.equal(empty.atkProfile, false);
+ assert.equal(empty.atkReceiver, false);
+
+ const inspected = cardAvailability(snapshot({
+ status: {
+ brand: "VXE",
+ ui: { family: "atk" },
+ activeProfile: 0,
+ atkProfileCount: 4,
+ atkButtonMappings: [{ id: "left" }] as never,
+ atkReceiver: { online: true } as never,
+ },
+ }));
+ assert.equal(inspected.atkButtons, true);
+ assert.equal(inspected.atkProfile, true);
+ assert.equal(inspected.atkReceiver, true);
+});
+
test("Keychron Nape Pro gets Auto sleep without debounce or signal", () => {
const has = cardAvailability(snapshot({
status: {
diff --git a/src/app/cards/availability.ts b/src/app/cards/availability.ts
index 1ad7d989..39db3a9a 100644
--- a/src/app/cards/availability.ts
+++ b/src/app/cards/availability.ts
@@ -25,6 +25,9 @@ export interface CardAvailability {
eggCpi: boolean;
eggButtons: boolean;
razerButtons: boolean;
+ atkButtons: boolean;
+ atkProfile: boolean;
+ atkReceiver: boolean;
mxMasterButtons: boolean;
pulsarPro: boolean;
profiles: boolean;
@@ -56,6 +59,9 @@ const NOTHING: CardAvailability = {
eggCpi: false,
eggButtons: false,
razerButtons: false,
+ atkButtons: false,
+ atkProfile: false,
+ atkReceiver: false,
mxMasterButtons: false,
pulsarPro: false,
profiles: false,
@@ -82,6 +88,7 @@ export function cardAvailability(snapshot: ControlSnapshot): CardAvailability {
|| (status.rippleControl != null && ui?.hideRippleControl !== true)
|| (status.performanceMode != null && !traits.eggFamily && !traits.finalmouse)
|| status.hyperMode != null
+ || status.longRangeMode != null
|| status.sensorMode != null || status.performanceDuration != null
);
@@ -117,7 +124,8 @@ export function cardAvailability(snapshot: ControlSnapshot): CardAvailability {
&& Boolean(status.ninjutsoSystemMode || status.ninjutsoOpticalEngine),
ninjutsoClick: host && traits.ninjutso
&& Boolean(status.ninjutsoHyperClick != null || status.ninjutsoSlamClick),
- teevolutionDpiLighting: host && traits.teevolution && capabilities?.teevolutionProfile != null,
+ teevolutionDpiLighting: host && (ui?.dpiLighting != null
+ || (traits.teevolution && capabilities?.teevolutionProfile != null)),
finalmouse: host && traits.finalmouse,
eggFilter: eggs,
eggSpdt: eggs,
@@ -135,6 +143,9 @@ export function cardAvailability(snapshot: ControlSnapshot): CardAvailability {
// the driver's own field directly. Only the base RazerHidClient populates
// it, and only for a product whose profile sets buttonMapping.
razerButtons: status.razerButtonMappings != null,
+ atkButtons: (status.atkButtonMappings?.length ?? 0) > 0,
+ atkProfile: status.atkProfileCount !== undefined && status.activeProfile !== null,
+ atkReceiver: status.atkReceiver !== undefined,
mxMasterButtons: traits.logitech && (snapshot.buttons?.length ?? 0) > 0,
pulsarPro: host && isPulsarProProtocol(status),
};
diff --git a/src/control-profiles.css b/src/control-profiles.css
index e89d02fc..a9caba89 100644
--- a/src/control-profiles.css
+++ b/src/control-profiles.css
@@ -267,8 +267,11 @@
#dpi-stages { margin-top: .65rem; }
.dpi-stage-list .dpi-slot-row { grid-template-columns:1.7rem minmax(0,1fr) }
.dpi-stage-list .dpi-slot-head { grid-template-columns:1.7rem minmax(0,1fr) }
+.dpi-stage-list.has-colors .dpi-slot-row { grid-template-columns:1.7rem minmax(0,1fr) 3rem }
+.dpi-stage-list input[type="color"] { width:100%;height:1.8rem;padding:.15rem;border:1px solid var(--border);border-radius:7px;background:var(--surface-raised) }
@media (max-width:640px) {
.dpi-stage-list .dpi-slot-row { grid-template-columns:1.7rem minmax(0,1fr) }
+ .dpi-stage-list.has-colors .dpi-slot-row { grid-template-columns:1.7rem minmax(0,1fr) 3rem }
}
/* `.segmented` and friends set display, which outranks the user-agent
diff --git a/src/device/atk.test.ts b/src/device/atk.test.ts
new file mode 100644
index 00000000..f8a167c2
--- /dev/null
+++ b/src/device/atk.test.ts
@@ -0,0 +1,17 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { isVxeR1SePlusReceiver, receiverPairingSucceeded } from "./atk.ts";
+
+test("R1 SE+ pairing controls require the exact verified receiver", () => {
+ assert.equal(isVxeR1SePlusReceiver({ vendorId: 0x3554, productId: 0xf58e }), true);
+ assert.equal(isVxeR1SePlusReceiver({ vendorId: 0x373b, productId: 0x1085 }), false);
+ assert.equal(isVxeR1SePlusReceiver(null), false);
+});
+
+test("pairing requires terminal status and online telemetry", () => {
+ const receiver = { status: 1, rfId: "CBCAD9", pairingSecondsRemaining: 0 };
+ assert.equal(receiverPairingSucceeded({ ...receiver, online: true, pairingStatus: 2 }, true), true);
+ assert.equal(receiverPairingSucceeded({ ...receiver, online: true, pairingStatus: 2 }, false), false);
+ assert.equal(receiverPairingSucceeded({ ...receiver, online: false, pairingStatus: 2 }, true), false);
+ assert.equal(receiverPairingSucceeded({ ...receiver, online: true, pairingStatus: 1 }, true), false);
+});
diff --git a/src/device/atk.ts b/src/device/atk.ts
new file mode 100644
index 00000000..ee63729a
--- /dev/null
+++ b/src/device/atk.ts
@@ -0,0 +1,17 @@
+import type { AtkReceiverInfo } from "@openmouse/protocol/drivers/mouse-types";
+
+export const VXE_R1_SE_PLUS_RECEIVER = {
+ vendorId: 0x3554,
+ productId: 0xf58e,
+ cid: 0x02,
+ mid: 0x20,
+} as const;
+
+export function isVxeR1SePlusReceiver(device: Pick
| null): boolean {
+ return device?.vendorId === VXE_R1_SE_PLUS_RECEIVER.vendorId
+ && device.productId === VXE_R1_SE_PLUS_RECEIVER.productId;
+}
+
+export function receiverPairingSucceeded(receiver: AtkReceiverInfo, observedInProgress: boolean): boolean {
+ return observedInProgress && receiver.pairingStatus === 2 && receiver.online;
+}
diff --git a/src/device/controller.ts b/src/device/controller.ts
index 908202bf..b52d7f50 100644
--- a/src/device/controller.ts
+++ b/src/device/controller.ts
@@ -24,6 +24,11 @@ import {
} from "../pending-changes";
import { deviceImage } from "../ui/device-images";
import { batteryNeedsCharging } from "../ui/battery-icon";
+import {
+ isVxeR1SePlusReceiver,
+ receiverPairingSucceeded,
+ VXE_R1_SE_PLUS_RECEIVER,
+} from "./atk";
import {
DEFAULT_INTERFACE_PREFERENCES,
loadInterfacePreferences,
@@ -380,6 +385,7 @@ function buildSnapshot(): ControlSnapshot {
hasActiveDevice: activeDevice !== null,
deviceArtwork: deviceArtwork(status),
settingInProgress,
+ atkR1SePlusPairingAvailable: isVxeR1SePlusReceiver(activeDevice),
preferences: interfacePreferences,
sidebarHidden,
interfaceSettingsOpen,
@@ -531,6 +537,7 @@ function readCapabilities(): DeviceCapabilities {
? [...keychron.getSleepOptions()]
: null,
debounceMaxMs: dm?.getDebounceMaxMs() ?? null,
+ debounceOptions: dm && "getDebounceOptions" in dm ? [...dm.getDebounceOptions()] : null,
razerSleepOptions: razer?.getSleepOptions() ?? null,
razerLowPowerOptions: razer?.getLowPowerOptions() ?? null,
lowPowerPollingCeiling: razer?.getLowPowerPollingCeiling() ?? null,
@@ -903,6 +910,89 @@ export async function restoreDivertedButtons(): Promise {
}
}
+export async function selectAtkR1Profile(profile: number): Promise {
+ const client = activeAs(AtkHidClient);
+ if (!client || refreshInProgress || settingInProgress) return;
+ if (hasPendingChanges()) {
+ setReadStatus("Apply or discard pending changes before switching configuration banks.");
+ emit();
+ return;
+ }
+ const device = activeDevice;
+ settingInProgress = true;
+ setReadStatus(`Switching to configuration bank ${profile}…`);
+ emit();
+ recordDiagnosticCommand(`Select ATK R1 configuration bank ${profile}`);
+ try {
+ await client.setR1ActiveProfile(profile);
+ if (active !== client || activeDevice !== device) return;
+ const status = await statusAfterWrite(client);
+ if (active !== client || activeDevice !== device) return;
+ applyStatus(status);
+ setReadStatus(`Configuration bank ${profile} is active.`);
+ } catch (error) {
+ if (active !== client || activeDevice !== device) return;
+ recordDiagnosticError(error, "Unable to select that ATK R1 configuration bank.");
+ setReadStatus(error instanceof Error ? error.message : "Unable to select that configuration bank.");
+ } finally {
+ settingInProgress = false;
+ emit();
+ }
+}
+
+export async function pairAtkR1SePlusReceiver(): Promise {
+ const client = activeAs(AtkHidClient);
+ if (!client || !isVxeR1SePlusReceiver(activeDevice) || refreshInProgress || settingInProgress) return;
+ const device = activeDevice;
+ settingInProgress = true;
+ setReadStatus("Starting the R1 SE+ receiver pairing window…");
+ emit();
+ recordDiagnosticCommand("Pair ATK R1 SE+ receiver with CID 0x02, MID 0x20");
+ try {
+ const current = await client.readR1ReceiverInfo();
+ if (active !== client || activeDevice !== device) return;
+ if (current.pairingStatus === 1) {
+ if (latestDeviceStatus) latestDeviceStatus = { ...latestDeviceStatus, atkReceiver: current };
+ setReadStatus("The receiver already has an active pairing window. Wait for it to finish, then start R1 SE+ pairing.");
+ pushToast("info", "Pairing already active", "OpenMouse left the existing receiver countdown unchanged.");
+ return;
+ }
+ await client.startR1ReceiverPairing(VXE_R1_SE_PLUS_RECEIVER.cid, VXE_R1_SE_PLUS_RECEIVER.mid);
+ if (active !== client || activeDevice !== device) return;
+ let observedInProgress = false;
+ const deadline = Date.now() + 40_000;
+ while (Date.now() < deadline) {
+ const receiver = await client.readR1ReceiverInfo();
+ if (active !== client || activeDevice !== device) return;
+ if (latestDeviceStatus) latestDeviceStatus = { ...latestDeviceStatus, atkReceiver: receiver };
+ if (receiver.pairingStatus === 1) observedInProgress = true;
+ setReadStatus(receiver.pairingStatus === 1
+ ? `Pairing R1 SE+… ${receiver.pairingSecondsRemaining ?? 0}s remaining.`
+ : observedInProgress ? "Checking the completed pairing…" : "Waiting for the receiver pairing window…");
+ emit();
+ if (receiver.pairingStatus !== 1 && observedInProgress) {
+ if (receiverPairingSucceeded(receiver, observedInProgress)) {
+ setReadStatus("R1 SE+ pairing complete. The mouse is online.");
+ pushToast("success", "Receiver paired", "The R1 SE+ is online through the 1K receiver.");
+ return;
+ }
+ throw new Error(`Pairing ended without an online mouse (status ${receiver.pairingStatus ?? "unknown"}).`);
+ }
+ await wait(500);
+ }
+ throw new Error("The receiver pairing window expired before completion.");
+ } catch (error) {
+ if (active !== client || activeDevice !== device) return;
+ recordDiagnosticError(error, "Unable to pair the R1 SE+ receiver.");
+ const message = error instanceof Error ? error.message : "Unable to pair the R1 SE+ receiver.";
+ setReadStatus(message);
+ pushToast("error", "Pairing failed", message);
+ } finally {
+ settingInProgress = false;
+ emit();
+ }
+}
+
async function flashPause(milliseconds = FLASH_STEP_DELAY_MS): Promise {
if (interfacePreferences.reducedMotion) return;
await wait(milliseconds);
@@ -1826,8 +1916,17 @@ export function applyDpiStageCount(count: number): void {
if (!editor || editor.countEditable !== true) return;
if (!Number.isInteger(count) || count < 1 || count > editor.maxStages) return;
if (!("setDpiStageCount" in requireSettingsClient())) return;
+ const currentCount = latestDeviceStatus?.dpiStages?.length ?? count;
+ if (count < currentCount) {
+ for (const change of pendingChanges()) {
+ const match = /^dpi-stage(?:-color)?-(\d+)$/.exec(change.key);
+ if (match && Number(match[1]) >= count) dropPendingChange(change.key);
+ }
+ dropPendingChange("dpi-active-stage");
+ }
stageChange({
key: "dpi-stage-count",
+ priority: count > currentCount ? -1 : count < currentCount ? 1 : 0,
label: `${count} DPI stage${count === 1 ? "" : "s"}`,
command: `Set DPI stage count to ${count}`,
progress: `Setting ${count} DPI stages…`,
@@ -1839,6 +1938,11 @@ export function applyDpiStageCount(count: number): void {
while (padded.length < count) padded.push(padded.at(-1) ?? status.dpi);
status.dpiStages = padded;
}
+ if (status.dpiStageColors) {
+ const colors = status.dpiStageColors.slice(0, count);
+ while (colors.length < count) colors.push(colors.at(-1) ?? "#000000");
+ status.dpiStageColors = colors;
+ }
if ((status.activeDpiStage ?? 0) >= count) {
status.activeDpiStage = count - 1;
status.dpi = status.dpiStages[count - 1] ?? status.dpi;
@@ -1900,6 +2004,28 @@ export function applyDpiStageValue(stage: number, rawDpi: number): void {
});
}
+export function applyDpiStageColor(stage: number, color: string): void {
+ if (!hasActiveClient() || !/^#[0-9a-f]{6}$/i.test(color)) return;
+ const colors = latestDeviceStatus ? withPendingChanges(latestDeviceStatus).dpiStageColors : undefined;
+ if (!colors || !Number.isInteger(stage) || stage < 0 || stage >= colors.length) return;
+ const normalized = color.toLowerCase();
+ stageChange({
+ key: `dpi-stage-color-${stage}`,
+ label: `Stage ${stage + 1} color ${normalized}`,
+ command: `Set DPI stage ${stage + 1} color to ${normalized}`,
+ progress: `Setting stage ${stage + 1} color…`,
+ preview: (status) => {
+ const next = status.dpiStageColors?.slice() ?? [];
+ while (next.length <= stage) next.push("#000000");
+ next[stage] = normalized;
+ status.dpiStageColors = next;
+ },
+ apply: async () => {
+ await requireClientMethod("setDpiStageColor", "the DPI stage color").setDpiStageColor(stage, normalized);
+ },
+ });
+}
+
export function applyLogitechAxisDpi(dpiX: number, dpiY: number): void {
if (!logitechClient()) return;
if (!dpiOptions.includes(dpiX) || !dpiOptions.includes(dpiY)) {
@@ -2962,6 +3088,7 @@ function settingLabel(setting: PulsarToggleSetting): string {
rippleControl: "ripple control",
performanceMode: teevolutionClient() ? "highest performance" : "performance mode",
hyperMode: "Hyper mode",
+ longRangeMode: "ultra long range",
} as const)[setting];
}
@@ -2971,6 +3098,7 @@ const PULSAR_TOGGLE_METHOD: Record = {
rippleControl: "setRippleControl",
performanceMode: "setPerformanceMode",
hyperMode: "setHyperMode",
+ longRangeMode: "setLongRangeMode",
};
export function applyPulsarToggle(setting: PulsarToggleSetting, enabled: boolean): void {
@@ -3178,12 +3306,12 @@ export function applyTeevolutionPerformanceDuration(duration: number): void {
const TEEVOLUTION_DPI_LIGHT_GROUP = "teevolution-dpi-lighting";
async function writeStagedTeevolutionDpiLighting(): Promise {
- const client = teevolutionClient();
+ const client = activeSettingsClient();
if (!client || !latestDeviceStatus) {
- throw new Error("The Teevolution mouse is no longer connected.");
+ throw new Error("The mouse is no longer connected.");
}
const status = withPendingChanges(latestDeviceStatus);
- await client.setDpiLighting(
+ await requireClientMethod("setDpiLighting", "DPI lighting").setDpiLighting(
status.dpiLedMode ?? 0,
status.dpiLedBrightness ?? 5,
status.dpiLedSpeed ?? 3,
@@ -3191,7 +3319,7 @@ async function writeStagedTeevolutionDpiLighting(): Promise {
}
export function applyTeevolutionDpiLighting(setting: "mode" | "brightness" | "speed", value: number): void {
- if (!teevolutionClient()) return;
+ if (!hasActiveClient()) return;
const names = { mode: "effect", brightness: "brightness", speed: "speed" } as const;
const display = setting === "mode" ? (["Off", "Steady", "Breathing"][value] ?? `${value}`) : `${value}`;
stageChange({
diff --git a/src/device/types.ts b/src/device/types.ts
index 78547497..b6c28c18 100644
--- a/src/device/types.ts
+++ b/src/device/types.ts
@@ -23,7 +23,8 @@ export type PulsarToggleSetting =
| "angleSnapping"
| "rippleControl"
| "performanceMode"
- | "hyperMode";
+ | "hyperMode"
+ | "longRangeMode";
export interface TeevolutionProfile {
sleepOptions: readonly number[];
@@ -41,6 +42,7 @@ export interface DeviceCapabilities {
canDisableSleep: boolean;
sleepOptions: number[] | null;
debounceMaxMs: number | null;
+ debounceOptions?: number[] | null;
razerSleepOptions: number[] | null;
razerLowPowerOptions: number[] | null;
lowPowerPollingCeiling: number | null;
@@ -145,6 +147,7 @@ export interface ControlSnapshot {
hasActiveDevice: boolean;
deviceArtwork: string | null;
settingInProgress: boolean;
+ atkR1SePlusPairingAvailable: boolean;
preferences: InterfacePreferences;
sidebarHidden: boolean;
diff --git a/src/pending-changes.test.ts b/src/pending-changes.test.ts
index 61946172..e4f03a6a 100644
--- a/src/pending-changes.test.ts
+++ b/src/pending-changes.test.ts
@@ -92,6 +92,32 @@ test("restaging a grouped change does not split its group", () => {
clearPendingChanges();
});
+test("priority orders count increases first and reductions last", () => {
+ clearPendingChanges();
+ const stage = (key: string, priority?: number) => stagePendingChange({
+ key, priority, label: key, command: "", progress: "", apply: async () => {},
+ });
+
+ stage("dpi-stage-count", -1);
+ stage("dpi-stage-color-3");
+ stage("dpi-stage-count", -1);
+
+ assert.deepEqual(pendingChangeBatches().map(([change]) => change?.key), [
+ "dpi-stage-count",
+ "dpi-stage-color-3",
+ ]);
+
+ clearPendingChanges();
+ stage("dpi-stage-count", 1);
+ stage("dpi-stage-0");
+ stage("dpi-stage-count", 1);
+ assert.deepEqual(pendingChangeBatches().map(([change]) => change?.key), [
+ "dpi-stage-0",
+ "dpi-stage-count",
+ ]);
+ clearPendingChanges();
+});
+
test("pending changes preview without mutating the device status", () => {
clearPendingChanges();
const deviceStatus = status();
diff --git a/src/pending-changes.ts b/src/pending-changes.ts
index 704fe44d..db1b47ba 100644
--- a/src/pending-changes.ts
+++ b/src/pending-changes.ts
@@ -15,6 +15,8 @@ export interface PendingChange {
command: string;
// Live-status copy shown while this change is being written.
progress: string;
+ // Lower priorities flash first when one setting must establish storage used by another.
+ priority?: number;
// Mirrors the staged value onto a status snapshot so the UI can render it.
// Omitted when the setting is not part of MouseStatus (a Logitech onboard
// profile value, say); such a change renders itself and cannot be compared
@@ -71,7 +73,7 @@ export function pendingChangeBatches(): PendingChange[][] {
byGroup.set(change.group, batch);
batches.push(batch);
}
- return batches;
+ return batches.sort((left, right) => (left[0]?.priority ?? 0) - (right[0]?.priority ?? 0));
}
export function pendingChangeCount(): number {
diff --git a/src/supported-mice.test.ts b/src/supported-mice.test.ts
index 11b474a4..e49805ab 100644
--- a/src/supported-mice.test.ts
+++ b/src/supported-mice.test.ts
@@ -2,7 +2,8 @@ import assert from "node:assert/strict";
import test from "node:test";
import { DEVICE_DRIVERS } from "@openmouse/protocol/drivers/registry";
-import { WLMOUSE_PRODUCTS, GLORIOUS_PRODUCTS, GLORIOUS_CLASSIC_PRODUCTS, VENDOR_ID } from "@openmouse/protocol/drivers/vendors";
+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 { EGG_DEVICE_PROFILES } from "@openmouse/protocol/endgame-gear-op1";
import { KEYCHRON_NAPE_PRODUCTS } from "@openmouse/protocol/keychron";
import { LAMZU_PRODUCTS } from "@openmouse/protocol/lamzu";
@@ -54,9 +55,10 @@ test("brand + model are unique and request counts are sane", () => {
// Brands with an actual driver in @openmouse/protocol/drivers/registry.ts.
const DRIVER_BRANDS = new Set([
...DEVICE_DRIVERS.map((driver) => driver.brand.toLowerCase()),
- // CRDRAKO products are driven by the Lamzu/CompX driver and report their own
- // brand via deviceBrand().
+ // These products are driven by a shared protocol family and report their own
+ // brand after identification.
"crdrako",
+ "vxe",
]);
test("supported / PR / quickwin claims require a registered driver brand", () => {
@@ -101,6 +103,8 @@ const PID_UNIVERSE = new Set([
0x1960, 0x1961, 0x1962, 0x1968, 0x1970, 0x1972, 0x1982,
// VGN Dragonfly F2 Master+ (drivers/vgn/hid.ts).
0xfb56, 0xfb57,
+ // VXE R1 SE+ wired transport (drivers/atk/hid.ts).
+ ...ATK_COMPX_PRODUCT_IDS,
// Pulsar X3 family on the Sonix XS-1 feature interface (drivers/pulsar/pulsar-xs1-hid.ts).
...PULSAR_XS1_PRODUCT_IDS,
// Finalmouse ULX dongle (drivers/finalmouse/hid.ts).
diff --git a/src/supported-mice.ts b/src/supported-mice.ts
index 0086164a..3ca7c908 100644
--- a/src/supported-mice.ts
+++ b/src/supported-mice.ts
@@ -421,9 +421,9 @@ export const MICE: Mouse[] = [
note: "ATK vendor (0x373b, usagePage 0xff02) covered" },
{ brand: "ATK", model: "VXE Dragonfly R1 Pro", status: "likely", req: 3,
note: "VGN F2 driver (0xfb56/0xfb57) covers Dragonfly F2; R1 Pro needs test" },
- { brand: "ATK", model: "VXE Dragonfly R1 SE / SE+", status: "likely", req: 3,
- pids: [0x1085],
- note: "Beken R1 on its stock 1K dongle (0x373b:0x1085). Poll rate now reads/writes the 0x0070 live-settings row per OpenVXE; debounce/angle on this family remain unverified" },
+ { brand: "VXE", model: "R1 SE+ (wired)", status: "supported", req: 3,
+ pids: [0xf58f],
+ note: "Wired 0x3554:0xf58f identity, status reads, and DPI writes verified through Chromium WebHID. Driver supports PAW3395SE DPI, polling, LOD, debounce, motion sync, ripple control, sleep timeout, angle snapping, battery, and firmware; receiver mode remains unverified" },
{ brand: "ATK", model: "X1 V2 Ultimate", status: "likely", req: 1,
note: "ATK driver (0x373b) likely covers — needs hardware test" },
{ brand: "ATK", model: "A9 Air", status: "likely", req: 1,