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.
- package/README.md +251 -0
- package/dist/core/device-interface.d.ts +121 -0
- package/dist/core/device-interface.js +25 -0
- package/dist/core/device-manager.d.ts +74 -0
- package/dist/core/device-manager.js +211 -0
- package/dist/core/events.d.ts +32 -0
- package/dist/core/events.js +70 -0
- package/dist/devices/buttplug/buttplug-api.d.ts +104 -0
- package/dist/devices/buttplug/buttplug-api.js +459 -0
- package/dist/devices/buttplug/buttplug-device.d.ts +85 -0
- package/dist/devices/buttplug/buttplug-device.js +511 -0
- package/dist/devices/buttplug/buttplug-server.d.ts +13 -0
- package/dist/devices/buttplug/buttplug-server.js +27 -0
- package/dist/devices/buttplug/command-helpers.d.ts +23 -0
- package/dist/devices/buttplug/command-helpers.js +95 -0
- package/dist/devices/buttplug/types.d.ts +59 -0
- package/dist/devices/buttplug/types.js +20 -0
- package/dist/devices/handy/handy-api.d.ts +113 -0
- package/dist/devices/handy/handy-api.js +361 -0
- package/dist/devices/handy/handy-device.d.ts +94 -0
- package/dist/devices/handy/handy-device.js +413 -0
- package/dist/devices/handy/types.d.ts +87 -0
- package/dist/devices/handy/types.js +2 -0
- package/dist/devices/index.d.ts +11 -0
- package/dist/devices/index.js +29 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +33 -0
- package/package.json +53 -0
|
@@ -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
|
+
};
|