ive-connect 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Buttplug Device Implementation
3
+ *
4
+ * Implements the HapticDevice interface for Buttplug devices
5
+ */
6
+ import { DeviceCapability, DeviceInfo, HapticDevice, ScriptData } from "../../core/device-interface";
7
+ import { EventEmitter } from "../../core/events";
8
+ import { ButtplugSettings } from "./types";
9
+ /**
10
+ * Buttplug device implementation
11
+ */
12
+ export declare class ButtplugDevice extends EventEmitter implements HapticDevice {
13
+ private _api;
14
+ private _config;
15
+ private _connectionState;
16
+ private _isPlaying;
17
+ private _loadedScript;
18
+ private _currentScriptActions;
19
+ private _lastActionIndex;
20
+ private _playbackInterval;
21
+ private _playbackStartTime;
22
+ private _playbackRate;
23
+ private _loopPlayback;
24
+ readonly id: string;
25
+ readonly name: string;
26
+ readonly type: string;
27
+ readonly capabilities: DeviceCapability[];
28
+ constructor(config?: Partial<ButtplugSettings>);
29
+ /**
30
+ * Get connected state
31
+ */
32
+ get isConnected(): boolean;
33
+ /**
34
+ * Get playing state
35
+ */
36
+ get isPlaying(): boolean;
37
+ /**
38
+ * Connect to Buttplug server
39
+ */
40
+ connect(config?: Partial<ButtplugSettings>): Promise<boolean>;
41
+ /**
42
+ * Disconnect from the server
43
+ */
44
+ disconnect(): Promise<boolean>;
45
+ /**
46
+ * Get current configuration
47
+ */
48
+ getConfig(): ButtplugSettings;
49
+ /**
50
+ * Update configuration
51
+ */
52
+ updateConfig(config: Partial<ButtplugSettings>): Promise<boolean>;
53
+ /**
54
+ * Load a script for playback
55
+ */
56
+ loadScript(scriptData: ScriptData): Promise<boolean>;
57
+ /**
58
+ * Play the loaded script
59
+ */
60
+ play(timeMs: number, playbackRate?: number, loop?: boolean): Promise<boolean>;
61
+ /**
62
+ * Stop playback
63
+ */
64
+ stop(): Promise<boolean>;
65
+ /**
66
+ * Sync playback time
67
+ */
68
+ syncTime(timeMs: number): Promise<boolean>;
69
+ /**
70
+ * Get device information
71
+ */
72
+ getDeviceInfo(): DeviceInfo | null;
73
+ /**
74
+ * Process script actions based on current time
75
+ */
76
+ private _processActions;
77
+ /**
78
+ * Find the action index for the given time
79
+ */
80
+ private _findActionIndexForTime;
81
+ /**
82
+ * Set up event handlers for the Buttplug API
83
+ */
84
+ private _setupApiEventHandlers;
85
+ }
@@ -0,0 +1,511 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ButtplugDevice = void 0;
4
+ /**
5
+ * Buttplug Device Implementation
6
+ *
7
+ * Implements the HapticDevice interface for Buttplug devices
8
+ */
9
+ const device_interface_1 = require("../../core/device-interface");
10
+ const events_1 = require("../../core/events");
11
+ const buttplug_api_1 = require("./buttplug-api");
12
+ const types_1 = require("./types");
13
+ const buttplug_server_1 = require("./buttplug-server");
14
+ const command_helpers_1 = require("./command-helpers");
15
+ /**
16
+ * Default Buttplug configuration
17
+ */
18
+ const DEFAULT_CONFIG = {
19
+ id: "buttplug",
20
+ name: "Buttplug Devices",
21
+ enabled: true,
22
+ connectionType: types_1.ButtplugConnectionType.LOCAL,
23
+ clientName: (0, buttplug_server_1.generateClientName)(),
24
+ allowedFeatures: {
25
+ vibrate: true,
26
+ rotate: true,
27
+ linear: true,
28
+ },
29
+ devicePreferences: {},
30
+ };
31
+ /**
32
+ * Buttplug device implementation
33
+ */
34
+ class ButtplugDevice extends events_1.EventEmitter {
35
+ constructor(config) {
36
+ super();
37
+ this._connectionState = device_interface_1.ConnectionState.DISCONNECTED;
38
+ this._isPlaying = false;
39
+ this._loadedScript = null;
40
+ this._currentScriptActions = [];
41
+ this._lastActionIndex = -1;
42
+ this._playbackInterval = null;
43
+ this._playbackStartTime = 0;
44
+ this._playbackRate = 1.0;
45
+ this._loopPlayback = false;
46
+ this.id = "buttplug";
47
+ this.name = "Buttplug Devices";
48
+ this.type = "buttplug";
49
+ this.capabilities = [
50
+ device_interface_1.DeviceCapability.VIBRATE,
51
+ device_interface_1.DeviceCapability.ROTATE,
52
+ device_interface_1.DeviceCapability.LINEAR,
53
+ ];
54
+ this._config = { ...DEFAULT_CONFIG };
55
+ if (config) {
56
+ Object.assign(this._config, config);
57
+ }
58
+ this._api = new buttplug_api_1.ButtplugApi(this._config.clientName);
59
+ this._setupApiEventHandlers();
60
+ }
61
+ /**
62
+ * Get connected state
63
+ */
64
+ get isConnected() {
65
+ return this._connectionState === device_interface_1.ConnectionState.CONNECTED;
66
+ }
67
+ /**
68
+ * Get playing state
69
+ */
70
+ get isPlaying() {
71
+ return this._isPlaying;
72
+ }
73
+ /**
74
+ * Connect to Buttplug server
75
+ */
76
+ async connect(config) {
77
+ try {
78
+ // Update config if provided
79
+ if (config) {
80
+ await this.updateConfig(config);
81
+ }
82
+ // Check if WebBluetooth is supported for local connections
83
+ if (this._config.connectionType === types_1.ButtplugConnectionType.LOCAL &&
84
+ !(0, buttplug_server_1.isWebBluetoothSupported)()) {
85
+ this.emit("error", "WebBluetooth is not supported in this browser or device");
86
+ return false;
87
+ }
88
+ // Update connection state
89
+ this._connectionState = device_interface_1.ConnectionState.CONNECTING;
90
+ this.emit("connectionStateChanged", this._connectionState);
91
+ // Connect to the server
92
+ const success = await this._api.connect(this._config.connectionType, this._config.serverUrl);
93
+ if (success) {
94
+ this._connectionState = device_interface_1.ConnectionState.CONNECTED;
95
+ this.emit("connectionStateChanged", this._connectionState);
96
+ this.emit("connected", this.getDeviceInfo());
97
+ // Start scanning for devices automatically
98
+ this._api.startScanning().catch((error) => {
99
+ console.error("Error starting device scan:", error);
100
+ });
101
+ return true;
102
+ }
103
+ else {
104
+ this._connectionState = device_interface_1.ConnectionState.DISCONNECTED;
105
+ this.emit("connectionStateChanged", this._connectionState);
106
+ this.emit("error", "Failed to connect to Buttplug server");
107
+ return false;
108
+ }
109
+ }
110
+ catch (error) {
111
+ console.error("Buttplug: Error connecting to server:", error);
112
+ this._connectionState = device_interface_1.ConnectionState.DISCONNECTED;
113
+ this.emit("connectionStateChanged", this._connectionState);
114
+ this.emit("error", `Connection error: ${error instanceof Error ? error.message : String(error)}`);
115
+ return false;
116
+ }
117
+ }
118
+ /**
119
+ * Disconnect from the server
120
+ */
121
+ async disconnect() {
122
+ try {
123
+ // Stop playback if active
124
+ if (this._isPlaying) {
125
+ await this.stop();
126
+ }
127
+ // Disconnect from server
128
+ await this._api.disconnect();
129
+ // Update state
130
+ this._connectionState = device_interface_1.ConnectionState.DISCONNECTED;
131
+ this.emit("connectionStateChanged", this._connectionState);
132
+ this.emit("disconnected");
133
+ return true;
134
+ }
135
+ catch (error) {
136
+ console.error("Buttplug: Error disconnecting:", error);
137
+ this._connectionState = device_interface_1.ConnectionState.DISCONNECTED;
138
+ this.emit("connectionStateChanged", this._connectionState);
139
+ this.emit("disconnected");
140
+ return true; // Return true anyway for better UX
141
+ }
142
+ }
143
+ /**
144
+ * Get current configuration
145
+ */
146
+ getConfig() {
147
+ return { ...this._config };
148
+ }
149
+ /**
150
+ * Update configuration
151
+ */
152
+ async updateConfig(config) {
153
+ // Update local config
154
+ if (config.connectionType !== undefined) {
155
+ this._config.connectionType = config.connectionType;
156
+ }
157
+ if (config.serverUrl !== undefined) {
158
+ this._config.serverUrl = config.serverUrl;
159
+ }
160
+ if (config.clientName !== undefined) {
161
+ this._config.clientName = config.clientName;
162
+ }
163
+ if (config.allowedFeatures !== undefined) {
164
+ this._config.allowedFeatures = {
165
+ ...this._config.allowedFeatures,
166
+ ...config.allowedFeatures,
167
+ };
168
+ }
169
+ if (config.devicePreferences !== undefined) {
170
+ // Update device preferences in the API
171
+ for (const [index, prefs] of Object.entries(config.devicePreferences)) {
172
+ const deviceIndex = Number(index);
173
+ this._api.setDevicePreference(deviceIndex, prefs);
174
+ }
175
+ // Update local config
176
+ this._config.devicePreferences = {
177
+ ...this._config.devicePreferences,
178
+ ...config.devicePreferences,
179
+ };
180
+ }
181
+ // Update other fields if present
182
+ if (config.name !== undefined) {
183
+ this._config.name = config.name;
184
+ }
185
+ if (config.enabled !== undefined) {
186
+ this._config.enabled = config.enabled;
187
+ }
188
+ // Emit configuration changed event
189
+ this.emit("configChanged", this._config);
190
+ return true;
191
+ }
192
+ /**
193
+ * Load a script for playback
194
+ */
195
+ async loadScript(scriptData) {
196
+ var _a;
197
+ if (!this.isConnected) {
198
+ this.emit("error", "Cannot load script: Not connected to a server");
199
+ return false;
200
+ }
201
+ try {
202
+ // Parse script data
203
+ let scriptContent;
204
+ if (scriptData.content) {
205
+ // If content is directly provided
206
+ scriptContent = scriptData.content;
207
+ }
208
+ else if (scriptData.url) {
209
+ // If URL is provided, fetch the script
210
+ try {
211
+ console.log(`[BUTTPLUG-SCRIPT] Fetching script from URL: ${scriptData.url}`);
212
+ const response = await fetch(scriptData.url);
213
+ if (!response.ok) {
214
+ throw new Error(`Failed to fetch script: ${response.status} ${response.statusText}`);
215
+ }
216
+ scriptContent = await response.json();
217
+ console.log(`[BUTTPLUG-SCRIPT] Script loaded successfully, actions:`, (_a = scriptContent.actions) === null || _a === void 0 ? void 0 : _a.length);
218
+ }
219
+ catch (error) {
220
+ this.emit("error", `Failed to fetch script: ${error instanceof Error ? error.message : String(error)}`);
221
+ return false;
222
+ }
223
+ }
224
+ else {
225
+ this.emit("error", "Invalid script data: Either URL or content must be provided");
226
+ return false;
227
+ }
228
+ // Validate script format (basic checks for funscript)
229
+ if (!scriptContent ||
230
+ !scriptContent.actions ||
231
+ !Array.isArray(scriptContent.actions)) {
232
+ this.emit("error", "Invalid script format: Missing actions array");
233
+ console.error("[BUTTPLUG-SCRIPT] Invalid script format:", scriptContent);
234
+ return false;
235
+ }
236
+ // Sort actions by timestamp
237
+ const actions = [...scriptContent.actions].sort((a, b) => a.at - b.at);
238
+ // Store the script and actions
239
+ this._loadedScript = scriptContent;
240
+ this._currentScriptActions = actions;
241
+ this._lastActionIndex = -1;
242
+ this.emit("scriptLoaded", {
243
+ type: scriptData.type || "funscript",
244
+ name: scriptContent.name || "Unnamed Script",
245
+ actions: this._currentScriptActions.length,
246
+ });
247
+ return true;
248
+ }
249
+ catch (error) {
250
+ console.error("Buttplug: Error loading script:", error);
251
+ this.emit("error", `Script loading error: ${error instanceof Error ? error.message : String(error)}`);
252
+ return false;
253
+ }
254
+ }
255
+ /**
256
+ * Play the loaded script
257
+ */
258
+ async play(timeMs, playbackRate = 1.0, loop = false) {
259
+ if (!this.isConnected) {
260
+ this.emit("error", "Cannot play: Not connected to a server");
261
+ return false;
262
+ }
263
+ if (!this._loadedScript || !this._currentScriptActions.length) {
264
+ this.emit("error", "Cannot play: No script loaded");
265
+ return false;
266
+ }
267
+ try {
268
+ // Stop any existing playback
269
+ if (this._isPlaying) {
270
+ await this.stop();
271
+ }
272
+ // Set playback parameters
273
+ this._playbackStartTime = Date.now() - timeMs;
274
+ this._playbackRate = playbackRate;
275
+ this._loopPlayback = loop;
276
+ this._lastActionIndex = -1;
277
+ // Create command executor for all devices
278
+ const devices = this._api.getDevices();
279
+ const preferences = this._api.getDevicePreferences();
280
+ const executor = (0, command_helpers_1.createMultiDeviceCommandExecutor)(this._api, devices, preferences);
281
+ // Start playback
282
+ this._isPlaying = true;
283
+ // Create an interval to check for actions
284
+ this._playbackInterval = setInterval(() => {
285
+ this._processActions(executor);
286
+ }, 20); // Check every 20ms for smoother playback
287
+ this.emit("playbackStateChanged", {
288
+ isPlaying: this._isPlaying,
289
+ timeMs,
290
+ playbackRate,
291
+ loop,
292
+ });
293
+ return true;
294
+ }
295
+ catch (error) {
296
+ console.error("Buttplug: Error starting playback:", error);
297
+ this._isPlaying = false;
298
+ this.emit("error", `Playback error: ${error instanceof Error ? error.message : String(error)}`);
299
+ this.emit("playbackStateChanged", { isPlaying: false });
300
+ return false;
301
+ }
302
+ }
303
+ /**
304
+ * Stop playback
305
+ */
306
+ async stop() {
307
+ if (!this.isConnected) {
308
+ this.emit("error", "Cannot stop: Not connected to a server");
309
+ return false;
310
+ }
311
+ try {
312
+ // Clear playback interval
313
+ if (this._playbackInterval !== null) {
314
+ clearInterval(this._playbackInterval);
315
+ this._playbackInterval = null;
316
+ }
317
+ // Stop all devices
318
+ await this._api.stopAllDevices();
319
+ // Update playback state
320
+ this._isPlaying = false;
321
+ this._lastActionIndex = -1;
322
+ this.emit("playbackStateChanged", { isPlaying: false });
323
+ return true;
324
+ }
325
+ catch (error) {
326
+ console.error("Buttplug: Error stopping playback:", error);
327
+ this._isPlaying = false;
328
+ this.emit("error", `Stopping playback error: ${error instanceof Error ? error.message : String(error)}`);
329
+ this.emit("playbackStateChanged", { isPlaying: false });
330
+ return false;
331
+ }
332
+ }
333
+ /**
334
+ * Sync playback time
335
+ */
336
+ async syncTime(timeMs) {
337
+ if (!this.isConnected || !this._isPlaying) {
338
+ return false;
339
+ }
340
+ try {
341
+ // Update the playback start time based on the current time
342
+ this._playbackStartTime = Date.now() - timeMs;
343
+ return true;
344
+ }
345
+ catch (error) {
346
+ console.error("Buttplug: Error syncing time:", error);
347
+ return false;
348
+ }
349
+ }
350
+ /**
351
+ * Get device information
352
+ */
353
+ getDeviceInfo() {
354
+ // Get connected devices
355
+ const devices = this._api.getDevices();
356
+ if (devices.length === 0) {
357
+ return {
358
+ id: this.id,
359
+ name: this.name,
360
+ type: this.type,
361
+ deviceCount: 0,
362
+ devices: [],
363
+ };
364
+ }
365
+ return {
366
+ id: this.id,
367
+ name: this.name,
368
+ type: this.type,
369
+ deviceCount: devices.length,
370
+ devices: devices.map((device) => ({
371
+ index: device.index,
372
+ name: device.name,
373
+ features: [
374
+ device.canVibrate ? "vibrate" : null,
375
+ device.canRotate ? "rotate" : null,
376
+ device.canLinear ? "linear" : null,
377
+ ].filter(Boolean),
378
+ })),
379
+ };
380
+ }
381
+ /**
382
+ * Process script actions based on current time
383
+ */
384
+ _processActions(executor) {
385
+ if (!this._isPlaying || !this._currentScriptActions.length) {
386
+ return;
387
+ }
388
+ // Calculate current time in the script
389
+ const currentTime = Date.now();
390
+ const elapsedMs = (currentTime - this._playbackStartTime) * this._playbackRate;
391
+ // Find the action for the current time
392
+ const actionIndex = this._findActionIndexForTime(elapsedMs);
393
+ // If we reached the end of the script
394
+ if (actionIndex === this._currentScriptActions.length - 1 &&
395
+ elapsedMs > this._currentScriptActions[actionIndex].at + 1000) {
396
+ if (this._loopPlayback) {
397
+ // Reset for loop playback
398
+ this._playbackStartTime = Date.now();
399
+ this._lastActionIndex = -1;
400
+ return;
401
+ }
402
+ else {
403
+ // We're past the end of the script, stop playback
404
+ this.stop().catch(console.error);
405
+ return;
406
+ }
407
+ }
408
+ // If we have a new action to execute
409
+ if (actionIndex !== this._lastActionIndex && actionIndex >= 0) {
410
+ const action = this._currentScriptActions[actionIndex];
411
+ const prevAction = actionIndex > 0
412
+ ? this._currentScriptActions[actionIndex - 1]
413
+ : { pos: 0 };
414
+ // Calculate duration for linear movement based on time to next action
415
+ let durationMs = 500; // Default duration if we can't determine
416
+ if (actionIndex < this._currentScriptActions.length - 1) {
417
+ const nextAction = this._currentScriptActions[actionIndex + 1];
418
+ durationMs = nextAction.at - action.at;
419
+ // Enforce a minimum duration to prevent erratic movement
420
+ durationMs = Math.max(100, durationMs);
421
+ }
422
+ // Execute the action on all devices
423
+ executor
424
+ .executeAction(action.pos, prevAction.pos, durationMs)
425
+ .catch((error) => {
426
+ console.error("Error executing action:", error);
427
+ });
428
+ this._lastActionIndex = actionIndex;
429
+ }
430
+ }
431
+ /**
432
+ * Find the action index for the given time
433
+ */
434
+ _findActionIndexForTime(timeMs) {
435
+ if (!this._currentScriptActions.length) {
436
+ return -1;
437
+ }
438
+ // If we're past the end of the script
439
+ if (timeMs >
440
+ this._currentScriptActions[this._currentScriptActions.length - 1].at) {
441
+ return this._currentScriptActions.length - 1;
442
+ }
443
+ // If we're before the beginning of the script
444
+ if (timeMs < this._currentScriptActions[0].at) {
445
+ return 0;
446
+ }
447
+ // Binary search for the action
448
+ let low = 0;
449
+ let high = this._currentScriptActions.length - 1;
450
+ let bestIndex = -1;
451
+ while (low <= high) {
452
+ const mid = Math.floor((low + high) / 2);
453
+ if (this._currentScriptActions[mid].at <= timeMs) {
454
+ bestIndex = mid;
455
+ low = mid + 1;
456
+ }
457
+ else {
458
+ high = mid - 1;
459
+ }
460
+ }
461
+ return bestIndex;
462
+ }
463
+ /**
464
+ * Set up event handlers for the Buttplug API
465
+ */
466
+ _setupApiEventHandlers() {
467
+ // Forward connection state changes
468
+ this._api.on("connectionStateChanged", (state) => {
469
+ let connectionState;
470
+ switch (state) {
471
+ case types_1.ButtplugConnectionState.CONNECTED:
472
+ connectionState = device_interface_1.ConnectionState.CONNECTED;
473
+ break;
474
+ case types_1.ButtplugConnectionState.CONNECTING:
475
+ connectionState = device_interface_1.ConnectionState.CONNECTING;
476
+ break;
477
+ default:
478
+ connectionState = device_interface_1.ConnectionState.DISCONNECTED;
479
+ break;
480
+ }
481
+ this._connectionState = connectionState;
482
+ this.emit("connectionStateChanged", connectionState);
483
+ });
484
+ // Forward device events
485
+ this._api.on("deviceAdded", (deviceInfo) => {
486
+ this.emit("deviceAdded", deviceInfo);
487
+ });
488
+ this._api.on("deviceRemoved", (deviceInfo) => {
489
+ this.emit("deviceRemoved", deviceInfo);
490
+ });
491
+ // Forward errors
492
+ this._api.on("error", (error) => {
493
+ this.emit("error", error);
494
+ });
495
+ // Forward scanning state changes
496
+ this._api.on("scanningChanged", (scanning) => {
497
+ this.emit("scanningChanged", scanning);
498
+ });
499
+ // Forward device preference changes
500
+ this._api.on("devicePreferenceChanged", (data) => {
501
+ this.emit("devicePreferenceChanged", data);
502
+ // Update local config
503
+ if (!this._config.devicePreferences) {
504
+ this._config.devicePreferences = {};
505
+ }
506
+ this._config.devicePreferences[data.deviceIndex] = data.preference;
507
+ this.emit("configChanged", this._config);
508
+ });
509
+ }
510
+ }
511
+ exports.ButtplugDevice = ButtplugDevice;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Buttplug Local Server
3
+ *
4
+ * Provides support for running a Buttplug server locally in the browser
5
+ */
6
+ /**
7
+ * Check if WebBluetooth is supported in the current environment
8
+ */
9
+ export declare function isWebBluetoothSupported(): boolean;
10
+ /**
11
+ * Generate name for the client
12
+ */
13
+ export declare function generateClientName(prefix?: string): string;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * Buttplug Local Server
4
+ *
5
+ * Provides support for running a Buttplug server locally in the browser
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.isWebBluetoothSupported = isWebBluetoothSupported;
9
+ exports.generateClientName = generateClientName;
10
+ // This module provides functionality to create a local WebBluetooth connector
11
+ // The actual import of ButtplugWasmClientConnector is done dynamically in
12
+ // the ButtplugApi class to ensure it only runs in browser environments
13
+ /**
14
+ * Check if WebBluetooth is supported in the current environment
15
+ */
16
+ function isWebBluetoothSupported() {
17
+ return (typeof window !== "undefined" &&
18
+ typeof window.navigator !== "undefined" &&
19
+ navigator.bluetooth !== undefined);
20
+ }
21
+ /**
22
+ * Generate name for the client
23
+ */
24
+ function generateClientName(prefix = "IVE-Connect") {
25
+ // Add a random suffix to make the client name unique
26
+ return `${prefix}-${Math.floor(Math.random() * 10000)}`;
27
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Command Helpers for Buttplug Devices
3
+ *
4
+ * Utility functions for converting script commands to Buttplug device commands
5
+ */
6
+ import { ButtplugApi } from "./buttplug-api";
7
+ import { ButtplugDeviceInfo, DevicePreference } from "./types";
8
+ /**
9
+ * Convert script position (0-100) to device position (0.0-1.0)
10
+ */
11
+ export declare function convertScriptPositionToDevicePosition(scriptPos: number, min?: number, max?: number, invert?: boolean): number;
12
+ /**
13
+ * Create a command executor for a specific device
14
+ */
15
+ export declare function createDeviceCommandExecutor(api: ButtplugApi, deviceInfo: ButtplugDeviceInfo, preferences: DevicePreference): {
16
+ executeAction: (pos: number, prevPos: number, durationMs: number) => Promise<void>;
17
+ };
18
+ /**
19
+ * Create a command executor for multiple devices
20
+ */
21
+ export declare function createMultiDeviceCommandExecutor(api: ButtplugApi, devices: ButtplugDeviceInfo[], preferences: Map<number, DevicePreference>): {
22
+ executeAction: (pos: number, prevPos: number, durationMs: number) => Promise<void>;
23
+ };