diff --git a/src/device/controller.ts b/src/device/controller.ts index 7ce0bbf8..4a4f19b3 100644 --- a/src/device/controller.ts +++ b/src/device/controller.ts @@ -253,9 +253,10 @@ export const PULSAR_SLEEP_OPTIONS: ReadonlyArray = [ const BACKGROUND = "Background refresh"; const FLASH_STEP_DELAY_MS = 420; const FLASH_SETTLE_MS = 320; -// Longest a flash waits for an in-flight device refresh before giving up -// instead of busy-waiting forever. -const REFRESH_WAIT_TIMEOUT_MS = 2000; +// Longest a flash waits for an in-flight device read before giving up +// instead of busy-waiting forever. A full onboard-profile reread over a +// wireless link can take a few seconds. +const REFRESH_WAIT_TIMEOUT_MS = 5000; const previewModeEnabled = previewsEnabled(__BUILD_CHANNEL__, import.meta.env.DEV); const previewMode = previewModeEnabled @@ -357,6 +358,8 @@ const napeKeymaps = new Map(); const stagedNapeAssignments = new Map(); let keymapReadToken = 0; let onboardProfilesLoading = false; +// A status applied mid-write skips the profile read; the write's end retries it. +let onboardReloadDeferred = false; let lastDeviceMode: MouseStatus["deviceMode"] = "Unknown"; let lastProfileFormat: MouseStatus["onboardProfileFormat"] = null; @@ -2192,13 +2195,17 @@ export async function flashPendingChanges(): Promise { emit(); let written = 0; let failure: string | null = null; - if (refreshInProgress) { + // HID++ replies are matched by feature and function only, so a profile read + // still running in the background would take the write's sector reads and + // fail its checksum check. Let every read finish first. + const reading = (): boolean => refreshInProgress || onboardProfilesLoading; + if (reading()) { pendingStatusText = st("ctl.waitRefresh"); readStatus = pendingStatusText; emit(); const refreshDeadline = Date.now() + REFRESH_WAIT_TIMEOUT_MS; - while (refreshInProgress && Date.now() <= refreshDeadline) await wait(25); - if (refreshInProgress) { + while (reading() && Date.now() <= refreshDeadline) await wait(25); + if (reading()) { const timeout = new Error("Timed out waiting for the device refresh."); recordDiagnosticError(timeout, st("ctl.unableFlash")); failure = timeout.message; @@ -2248,8 +2255,11 @@ export async function flashPendingChanges(): Promise { function endDeviceWrite(): void { settingInProgress = false; + const retryProfiles = onboardReloadDeferred && onboardProfiles === null && !onboardProfilesLoading; + onboardReloadDeferred = false; syncProfileDerivedState(); emit(); + if (retryProfiles) void reloadOnboardProfiles(); } function saveInterfacePreferences(): void { @@ -2697,7 +2707,10 @@ function applyStatusInner(deviceStatus: MouseStatus, statusKey?: string): void { if (supportsOnboard) { lastDeviceMode = status.deviceMode; lastProfileFormat = status.onboardProfileFormat ?? null; - if (onboardProfiles === null && !onboardProfilesLoading) void reloadOnboardProfiles(); + // A read started mid-write would interleave with the write's sector reads, + // so it waits for endDeviceWrite(). + if (onboardProfiles === null && !onboardProfilesLoading && settingInProgress) onboardReloadDeferred = true; + if (onboardProfiles === null && !onboardProfilesLoading && !settingInProgress) void reloadOnboardProfiles(); else syncProfileDerivedState(); } syncEditedNapeLayer(deviceStatus); @@ -4696,6 +4709,12 @@ export function describeProfileEntry(entry: OnboardProfile | null): { name: stri }; } +/** The reload button. Skipped mid-write, which rereads the profiles itself. */ +export function rereadOnboardProfiles(): void { + if (settingInProgress) return; + void reloadOnboardProfiles(); +} + export async function reloadOnboardProfiles(): Promise { const client = logitechClient(); if (!client || onboardProfilesLoading) return false; diff --git a/src/features/profiles/Profiles.tsx b/src/features/profiles/Profiles.tsx index e081e92f..278c1cc8 100644 --- a/src/features/profiles/Profiles.tsx +++ b/src/features/profiles/Profiles.tsx @@ -84,7 +84,7 @@ export function Profiles({ type="button" aria-label={t(locale, "prof.reload")} title={t(locale, "prof.reload")} - onClick={() => void control.reloadOnboardProfiles()} + onClick={() => control.rereadOnboardProfiles()} >