Report only real device data and remove unverifiable fields
- Drop simulated-controller fallback: simulation now only runs when explicitly enabled in Settings (or outside Tauri), so the app no longer claims a controller is connected when none is plugged in - Derive connection type from the kernel bus type (USB/Bluetooth) instead of an unreliable udev property - Read real battery percentage from /sys/class/power_supply; hide the field entirely when the device reports none - Remove Input mode and Firmware version fields (not obtainable) - Query virtual controller status from the backend instead of a hardcoded per-device flag - Exclude the app's own uinput virtual controller from device discovery - Rescan for controllers indefinitely while disconnected so a controller plugged in at any time is detected without restarting - Strip leftover debug instrumentation from the previous session Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Bluetooth, Cable, Radio, Usb } from "lucide-react";
|
||||
import { Bluetooth, Cable, Usb } from "lucide-react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
import { Badge } from "@/components/common/Badge";
|
||||
@@ -11,14 +11,12 @@ import type { ConnectionKind } from "@/types/controller";
|
||||
const CONNECTION_ICON: Record<ConnectionKind, typeof Usb> = {
|
||||
usb: Usb,
|
||||
bluetooth: Bluetooth,
|
||||
receiver2_4ghz: Radio,
|
||||
unknown: Cable,
|
||||
};
|
||||
|
||||
const CONNECTION_LABEL: Record<ConnectionKind, string> = {
|
||||
usb: "USB",
|
||||
bluetooth: "Bluetooth",
|
||||
receiver2_4ghz: "2.4GHz Receiver",
|
||||
unknown: "Unknown",
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,13 @@ import { useControllerStore } from "@/stores/controllerStore";
|
||||
import { useProfileStore } from "@/stores/profileStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { processStick, processTrigger } from "@/utils/deadzoneMath";
|
||||
import type { ConnectionKind } from "@/types/controller";
|
||||
|
||||
const CONNECTION_LABEL: Record<ConnectionKind, string> = {
|
||||
usb: "USB",
|
||||
bluetooth: "Bluetooth",
|
||||
unknown: "Unknown connection",
|
||||
};
|
||||
|
||||
export function Dashboard() {
|
||||
const controllers = useControllerStore((s) => s.controllers);
|
||||
@@ -20,6 +27,7 @@ export function Dashboard() {
|
||||
const pollRateHz = useControllerStore((s) => s.pollRateHz);
|
||||
const connectionStatus = useControllerStore((s) => s.connectionStatus);
|
||||
const connectionMessage = useControllerStore((s) => s.connectionMessage);
|
||||
const virtualControllerActive = useControllerStore((s) => s.virtualControllerActive);
|
||||
|
||||
const draftProfile = useProfileStore((s) => s.draftProfile);
|
||||
const summaries = useProfileStore((s) => s.summaries);
|
||||
@@ -60,16 +68,16 @@ export function Dashboard() {
|
||||
Connected
|
||||
</Badge>
|
||||
<Badge tone="neutral">
|
||||
<Usb size={12} /> {activeController.mode.toUpperCase()}
|
||||
<Usb size={12} /> {CONNECTION_LABEL[activeController.connection]}
|
||||
</Badge>
|
||||
{activeController.batteryPercent !== null && (
|
||||
<Badge tone="neutral">
|
||||
<BatteryMedium size={12} /> {activeController.batteryPercent}%
|
||||
</Badge>
|
||||
)}
|
||||
<Badge tone={activeController.virtualControllerActive ? "accent" : "warning"}>
|
||||
<Badge tone={virtualControllerActive ? "accent" : "warning"}>
|
||||
<Power size={12} />
|
||||
Virtual controller {activeController.virtualControllerActive ? "active" : "inactive"}
|
||||
Virtual controller {virtualControllerActive ? "active" : "inactive"}
|
||||
</Badge>
|
||||
{activeController.isSimulated && <Badge tone="warning">Simulated device</Badge>}
|
||||
</div>
|
||||
@@ -79,7 +87,7 @@ export function Dashboard() {
|
||||
<p className="text-sm text-fg-muted">
|
||||
{connectionStatus === "reconnecting"
|
||||
? connectionMessage ?? "Reconnecting to controller…"
|
||||
: "No controller detected. Connect an 8BitDo Ultimate controller to get started."}
|
||||
: connectionMessage ?? "No controller detected. Connect an 8BitDo Ultimate controller to get started."}
|
||||
</p>
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
@@ -2,14 +2,12 @@ import type { ReactNode } from "react";
|
||||
|
||||
import { Badge } from "@/components/common/Badge";
|
||||
import { Panel } from "@/components/common/Panel";
|
||||
import { StatusDot } from "@/components/common/StatusDot";
|
||||
import { PageShell } from "@/components/layout/PageShell";
|
||||
import { useControllerStore } from "@/stores/controllerStore";
|
||||
|
||||
const CONNECTION_LABEL: Record<string, string> = {
|
||||
usb: "USB",
|
||||
bluetooth: "Bluetooth",
|
||||
receiver2_4ghz: "2.4GHz Receiver",
|
||||
unknown: "Unknown",
|
||||
};
|
||||
|
||||
@@ -66,21 +64,9 @@ export function DeviceInfo() {
|
||||
<Field label="Device path" value={controller.devicePath} mono />
|
||||
<Field label="Vendor ID" value={hex(controller.vendorId)} mono />
|
||||
<Field label="Product ID" value={hex(controller.productId)} mono />
|
||||
<Field label="Input mode" value={controller.mode.toUpperCase()} />
|
||||
<Field
|
||||
label="Battery"
|
||||
value={controller.batteryPercent !== null ? `${controller.batteryPercent}%` : "Not reported"}
|
||||
/>
|
||||
<Field label="Firmware version" value={controller.firmwareVersion ?? "Not available"} />
|
||||
<Field
|
||||
label="Virtual controller"
|
||||
value={
|
||||
<span className="flex items-center gap-1.5">
|
||||
<StatusDot tone={controller.virtualControllerActive ? "success" : "neutral"} />
|
||||
{controller.virtualControllerActive ? "Active" : "Inactive"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
{controller.batteryPercent !== null && (
|
||||
<Field label="Battery" value={`${controller.batteryPercent}%`} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 border-t border-border pt-4">
|
||||
|
||||
@@ -33,9 +33,6 @@ export function Settings() {
|
||||
const [diagnostics, setDiagnostics] = useState<DiagnosticsSnapshot | null>(null);
|
||||
|
||||
async function handleRemappingChange(enabled: boolean) {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H14',location:'src/pages/Settings.tsx:handleRemappingChange',message:'Settings remapping toggle changed',data:{enabled},timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
setRemappingEnabled(enabled);
|
||||
try {
|
||||
await setControllerRemapping(enabled);
|
||||
|
||||
@@ -33,10 +33,7 @@ export class MockControllerBackend implements ControllerBackend {
|
||||
devicePath: "/dev/input/event33 (simulated)",
|
||||
vendorId: DEMO_DEVICE_CAPABILITIES.vendorId,
|
||||
productId: DEMO_DEVICE_CAPABILITIES.productId,
|
||||
mode: "xinput",
|
||||
batteryPercent: 82,
|
||||
firmwareVersion: null,
|
||||
virtualControllerActive: true,
|
||||
batteryPercent: null,
|
||||
capabilities: DEMO_DEVICE_CAPABILITIES,
|
||||
isSimulated: true,
|
||||
};
|
||||
@@ -74,9 +71,8 @@ export class MockControllerBackend implements ControllerBackend {
|
||||
return this.rawEventEmitter.on(callback);
|
||||
}
|
||||
|
||||
async setRemappingEnabled(enabled: boolean): Promise<void> {
|
||||
this.info = { ...this.info, virtualControllerActive: enabled };
|
||||
this.controllersEmitter.emit(this.snapshotControllers());
|
||||
async setRemappingEnabled(_enabled: boolean): Promise<void> {
|
||||
// The simulated backend has no real uinput device to toggle.
|
||||
}
|
||||
|
||||
setSimulatedConnected(connected: boolean): void {
|
||||
|
||||
@@ -36,6 +36,7 @@ export const tauriControllerService = {
|
||||
startReader: (devicePath: string) => invoke<void>("start_device_reader", { devicePath }),
|
||||
applyRemappingProfile: (profile: Profile) => invoke<void>("apply_remapping_profile", { profile }),
|
||||
setRemappingEnabled: (enabled: boolean) => invoke<void>("set_remapping_enabled", { enabled }),
|
||||
virtualControllerActive: () => invoke<boolean>("virtual_controller_active"),
|
||||
onInput: async (callback: (event: RawInputEvent) => void): Promise<() => void> =>
|
||||
listen<NativeInputEvent>(INPUT_EVENT, (event) => callback(toRawInputEvent(event.payload))),
|
||||
onReaderStatus: async (callback: (status: ReaderStatus) => void): Promise<() => void> =>
|
||||
|
||||
@@ -7,21 +7,10 @@ import type { ControllerInfo, ControllerLiveState, RawInputEvent } from "@/types
|
||||
|
||||
const MAX_EVENT_LOG = 200;
|
||||
const MAX_POLL_SAMPLES = 30;
|
||||
const RECONNECT_INTERVAL_MS = 1_500;
|
||||
const MAX_RECONNECT_ATTEMPTS = 20;
|
||||
const RESCAN_INTERVAL_MS = 2_000;
|
||||
|
||||
export type ControllerConnectionStatus = "connected" | "reconnecting" | "disconnected" | "simulation";
|
||||
|
||||
function samePhysicalController(a: ControllerInfo, b: ControllerInfo): boolean {
|
||||
return (
|
||||
a.vendorId === b.vendorId &&
|
||||
a.productId === b.productId &&
|
||||
a.name === b.name &&
|
||||
a.connection === b.connection &&
|
||||
a.mode === b.mode
|
||||
);
|
||||
}
|
||||
|
||||
function estimatePollRateHz(timestamps: number[]): number | null {
|
||||
if (timestamps.length < 2) return null;
|
||||
const span = timestamps[timestamps.length - 1] - timestamps[0];
|
||||
@@ -29,6 +18,19 @@ function estimatePollRateHz(timestamps: number[]): number | null {
|
||||
return Math.round(((timestamps.length - 1) / span) * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches a controller across reconnects, where the kernel usually assigns a
|
||||
* new /dev/input/event* node (and therefore a new id).
|
||||
*/
|
||||
function samePhysicalController(a: ControllerInfo, b: ControllerInfo): boolean {
|
||||
return (
|
||||
a.vendorId === b.vendorId &&
|
||||
a.productId === b.productId &&
|
||||
a.name === b.name &&
|
||||
a.connection === b.connection
|
||||
);
|
||||
}
|
||||
|
||||
interface ControllerState {
|
||||
controllers: ControllerInfo[];
|
||||
activeControllerId: string | null;
|
||||
@@ -38,6 +40,7 @@ interface ControllerState {
|
||||
latencyEstimateMs: number | null;
|
||||
connectionStatus: ControllerConnectionStatus;
|
||||
connectionMessage: string | null;
|
||||
virtualControllerActive: boolean;
|
||||
initialized: boolean;
|
||||
usingSimulation: boolean;
|
||||
init: () => Promise<void>;
|
||||
@@ -48,17 +51,14 @@ interface ControllerState {
|
||||
}
|
||||
|
||||
let pollTimestamps: number[] = [];
|
||||
let hasLoggedFirstPhysicalEvent = false;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let reconnectAttempts = 0;
|
||||
let rescanTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let refreshInFlight = false;
|
||||
|
||||
function clearReconnectTimer(): void {
|
||||
if (reconnectTimer !== null) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
function clearRescanTimer(): void {
|
||||
if (rescanTimer !== null) {
|
||||
clearTimeout(rescanTimer);
|
||||
rescanTimer = null;
|
||||
}
|
||||
reconnectAttempts = 0;
|
||||
}
|
||||
|
||||
export const useControllerStore = create<ControllerState>((set, get) => ({
|
||||
@@ -70,6 +70,7 @@ export const useControllerStore = create<ControllerState>((set, get) => ({
|
||||
latencyEstimateMs: null,
|
||||
connectionStatus: "disconnected",
|
||||
connectionMessage: null,
|
||||
virtualControllerActive: false,
|
||||
initialized: false,
|
||||
usingSimulation: false,
|
||||
|
||||
@@ -77,31 +78,6 @@ export const useControllerStore = create<ControllerState>((set, get) => ({
|
||||
if (get().initialized) return;
|
||||
set({ initialized: true });
|
||||
|
||||
const simulationRequested = useSettingsStore.getState().simulationMode;
|
||||
let realControllers: ControllerInfo[] = [];
|
||||
try {
|
||||
realControllers = simulationRequested ? [] : await tauriControllerService.list();
|
||||
} catch {
|
||||
// Browser-only development and missing permissions both retain the safe
|
||||
// simulated fallback rather than leaving the UI unusable.
|
||||
}
|
||||
|
||||
const useSimulation = simulationRequested || realControllers.length === 0;
|
||||
set({
|
||||
controllers: useSimulation ? [] : realControllers,
|
||||
activeControllerId: useSimulation ? null : realControllers[0]?.id ?? null,
|
||||
usingSimulation: useSimulation,
|
||||
connectionStatus: useSimulation ? "simulation" : "connected",
|
||||
connectionMessage: useSimulation
|
||||
? simulationRequested
|
||||
? "Simulation mode is enabled."
|
||||
: "No physical controller detected; using simulated input."
|
||||
: null,
|
||||
});
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H3',location:'src/stores/controllerStore.ts:init',message:'Controller backend selected',data:{simulationRequested,realControllerCount:realControllers.length,useSimulation,devices:realControllers.map((controller)=>({id:controller.id,name:controller.name,connection:controller.connection,devicePath:controller.devicePath}))},timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
const receiveLiveState = (liveState: ControllerLiveState) => {
|
||||
pollTimestamps.push(liveState.timestamp);
|
||||
if (pollTimestamps.length > MAX_POLL_SAMPLES) pollTimestamps.shift();
|
||||
@@ -111,12 +87,6 @@ export const useControllerStore = create<ControllerState>((set, get) => ({
|
||||
};
|
||||
|
||||
const receiveRawEvent = (event: RawInputEvent) => {
|
||||
if (!get().usingSimulation && !hasLoggedFirstPhysicalEvent) {
|
||||
hasLoggedFirstPhysicalEvent = true;
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H2',location:'src/stores/controllerStore.ts:receiveRawEvent',message:'First physical input event received',data:{type:event.type,code:event.code,rawValue:event.rawValue,processedValue:event.processedValue},timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
}
|
||||
set((state) => ({ eventLog: [event, ...state.eventLog].slice(0, MAX_EVENT_LOG) }));
|
||||
set((state) => {
|
||||
const liveState = state.liveState ?? {
|
||||
@@ -135,113 +105,153 @@ export const useControllerStore = create<ControllerState>((set, get) => ({
|
||||
});
|
||||
};
|
||||
|
||||
if (!useSimulation) {
|
||||
const active = realControllers[0];
|
||||
await tauriControllerService.onInput(receiveRawEvent);
|
||||
const refreshNativeDevices = async (): Promise<boolean> => {
|
||||
if (refreshInFlight) return false;
|
||||
refreshInFlight = true;
|
||||
try {
|
||||
const previousActive = get().activeController();
|
||||
const devices = await tauriControllerService.list();
|
||||
const nextActive =
|
||||
devices.find((controller) => controller.id === previousActive?.id) ??
|
||||
(previousActive ? devices.find((controller) => samePhysicalController(controller, previousActive)) : undefined) ??
|
||||
devices[0];
|
||||
|
||||
if (!nextActive) {
|
||||
set({
|
||||
controllers: [],
|
||||
activeControllerId: null,
|
||||
liveState: null,
|
||||
pollRateHz: null,
|
||||
latencyEstimateMs: null,
|
||||
connectionStatus: "reconnecting",
|
||||
connectionMessage: "Waiting for the controller to reconnect…",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
set({
|
||||
controllers: devices,
|
||||
activeControllerId: nextActive.id,
|
||||
liveState: null,
|
||||
pollRateHz: null,
|
||||
latencyEstimateMs: null,
|
||||
connectionStatus: "connected",
|
||||
connectionMessage: null,
|
||||
});
|
||||
await tauriControllerService.startReader(nextActive.devicePath);
|
||||
clearReconnectTimer();
|
||||
return true;
|
||||
} catch {
|
||||
set({
|
||||
connectionStatus: "reconnecting",
|
||||
connectionMessage: "Unable to access the controller; retrying…",
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
refreshInFlight = false;
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleReconnect = (): void => {
|
||||
if (reconnectTimer !== null) return;
|
||||
const attemptReconnect = async (): Promise<void> => {
|
||||
reconnectTimer = null;
|
||||
if (await refreshNativeDevices()) return;
|
||||
reconnectAttempts += 1;
|
||||
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
set({
|
||||
connectionStatus: "disconnected",
|
||||
connectionMessage: "Controller did not reconnect. Reconnect it or restart input detection.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
reconnectTimer = setTimeout(() => void attemptReconnect(), RECONNECT_INTERVAL_MS);
|
||||
};
|
||||
void attemptReconnect();
|
||||
};
|
||||
|
||||
await tauriControllerService.onReaderStatus((status) => {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H6',location:'src/stores/controllerStore.ts:onReaderStatus',message:'Physical reader status changed',data:status,timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
const activeController = get().activeController();
|
||||
if (status.connected && activeController?.devicePath === status.devicePath) {
|
||||
clearReconnectTimer();
|
||||
set({ connectionStatus: "connected", connectionMessage: null });
|
||||
return;
|
||||
}
|
||||
if (!status.connected && activeController?.devicePath === status.devicePath) {
|
||||
set({
|
||||
liveState: null,
|
||||
pollRateHz: null,
|
||||
latencyEstimateMs: null,
|
||||
connectionStatus: "reconnecting",
|
||||
connectionMessage: status.message ?? "Controller disconnected; searching for it again…",
|
||||
});
|
||||
scheduleReconnect();
|
||||
}
|
||||
const startSimulation = async (message: string) => {
|
||||
set({
|
||||
controllers: [],
|
||||
activeControllerId: null,
|
||||
usingSimulation: true,
|
||||
connectionStatus: "simulation",
|
||||
connectionMessage: message,
|
||||
});
|
||||
if (active) {
|
||||
await tauriControllerService.startReader(active.devicePath);
|
||||
}
|
||||
await mockControllerBackend.init();
|
||||
mockControllerBackend.onControllersChanged((controllers) => {
|
||||
set((state) => ({
|
||||
controllers,
|
||||
activeControllerId: controllers.some((c) => c.id === state.activeControllerId)
|
||||
? state.activeControllerId
|
||||
: controllers[0]?.id ?? null,
|
||||
}));
|
||||
});
|
||||
mockControllerBackend.onLiveState(receiveLiveState);
|
||||
mockControllerBackend.onRawEvent(receiveRawEvent);
|
||||
};
|
||||
|
||||
if (useSettingsStore.getState().simulationMode) {
|
||||
await startSimulation("Simulation mode is enabled in Settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
await mockControllerBackend.init();
|
||||
mockControllerBackend.onControllersChanged((controllers) => {
|
||||
set((state) => ({
|
||||
controllers,
|
||||
activeControllerId: controllers.some((c) => c.id === state.activeControllerId)
|
||||
? state.activeControllerId
|
||||
: controllers[0]?.id ?? null,
|
||||
}));
|
||||
});
|
||||
let realControllers: ControllerInfo[];
|
||||
try {
|
||||
realControllers = await tauriControllerService.list();
|
||||
} catch {
|
||||
// The backend is unreachable only when running in a plain browser
|
||||
// (no Tauri). Fall back to the clearly-labeled simulated controller so
|
||||
// frontend development stays possible.
|
||||
await startSimulation("Native backend unavailable; using simulated input.");
|
||||
return;
|
||||
}
|
||||
|
||||
mockControllerBackend.onLiveState(receiveLiveState);
|
||||
mockControllerBackend.onRawEvent(receiveRawEvent);
|
||||
const refreshVirtualControllerActive = async () => {
|
||||
try {
|
||||
set({ virtualControllerActive: await tauriControllerService.virtualControllerActive() });
|
||||
} catch {
|
||||
set({ virtualControllerActive: false });
|
||||
}
|
||||
};
|
||||
|
||||
const refreshNativeDevices = async (): Promise<boolean> => {
|
||||
if (refreshInFlight) return false;
|
||||
refreshInFlight = true;
|
||||
try {
|
||||
const previousActive = get().activeController();
|
||||
const devices = await tauriControllerService.list();
|
||||
const nextActive =
|
||||
devices.find((controller) => controller.id === previousActive?.id) ??
|
||||
(previousActive
|
||||
? devices.find((controller) => samePhysicalController(controller, previousActive))
|
||||
: undefined) ??
|
||||
devices[0];
|
||||
|
||||
if (!nextActive) {
|
||||
set((state) => ({
|
||||
controllers: [],
|
||||
activeControllerId: null,
|
||||
liveState: null,
|
||||
pollRateHz: null,
|
||||
latencyEstimateMs: null,
|
||||
connectionStatus: state.connectionStatus === "reconnecting" ? "reconnecting" : "disconnected",
|
||||
connectionMessage:
|
||||
state.connectionStatus === "reconnecting"
|
||||
? "Waiting for the controller to reconnect…"
|
||||
: "No controller detected.",
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
set({
|
||||
controllers: devices,
|
||||
activeControllerId: nextActive.id,
|
||||
connectionStatus: "connected",
|
||||
connectionMessage: null,
|
||||
});
|
||||
await tauriControllerService.startReader(nextActive.devicePath);
|
||||
return true;
|
||||
} catch {
|
||||
set({
|
||||
connectionStatus: "reconnecting",
|
||||
connectionMessage: "Unable to access the controller; retrying…",
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
refreshInFlight = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Polls until a controller is present. Runs indefinitely so a controller
|
||||
* plugged in at any time is picked up without restarting the app.
|
||||
*/
|
||||
const scheduleRescan = (): void => {
|
||||
if (rescanTimer !== null) return;
|
||||
const attempt = async (): Promise<void> => {
|
||||
rescanTimer = null;
|
||||
if (await refreshNativeDevices()) return;
|
||||
rescanTimer = setTimeout(() => void attempt(), RESCAN_INTERVAL_MS);
|
||||
};
|
||||
rescanTimer = setTimeout(() => void attempt(), RESCAN_INTERVAL_MS);
|
||||
};
|
||||
|
||||
await tauriControllerService.onInput(receiveRawEvent);
|
||||
await tauriControllerService.onReaderStatus((status) => {
|
||||
const activeController = get().activeController();
|
||||
if (activeController?.devicePath !== status.devicePath) return;
|
||||
if (status.connected) {
|
||||
clearRescanTimer();
|
||||
set({ connectionStatus: "connected", connectionMessage: null });
|
||||
return;
|
||||
}
|
||||
set({
|
||||
liveState: null,
|
||||
pollRateHz: null,
|
||||
latencyEstimateMs: null,
|
||||
connectionStatus: "reconnecting",
|
||||
connectionMessage: status.message ?? "Controller disconnected; searching for it again…",
|
||||
});
|
||||
scheduleRescan();
|
||||
});
|
||||
await refreshVirtualControllerActive();
|
||||
|
||||
const active = realControllers[0];
|
||||
if (active) {
|
||||
set({
|
||||
controllers: realControllers,
|
||||
activeControllerId: active.id,
|
||||
usingSimulation: false,
|
||||
connectionStatus: "connected",
|
||||
connectionMessage: null,
|
||||
});
|
||||
await tauriControllerService.startReader(active.devicePath);
|
||||
} else {
|
||||
set({
|
||||
controllers: [],
|
||||
activeControllerId: null,
|
||||
usingSimulation: false,
|
||||
connectionStatus: "disconnected",
|
||||
connectionMessage: "No controller detected.",
|
||||
});
|
||||
scheduleRescan();
|
||||
}
|
||||
},
|
||||
|
||||
activeController: () => {
|
||||
@@ -263,14 +273,16 @@ export const useControllerStore = create<ControllerState>((set, get) => ({
|
||||
},
|
||||
|
||||
setRemappingEnabled: async (enabled) => {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H8',location:'src/stores/controllerStore.ts:setRemappingEnabled',message:'Remapping toggle requested',data:{enabled,usingSimulation:get().usingSimulation,activeControllerId:get().activeControllerId},timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
if (get().usingSimulation) {
|
||||
await mockControllerBackend.setRemappingEnabled(enabled);
|
||||
return;
|
||||
}
|
||||
await tauriControllerService.setRemappingEnabled(enabled);
|
||||
try {
|
||||
set({ virtualControllerActive: await tauriControllerService.virtualControllerActive() });
|
||||
} catch {
|
||||
set({ virtualControllerActive: false });
|
||||
}
|
||||
},
|
||||
|
||||
clearEventLog: () => set({ eventLog: [] }),
|
||||
|
||||
@@ -75,9 +75,6 @@ export const useProfileStore = create<ProfileState>((set, get) => ({
|
||||
const nextDraft = updater(deepClone(draft));
|
||||
const isDirty = !isEqual(nextDraft, get().activeProfile);
|
||||
set({ draftProfile: nextDraft, isDirty });
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H1',location:'src/stores/profileStore.ts:updateDraft',message:'Profile draft updated',data:{profileId:nextDraft.id,isDirty,leftTrigger:nextDraft.leftTrigger,rightTrigger:nextDraft.rightTrigger},timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
},
|
||||
|
||||
applyDraft: async () => {
|
||||
@@ -90,9 +87,6 @@ export const useProfileStore = create<ProfileState>((set, get) => ({
|
||||
useUIStore.getState().pushToast(`Could not apply remapping: ${err}`, "danger");
|
||||
return;
|
||||
}
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'post-fix',hypothesisId:'H7',location:'src/stores/profileStore.ts:applyDraft',message:'Profile successfully applied to native remapping pipeline',data:{profileId:draft.id},timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
},
|
||||
|
||||
revertDraft: () => {
|
||||
@@ -111,9 +105,6 @@ export const useProfileStore = create<ProfileState>((set, get) => ({
|
||||
set({ activeProfile: saved, draftProfile: deepClone(saved), isDirty: false, loading: false });
|
||||
await get().refreshList();
|
||||
useUIStore.getState().pushToast(`Saved profile "${saved.name}".`, "success");
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H7',location:'src/stores/profileStore.ts:saveDraft',message:'Profile draft saved',data:{profileId:saved.id},timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
} catch (err) {
|
||||
set({ loading: false, error: String(err) });
|
||||
useUIStore.getState().pushToast(`Failed to save profile: ${err}`, "danger");
|
||||
|
||||
@@ -22,12 +22,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
remappingEnabled: true,
|
||||
setRemappingEnabled: (remappingEnabled) => set({ remappingEnabled }),
|
||||
simulationMode: false,
|
||||
setSimulationMode: (simulationMode) => {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H5',location:'src/stores/settingsStore.ts:setSimulationMode',message:'Simulation mode changed',data:{simulationMode},timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
set({ simulationMode });
|
||||
},
|
||||
setSimulationMode: (simulationMode) => set({ simulationMode }),
|
||||
autoSwitchProfiles: false,
|
||||
setAutoSwitchProfiles: (autoSwitchProfiles) => set({ autoSwitchProfiles }),
|
||||
}),
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
* `mockControllerBackend` provides a realistic example.
|
||||
*/
|
||||
|
||||
export type ConnectionKind = "usb" | "bluetooth" | "receiver2_4ghz" | "unknown";
|
||||
|
||||
export type ControllerMode = "xinput" | "directinput" | "switch" | "unknown";
|
||||
export type ConnectionKind = "usb" | "bluetooth" | "unknown";
|
||||
|
||||
export interface ButtonCapability {
|
||||
id: string;
|
||||
@@ -45,10 +43,8 @@ export interface ControllerInfo {
|
||||
devicePath: string;
|
||||
vendorId: number;
|
||||
productId: number;
|
||||
mode: ControllerMode;
|
||||
/** Kernel-reported battery percentage; null when the device reports none. */
|
||||
batteryPercent: number | null;
|
||||
firmwareVersion: string | null;
|
||||
virtualControllerActive: boolean;
|
||||
capabilities: DeviceCapabilities;
|
||||
isSimulated: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user