Initial implementation of Bluetooth toy control app
Next.js app with a browser-side buttplug/buttplug-wasm control layer (server never touches real-time device commands), SQLite storage via Drizzle, single-secret auth, recordings/replay with device remapping, a usage stats dashboard, Docker deployment, and Gitea CI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W8WkFF5ppURBAB918593Eb
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import type { ButtplugClient, ButtplugClientDevice } from "buttplug";
|
||||
import { deriveActuators, type ButtplugRuntime } from "./commands";
|
||||
import { useButtplugStore } from "./store";
|
||||
import type { ConnectedDeviceInfo } from "./types";
|
||||
|
||||
export function isWebBluetoothSupported(): boolean {
|
||||
return typeof navigator !== "undefined" && "bluetooth" in navigator;
|
||||
}
|
||||
|
||||
interface ClientHandle {
|
||||
client: ButtplugClient;
|
||||
runtime: ButtplugRuntime;
|
||||
}
|
||||
|
||||
let clientPromise: Promise<ClientHandle> | null = null;
|
||||
|
||||
function toDeviceInfo(device: ButtplugClientDevice): ConnectedDeviceInfo {
|
||||
return {
|
||||
index: device.index,
|
||||
name: device.name,
|
||||
displayName: device.displayName,
|
||||
actuators: deriveActuators(device),
|
||||
};
|
||||
}
|
||||
|
||||
async function initClient(): Promise<ClientHandle> {
|
||||
const [{ ButtplugClient, DeviceOutput, OutputType }, { ButtplugWasmClientConnector }] = await Promise.all([
|
||||
import("buttplug"),
|
||||
import("buttplug-wasm"),
|
||||
]);
|
||||
|
||||
const client = new ButtplugClient("sexy");
|
||||
|
||||
client.on("deviceadded", (device: ButtplugClientDevice) => {
|
||||
useButtplugStore.getState().upsertDevice(toDeviceInfo(device));
|
||||
});
|
||||
client.on("deviceremoved", (device: ButtplugClientDevice) => {
|
||||
useButtplugStore.getState().removeDevice(device.index);
|
||||
});
|
||||
client.on("scanningfinished", () => {
|
||||
useButtplugStore.getState().setScanning(false);
|
||||
});
|
||||
client.on("disconnect", () => {
|
||||
useButtplugStore.getState().reset();
|
||||
});
|
||||
|
||||
const connector = new ButtplugWasmClientConnector();
|
||||
await client.connect(connector);
|
||||
|
||||
useButtplugStore.getState().setConnected(true);
|
||||
for (const device of client.devices.values()) {
|
||||
useButtplugStore.getState().upsertDevice(toDeviceInfo(device));
|
||||
}
|
||||
|
||||
return { client, runtime: { DeviceOutput, OutputType } };
|
||||
}
|
||||
|
||||
/** Lazily creates and connects the singleton Buttplug client. Browser-only. */
|
||||
export function getButtplugClientHandle(): Promise<ClientHandle> {
|
||||
if (typeof window === "undefined") {
|
||||
return Promise.reject(new Error("Buttplug client is browser-only"));
|
||||
}
|
||||
if (!clientPromise) {
|
||||
clientPromise = initClient().catch((err) => {
|
||||
clientPromise = null;
|
||||
useButtplugStore.getState().setError(err instanceof Error ? err.message : String(err));
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return clientPromise;
|
||||
}
|
||||
|
||||
export async function startScanning(): Promise<void> {
|
||||
const { client } = await getButtplugClientHandle();
|
||||
useButtplugStore.getState().setScanning(true);
|
||||
await client.startScanning();
|
||||
}
|
||||
|
||||
export async function stopScanning(): Promise<void> {
|
||||
const { client } = await getButtplugClientHandle();
|
||||
await client.stopScanning();
|
||||
useButtplugStore.getState().setScanning(false);
|
||||
}
|
||||
|
||||
export async function disconnectAll(): Promise<void> {
|
||||
if (!clientPromise) return;
|
||||
const { client } = await clientPromise;
|
||||
clientPromise = null;
|
||||
await client.disconnect();
|
||||
useButtplugStore.getState().reset();
|
||||
}
|
||||
|
||||
export async function getDevice(deviceIndex: number): Promise<ButtplugClientDevice | undefined> {
|
||||
const { client } = await getButtplugClientHandle();
|
||||
return client.devices.get(deviceIndex);
|
||||
}
|
||||
Reference in New Issue
Block a user