@@ -425,8 +506,60 @@ export function ProcessingCard({ snapshot }: { snapshot: ControlSnapshot }): Rea
);
}
-function AngleTuningControl({ value, label }: { value: number; label: string }): ReactNode {
+/**
+ * ATK F1 Ultimate scroll anti-mistouch: on/off plus a 100-1000 ms window
+ * slider on the line below, mirroring the vendor HUB layout. Verified
+ * 100/500 ms on hardware; the driver accepts 10 ms steps.
+ */
+function AtkAntiMistouchControl({ milliseconds, busy }: { milliseconds: number; busy: boolean }): ReactNode {
const [dragging, setDragging] = useState
(null);
+ const shown = dragging ?? Math.max(100, milliseconds);
+ const apply = (next: number): void => {
+ void control.applyAtkAntiMistouch(Math.max(100, Math.min(1000, next)));
+ };
+ return (
+
+
0}
+ disabled={busy}
+ onChange={(next) => void control.applyAtkAntiMistouch(next ? 100 : 0)}
+ />
+
+ Anti-mistouch window
+ {milliseconds} ms
+
+
+ apply(shown - 50)}>−
+ setDragging(Number(event.currentTarget.value))}
+ onChange={(event) => {
+ const next = Number(event.currentTarget.value);
+ setDragging(null);
+ apply(next);
+ }}
+ onBlur={() => setDragging(null)}
+ />
+ = 1000} onClick={() => apply(shown + 50)}>+
+
+ 100 ms 500 ms 1000 ms
+
+
+ );
+}
+
+function AngleTuningControl({ value, label }: { value: number; label: string }): ReactNode { const [dragging, setDragging] = useState(null);
const shown = dragging ?? value;
const apply = (next: number): void => control.applyAngleTuning(Math.max(-30, Math.min(30, next)));
diff --git a/src/app/cards/AtkCards.tsx b/src/app/cards/AtkCards.tsx
index 11dc2188..14ec9a26 100644
--- a/src/app/cards/AtkCards.tsx
+++ b/src/app/cards/AtkCards.tsx
@@ -115,3 +115,63 @@ export function AtkReceiverCard({ snapshot }: { snapshot: ControlSnapshot }): Re
);
}
+
+const ATK_SENSOR_MODE_LABELS = ["Basic Mode", "ATK Shard Competitive Firmware", "ATK Shard Competitive Firmware MAX"] as const;
+const ATK_DONGLE_LIGHT_LABELS = ["Close", "Polling Rate Effect", "Battery Gradient Light Effect", "Low Battery Effect"] as const;
+
+/**
+ * F1 Ultimate extras, grouped like the ATK HUB pages: sensor sampling rate
+ * lives with polling under Performance, while scroll anti-mistouch and the
+ * receiver dongle LED live under Advanced (HUB Parameter page). Each control
+ * renders only when the driver actually reported the field; all three were
+ * verified on the F1 Ultimate 2.0 (CID 01, MID 08 over 373B:11D9).
+ */
+export function AtkSensorCard({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
+ const sensorMode = snapshot.status?.atkSensorMode ?? null;
+ if (sensorMode == null) return null;
+ return (
+
+
+
PERFORMANCE
Sensor Sampling Rate
+
+ ({ value, label }))}
+ value={sensorMode}
+ disabled={snapshot.settingInProgress}
+ className="three"
+ onChange={(mode) => void control.selectAtkSensorMode(mode)}
+ />
+
+ {sensorMode === 2
+ ? "In this mode, the mouse sensor is in high performance state, high scanning frequency, more responsive control."
+ : sensorMode === 1
+ ? "ATK Shard competitive firmware: balanced scan rate for match play."
+ : "Basic sensor mode for daily work and maximum battery life."}
+
+
+ );
+}
+
+export function AtkDongleCard({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
+ const dongleLight = snapshot.status?.atkDongleLight ?? (snapshot.status?.atkSensorMode != null ? 2 : null);
+ if (dongleLight == null) return null;
+ return (
+
+
+
PARAMETER
Dongle Light Effect
+
+ Lighting Effect Mode
+ ({ value, label }))}
+ value={dongleLight}
+ disabled={snapshot.settingInProgress}
+ onChange={(mode) => void control.selectAtkDongleLight(mode)}
+ />
+
+ Effect on the 8K receiver; write-only, confirmed on the LED.
+
+
+ );
+}
diff --git a/src/app/cards/availability.ts b/src/app/cards/availability.ts
index db6c717c..c401fbac 100644
--- a/src/app/cards/availability.ts
+++ b/src/app/cards/availability.ts
@@ -29,6 +29,8 @@ export interface CardAvailability {
atkButtons: boolean;
atkProfile: boolean;
atkReceiver: boolean;
+ atkF1Sensor: boolean;
+ atkF1Dongle: boolean;
mxMasterButtons: boolean;
pulsarPro: boolean;
onboardProfiles: boolean;
@@ -67,6 +69,8 @@ const NOTHING: CardAvailability = {
atkButtons: false,
atkProfile: false,
atkReceiver: false,
+ atkF1Sensor: false,
+ atkF1Dongle: false,
mxMasterButtons: false,
pulsarPro: false,
onboardProfiles: false,
@@ -173,6 +177,10 @@ export function cardAvailability(snapshot: ControlSnapshot): CardAvailability {
atkButtons: (status.atkButtonMappings?.length ?? 0) > 0,
atkProfile: status.atkProfileCount !== undefined && status.activeProfile !== null,
atkReceiver: status.atkReceiver !== undefined,
+ atkF1Sensor: status.atkSensorMode != null,
+ // Write-only with no read command: gate on F1 presence and default the
+ // selector to Battery (vendor default) until the first write lands.
+ atkF1Dongle: status.atkSensorMode != null,
mxMasterButtons: traits.logitech && (snapshot.buttons?.length ?? 0) > 0,
pulsarPro: host && isPulsarProProtocol(status),
};
diff --git a/src/control.css b/src/control.css
index d4e0d47b..0527b5c8 100644
--- a/src/control.css
+++ b/src/control.css
@@ -854,6 +854,7 @@ nav { display: grid; gap: .3rem; margin-top: 1.4rem; }
#polling-card .title-row p, #host-lod-row .title-row p { margin: 0; }
#polling-card .title-row h2, #host-lod-row .title-row h2 { font-size: 1.15rem; }
.setting-note { display: block; margin-top: 1rem; color: var(--text-soft); font-size: .72rem; line-height: 1.5; }
+.setting-note-stable { min-height: 3em; }
.setting-action { display: flex; align-items: center; justify-content: space-between; gap: .7rem; margin-top: .7rem; color: var(--text-soft); font-size: .7rem; font-weight: 500; }
.setting-action #dpi-pending { color: var(--text-strong); font-weight: 650; }
.setting-action button { padding: .4rem .65rem; border: 1px solid var(--text); border-radius: 6px; background: var(--text); color: var(--ink); font-size: .65rem; font-weight: 700; }
@@ -946,6 +947,7 @@ nav { display: grid; gap: .3rem; margin-top: 1.4rem; }
.angle-tuning-inputs input[type="range"] { width: 100%; margin: 0; accent-color: var(--ui-accent); cursor: pointer; }
.angle-tuning-scale { display: flex; justify-content: space-between; margin: .12rem 2.15rem 0; color: var(--ghost); font: .48rem var(--font-mono); }
.angle-tuning-scale i { font-style: normal; }
+.setting-separator { border-top: 1px solid var(--line-strong); margin-top: .55rem; }
.teevolution-dpi-light-controls { margin-top: .55rem; }
.egg-collapsible, .egg-experimental { overflow: hidden; border: 1px solid var(--line-strong); border-radius: 9px; background: var(--card); }
diff --git a/src/device/atk-fields.d.ts b/src/device/atk-fields.d.ts
new file mode 100644
index 00000000..0d139129
--- /dev/null
+++ b/src/device/atk-fields.d.ts
@@ -0,0 +1,9 @@
+import type {} from "@openmouse/protocol/drivers/mouse-types";
+
+declare module "@openmouse/protocol/drivers/mouse-types" {
+ interface MouseStatus {
+ atkSensorMode?: number | null;
+ atkAntiMistouchMs?: number | null;
+ atkDongleLight?: number | null;
+ }
+}
diff --git a/src/device/controller.ts b/src/device/controller.ts
index bef043cd..42ac46d5 100644
--- a/src/device/controller.ts
+++ b/src/device/controller.ts
@@ -1302,6 +1302,68 @@ export async function selectAtkR1Profile(profile: number): Promise {
}
}
+/**
+ * ATK F1 Ultimate settings (sensor mode, anti-mistouch, dongle light) use the
+ * shared staged flow: edits preview instantly and join the footer Apply bar
+ * like every other control.
+ */
+export function selectAtkSensorMode(mode: number): void {
+ if (!hasActiveClient()) return;
+ if (!Number.isInteger(mode) || mode < 0 || mode > 2) return;
+ stageChange({
+ key: "atk-sensor-mode",
+ label: `Sensor mode ${["Basic", "Shard", "MAX"][mode] ?? mode}`,
+ command: `Set ATK sensor mode to ${mode}`,
+ progress: `Setting ATK sensor mode…`,
+ preview: (status) => {
+ status.atkSensorMode = mode;
+ },
+ apply: async () => {
+ await (requireClientMethod("setAtkSensorMode", "the sensor mode") as unknown as {
+ setAtkSensorMode: (mode: number) => Promise;
+ }).setAtkSensorMode(mode);
+ },
+ });
+}
+
+export function applyAtkAntiMistouch(milliseconds: number): void {
+ if (!hasActiveClient()) return;
+ if (!Number.isInteger(milliseconds) || milliseconds < 0 || milliseconds > 2550) return;
+ stageChange({
+ key: "atk-anti-mistouch",
+ label: milliseconds === 0 ? "Anti-mistouch off" : `Anti-mistouch ${milliseconds} ms`,
+ command: `Set ATK anti-mistouch to ${milliseconds} ms`,
+ progress: `Setting ATK anti-mistouch…`,
+ preview: (status) => {
+ status.atkAntiMistouchMs = milliseconds;
+ },
+ apply: async () => {
+ await (requireClientMethod("setAntiMistouchMs", "anti-mistouch") as unknown as {
+ setAntiMistouchMs: (milliseconds: number) => Promise;
+ }).setAntiMistouchMs(milliseconds);
+ },
+ });
+}
+
+export function selectAtkDongleLight(mode: number): void {
+ if (!hasActiveClient()) return;
+ if (!Number.isInteger(mode) || mode < 0 || mode > 3) return;
+ stageChange({
+ key: "atk-dongle-light",
+ label: `Dongle light ${["Off", "Polling", "Battery", "Low battery"][mode] ?? mode}`,
+ command: `Set ATK dongle light to ${mode}`,
+ progress: `Setting ATK dongle light…`,
+ preview: (status) => {
+ status.atkDongleLight = mode;
+ },
+ apply: async () => {
+ await (requireClientMethod("setDongleLight", "the dongle light") as unknown as {
+ setDongleLight: (mode: number) => Promise;
+ }).setDongleLight(mode);
+ },
+ });
+}
+
export async function pairAtkR1SePlusReceiver(): Promise {
if (blockedByGameProfileDraft()) return;
const client = activeAs(AtkHidClient);