ive-connect 0.6.0 → 1.0.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.
@@ -16,6 +16,7 @@ export declare enum DeviceCapability {
16
16
  VIBRATE = "vibrate",
17
17
  ROTATE = "rotate",
18
18
  LINEAR = "linear",
19
+ OSCILLATE = "oscillate",
19
20
  STROKE = "stroke"
20
21
  }
21
22
  /**
@@ -27,7 +28,7 @@ export interface DeviceInfo {
27
28
  type: string;
28
29
  firmware?: string;
29
30
  hardware?: string;
30
- [key: string]: any;
31
+ [key: string]: unknown;
31
32
  }
32
33
  /**
33
34
  * Device settings interface
@@ -37,22 +38,60 @@ export interface DeviceSettings {
37
38
  id: string;
38
39
  name: string;
39
40
  enabled: boolean;
40
- [key: string]: any;
41
+ [key: string]: unknown;
41
42
  }
42
43
  /**
43
- * Script data interface
44
+ * Funscript action
45
+ */
46
+ export interface FunscriptAction {
47
+ at: number;
48
+ pos: number;
49
+ }
50
+ /**
51
+ * Funscript format
52
+ */
53
+ export interface Funscript {
54
+ actions: FunscriptAction[];
55
+ inverted?: boolean;
56
+ range?: number;
57
+ version?: string;
58
+ metadata?: Record<string, unknown>;
59
+ [key: string]: unknown;
60
+ }
61
+ /**
62
+ * Script data interface - input for loading scripts
44
63
  */
45
64
  export interface ScriptData {
46
65
  type: string;
47
66
  url?: string;
48
- content?: any;
67
+ content?: Funscript;
49
68
  }
50
69
  /**
51
70
  * Script options interface
52
71
  */
53
- export type ScriptOptions = {
72
+ export interface ScriptOptions {
54
73
  invertScript?: boolean;
55
- };
74
+ }
75
+ /**
76
+ * Result from loading a script to a single device
77
+ */
78
+ export interface DeviceScriptLoadResult {
79
+ success: boolean;
80
+ error?: string;
81
+ }
82
+ /**
83
+ * Result from loading a script via DeviceManager
84
+ */
85
+ export interface ScriptLoadResult {
86
+ /** The parsed and processed funscript content */
87
+ funscript: Funscript | null;
88
+ /** Whether the script was successfully fetched/parsed */
89
+ success: boolean;
90
+ /** Error message if fetching/parsing failed */
91
+ error?: string;
92
+ /** Per-device load results */
93
+ devices: Record<string, DeviceScriptLoadResult>;
94
+ }
56
95
  /**
57
96
  * Common interface for all haptic devices
58
97
  */
@@ -73,7 +112,7 @@ export interface HapticDevice {
73
112
  * Connect to the device
74
113
  * @param config Optional configuration
75
114
  */
76
- connect(config?: any): Promise<boolean>;
115
+ connect(config?: unknown): Promise<boolean>;
77
116
  /**
78
117
  * Disconnect from the device
79
118
  */
@@ -88,13 +127,14 @@ export interface HapticDevice {
88
127
  */
89
128
  updateConfig(config: Partial<DeviceSettings>): Promise<boolean>;
90
129
  /**
91
- * Load a script for playback
92
- * @param scriptData Script data to load
130
+ * Prepare the device to play a script
131
+ * The funscript content is already parsed - device just needs to prepare it
132
+ * (e.g., upload to server for Handy, store in memory for Buttplug)
133
+ *
134
+ * @param funscript The parsed funscript content
135
+ * @param options Script options (e.g., inversion already applied)
93
136
  */
94
- loadScript(scriptData: ScriptData, options?: ScriptOptions): Promise<{
95
- success: boolean;
96
- scriptContent?: ScriptData;
97
- }>;
137
+ prepareScript(funscript: Funscript, options?: ScriptOptions): Promise<DeviceScriptLoadResult>;
98
138
  /**
99
139
  * Play the loaded script at the specified time
100
140
  * @param timeMs Current time in milliseconds
@@ -121,11 +161,11 @@ export interface HapticDevice {
121
161
  * @param event Event name
122
162
  * @param callback Callback function
123
163
  */
124
- on(event: string, callback: (data: any) => void): void;
164
+ on(event: string, callback: (data: unknown) => void): void;
125
165
  /**
126
166
  * Remove event listener
127
167
  * @param event Event name
128
168
  * @param callback Callback function
129
169
  */
130
- off(event: string, callback: (data: any) => void): void;
170
+ off(event: string, callback: (data: unknown) => void): void;
131
171
  }
@@ -21,5 +21,6 @@ var DeviceCapability;
21
21
  DeviceCapability["VIBRATE"] = "vibrate";
22
22
  DeviceCapability["ROTATE"] = "rotate";
23
23
  DeviceCapability["LINEAR"] = "linear";
24
+ DeviceCapability["OSCILLATE"] = "oscillate";
24
25
  DeviceCapability["STROKE"] = "stroke";
25
26
  })(DeviceCapability || (exports.DeviceCapability = DeviceCapability = {}));
@@ -1,17 +1,19 @@
1
1
  /**
2
2
  * Device Manager
3
3
  *
4
- * Central manager for all haptic devices
4
+ * Central manager for all haptic devices.
5
+ * Handles unified script loading and distribution to devices.
5
6
  */
6
7
  import { EventEmitter } from "./events";
7
- import { HapticDevice, ScriptData, ScriptOptions } from "./device-interface";
8
+ import { HapticDevice, ScriptData, ScriptOptions, ScriptLoadResult, Funscript } from "./device-interface";
8
9
  /**
9
10
  * Device Manager class
10
11
  * Handles registration and control of multiple haptic devices
11
12
  */
12
13
  export declare class DeviceManager extends EventEmitter {
13
14
  private devices;
14
- private scriptData;
15
+ private currentFunscript;
16
+ private currentScriptOptions;
15
17
  /**
16
18
  * Register a device with the manager
17
19
  * @param device Device to register
@@ -31,6 +33,10 @@ export declare class DeviceManager extends EventEmitter {
31
33
  * @param deviceId Device ID to retrieve
32
34
  */
33
35
  getDevice(deviceId: string): HapticDevice | undefined;
36
+ /**
37
+ * Get the currently loaded funscript
38
+ */
39
+ getCurrentFunscript(): Funscript | null;
34
40
  /**
35
41
  * Connect to all registered devices
36
42
  * @returns Object with success status for each device
@@ -42,12 +48,19 @@ export declare class DeviceManager extends EventEmitter {
42
48
  */
43
49
  disconnectAll(): Promise<Record<string, boolean>>;
44
50
  /**
45
- * Load a script to all connected devices
46
- * @param scriptData Script data to load
51
+ * Load a script - fetches, parses, and prepares on all connected devices
52
+ *
53
+ * This is the main entry point for loading scripts. It:
54
+ * 1. Fetches and parses the script (once, centrally)
55
+ * 2. Applies any transformations (inversion, sorting)
56
+ * 3. Distributes to all connected devices
57
+ * 4. Returns the funscript along with per-device results
58
+ *
59
+ * @param scriptData Script data to load (URL or content)
47
60
  * @param options Options for script loading (e.g., invertScript)
48
- * @returns Object with success status for each device
61
+ * @returns ScriptLoadResult with funscript and per-device status
49
62
  */
50
- loadScriptAll(scriptData: ScriptData, options?: ScriptOptions): Promise<Record<string, boolean | ScriptData>>;
63
+ loadScript(scriptData: ScriptData, options?: ScriptOptions): Promise<ScriptLoadResult>;
51
64
  /**
52
65
  * Start playback on all connected devices
53
66
  * @param timeMs Current time in milliseconds
@@ -68,6 +81,10 @@ export declare class DeviceManager extends EventEmitter {
68
81
  * @returns Object with success status for each device
69
82
  */
70
83
  syncTimeAll(timeMs: number, filter?: number): Promise<Record<string, boolean>>;
84
+ /**
85
+ * Clear the currently loaded script
86
+ */
87
+ clearScript(): void;
71
88
  /**
72
89
  * Set up event forwarding from a device to the manager
73
90
  * @param device Device to forward events from
@@ -4,9 +4,11 @@ exports.DeviceManager = void 0;
4
4
  /**
5
5
  * Device Manager
6
6
  *
7
- * Central manager for all haptic devices
7
+ * Central manager for all haptic devices.
8
+ * Handles unified script loading and distribution to devices.
8
9
  */
9
10
  const events_1 = require("./events");
11
+ const script_loader_1 = require("./script-loader");
10
12
  /**
11
13
  * Device Manager class
12
14
  * Handles registration and control of multiple haptic devices
@@ -15,22 +17,29 @@ class DeviceManager extends events_1.EventEmitter {
15
17
  constructor() {
16
18
  super(...arguments);
17
19
  this.devices = new Map();
18
- this.scriptData = null;
20
+ this.currentFunscript = null;
21
+ this.currentScriptOptions = null;
19
22
  }
20
23
  /**
21
24
  * Register a device with the manager
22
25
  * @param device Device to register
23
26
  */
24
27
  registerDevice(device) {
25
- // Don't register the same device twice
28
+ var _a;
26
29
  if (this.devices.has(device.id)) {
27
30
  return;
28
31
  }
29
32
  this.devices.set(device.id, device);
30
- // Forward events from this device
31
33
  this.setupDeviceEventForwarding(device);
32
- // Emit device added event
33
34
  this.emit("deviceAdded", device);
35
+ // If we have a script loaded, prepare it on the new device
36
+ if (this.currentFunscript) {
37
+ device
38
+ .prepareScript(this.currentFunscript, (_a = this.currentScriptOptions) !== null && _a !== void 0 ? _a : undefined)
39
+ .catch((error) => {
40
+ console.error(`Error preparing script on newly registered device ${device.id}:`, error);
41
+ });
42
+ }
34
43
  }
35
44
  /**
36
45
  * Unregister a device from the manager
@@ -56,6 +65,12 @@ class DeviceManager extends events_1.EventEmitter {
56
65
  getDevice(deviceId) {
57
66
  return this.devices.get(deviceId);
58
67
  }
68
+ /**
69
+ * Get the currently loaded funscript
70
+ */
71
+ getCurrentFunscript() {
72
+ return this.currentFunscript;
73
+ }
59
74
  /**
60
75
  * Connect to all registered devices
61
76
  * @returns Object with success status for each device
@@ -91,36 +106,66 @@ class DeviceManager extends events_1.EventEmitter {
91
106
  return results;
92
107
  }
93
108
  /**
94
- * Load a script to all connected devices
95
- * @param scriptData Script data to load
109
+ * Load a script - fetches, parses, and prepares on all connected devices
110
+ *
111
+ * This is the main entry point for loading scripts. It:
112
+ * 1. Fetches and parses the script (once, centrally)
113
+ * 2. Applies any transformations (inversion, sorting)
114
+ * 3. Distributes to all connected devices
115
+ * 4. Returns the funscript along with per-device results
116
+ *
117
+ * @param scriptData Script data to load (URL or content)
96
118
  * @param options Options for script loading (e.g., invertScript)
97
- * @returns Object with success status for each device
119
+ * @returns ScriptLoadResult with funscript and per-device status
98
120
  */
99
- async loadScriptAll(scriptData, options) {
100
- const results = {};
101
- this.scriptData = scriptData;
121
+ async loadScript(scriptData, options) {
122
+ // Step 1: Fetch and parse the script centrally
123
+ const loadResult = await (0, script_loader_1.loadScript)(scriptData, options);
124
+ if (!loadResult.success || !loadResult.funscript) {
125
+ return {
126
+ success: false,
127
+ funscript: null,
128
+ error: loadResult.error,
129
+ devices: {},
130
+ };
131
+ }
132
+ // Store the loaded script
133
+ this.currentFunscript = loadResult.funscript;
134
+ this.currentScriptOptions = options !== null && options !== void 0 ? options : null;
135
+ // Step 2: Prepare on all connected devices
136
+ const deviceResults = {};
102
137
  for (const [id, device] of this.devices.entries()) {
138
+ // Only prepare on connected devices (or buttplug which manages its own connection)
103
139
  if (device.isConnected || device.id === "buttplug") {
104
140
  try {
105
- results[id] = await device.loadScript(scriptData, options);
141
+ const result = await device.prepareScript(loadResult.funscript, options);
142
+ deviceResults[id] = result;
106
143
  }
107
144
  catch (error) {
108
- console.error(`Error loading script to device ${id}:`, error);
109
- results[id] = { success: false };
145
+ console.error(`Error preparing script on device ${id}:`, error);
146
+ deviceResults[id] = {
147
+ success: false,
148
+ error: error instanceof Error ? error.message : String(error),
149
+ };
110
150
  }
111
151
  }
112
152
  else {
113
- results[id] = { success: false };
114
- }
115
- }
116
- const transformedResults = {};
117
- for (const [id, result] of Object.entries(results)) {
118
- if (result.scriptContent) {
119
- transformedResults["script"] = result.scriptContent;
153
+ deviceResults[id] = {
154
+ success: false,
155
+ error: "Device not connected",
156
+ };
120
157
  }
121
- transformedResults[id] = result.success;
122
158
  }
123
- return transformedResults;
159
+ // Emit event
160
+ this.emit("scriptLoaded", {
161
+ funscript: loadResult.funscript,
162
+ devices: deviceResults,
163
+ });
164
+ return {
165
+ success: true,
166
+ funscript: loadResult.funscript,
167
+ devices: deviceResults,
168
+ };
124
169
  }
125
170
  /**
126
171
  * Start playback on all connected devices
@@ -193,12 +238,18 @@ class DeviceManager extends events_1.EventEmitter {
193
238
  }
194
239
  return results;
195
240
  }
241
+ /**
242
+ * Clear the currently loaded script
243
+ */
244
+ clearScript() {
245
+ this.currentFunscript = null;
246
+ this.currentScriptOptions = null;
247
+ }
196
248
  /**
197
249
  * Set up event forwarding from a device to the manager
198
250
  * @param device Device to forward events from
199
251
  */
200
252
  setupDeviceEventForwarding(device) {
201
- // Forward common events
202
253
  const eventsToForward = [
203
254
  "error",
204
255
  "connected",
@@ -211,7 +262,6 @@ class DeviceManager extends events_1.EventEmitter {
211
262
  for (const eventName of eventsToForward) {
212
263
  device.on(eventName, (data) => {
213
264
  this.emit(`device:${device.id}:${eventName}`, data);
214
- // Also emit a general event for any device
215
265
  this.emit(`device:${eventName}`, { deviceId: device.id, data });
216
266
  });
217
267
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Core exports
3
+ */
4
+ export * from "./device-interface";
5
+ export * from "./device-manager";
6
+ export * from "./events";
7
+ export * from "./script-loader";
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ /**
18
+ * Core exports
19
+ */
20
+ __exportStar(require("./device-interface"), exports);
21
+ __exportStar(require("./device-manager"), exports);
22
+ __exportStar(require("./events"), exports);
23
+ __exportStar(require("./script-loader"), exports);
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Script Loader
3
+ *
4
+ * Centralized script fetching and parsing.
5
+ * Handles fetching from URLs, parsing CSV/JSON, and applying transformations.
6
+ */
7
+ import { Funscript, ScriptData, ScriptOptions } from "./device-interface";
8
+ /**
9
+ * Parse CSV content to Funscript format
10
+ */
11
+ export declare function parseCSVToFunscript(csvText: string): Funscript;
12
+ /**
13
+ * Apply inversion to funscript actions
14
+ */
15
+ export declare function invertFunscript(funscript: Funscript): Funscript;
16
+ /**
17
+ * Validate funscript structure
18
+ */
19
+ export declare function isValidFunscript(content: unknown): content is Funscript;
20
+ /**
21
+ * Result of loading a script
22
+ */
23
+ export interface LoadScriptResult {
24
+ success: boolean;
25
+ funscript: Funscript | null;
26
+ error?: string;
27
+ }
28
+ /**
29
+ * Load and parse a script from ScriptData
30
+ *
31
+ * @param scriptData - The script data (URL or content)
32
+ * @param options - Script options (e.g., inversion)
33
+ * @returns Parsed funscript or error
34
+ */
35
+ export declare function loadScript(scriptData: ScriptData, options?: ScriptOptions): Promise<LoadScriptResult>;
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ /**
3
+ * Script Loader
4
+ *
5
+ * Centralized script fetching and parsing.
6
+ * Handles fetching from URLs, parsing CSV/JSON, and applying transformations.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.parseCSVToFunscript = parseCSVToFunscript;
10
+ exports.invertFunscript = invertFunscript;
11
+ exports.isValidFunscript = isValidFunscript;
12
+ exports.loadScript = loadScript;
13
+ /**
14
+ * Parse CSV content to Funscript format
15
+ */
16
+ function parseCSVToFunscript(csvText) {
17
+ const lines = csvText.split(/\r?\n/).filter((line) => line.trim().length > 0);
18
+ const actions = [];
19
+ // Check if there's a header line (contains non-numeric characters in first column)
20
+ let startIndex = 0;
21
+ if (lines.length > 0 && isNaN(parseFloat(lines[0].split(",")[0]))) {
22
+ startIndex = 1;
23
+ }
24
+ // Parse each line
25
+ for (let i = startIndex; i < lines.length; i++) {
26
+ const line = lines[i].trim();
27
+ if (!line)
28
+ continue;
29
+ const columns = line.split(",");
30
+ if (columns.length >= 2) {
31
+ const at = parseFloat(columns[0].trim());
32
+ const pos = parseFloat(columns[1].trim());
33
+ if (!isNaN(at) && !isNaN(pos)) {
34
+ actions.push({
35
+ at: Math.round(at),
36
+ pos: Math.min(100, Math.max(0, Math.round(pos))),
37
+ });
38
+ }
39
+ }
40
+ }
41
+ return {
42
+ actions,
43
+ metadata: { convertedFrom: "csv" },
44
+ };
45
+ }
46
+ /**
47
+ * Apply inversion to funscript actions
48
+ */
49
+ function invertFunscript(funscript) {
50
+ return {
51
+ ...funscript,
52
+ actions: funscript.actions.map((action) => ({
53
+ ...action,
54
+ pos: 100 - action.pos,
55
+ })),
56
+ inverted: !funscript.inverted,
57
+ };
58
+ }
59
+ /**
60
+ * Validate funscript structure
61
+ */
62
+ function isValidFunscript(content) {
63
+ if (!content || typeof content !== "object") {
64
+ return false;
65
+ }
66
+ const obj = content;
67
+ if (!Array.isArray(obj.actions)) {
68
+ return false;
69
+ }
70
+ // Check that actions have the required properties
71
+ return obj.actions.every((action) => action &&
72
+ typeof action === "object" &&
73
+ typeof action.at === "number" &&
74
+ typeof action.pos === "number");
75
+ }
76
+ /**
77
+ * Load and parse a script from ScriptData
78
+ *
79
+ * @param scriptData - The script data (URL or content)
80
+ * @param options - Script options (e.g., inversion)
81
+ * @returns Parsed funscript or error
82
+ */
83
+ async function loadScript(scriptData, options) {
84
+ try {
85
+ let funscript;
86
+ if (scriptData.content) {
87
+ // Content already provided
88
+ if (!isValidFunscript(scriptData.content)) {
89
+ return {
90
+ success: false,
91
+ funscript: null,
92
+ error: "Invalid funscript format: content is not a valid funscript",
93
+ };
94
+ }
95
+ funscript = scriptData.content;
96
+ }
97
+ else if (scriptData.url) {
98
+ // Fetch from URL
99
+ const response = await fetch(scriptData.url);
100
+ if (!response.ok) {
101
+ return {
102
+ success: false,
103
+ funscript: null,
104
+ error: `Failed to fetch script: ${response.status} ${response.statusText}`,
105
+ };
106
+ }
107
+ const fileExtension = scriptData.url.toLowerCase().split(".").pop();
108
+ if (fileExtension === "csv") {
109
+ const csvText = await response.text();
110
+ funscript = parseCSVToFunscript(csvText);
111
+ }
112
+ else {
113
+ // Assume JSON/funscript
114
+ const text = await response.text();
115
+ try {
116
+ const parsed = JSON.parse(text);
117
+ if (!isValidFunscript(parsed)) {
118
+ return {
119
+ success: false,
120
+ funscript: null,
121
+ error: "Invalid funscript format: missing or invalid actions array",
122
+ };
123
+ }
124
+ funscript = parsed;
125
+ }
126
+ catch (_a) {
127
+ // Try parsing as CSV if JSON fails
128
+ funscript = parseCSVToFunscript(text);
129
+ if (funscript.actions.length === 0) {
130
+ return {
131
+ success: false,
132
+ funscript: null,
133
+ error: "Failed to parse script: not valid JSON or CSV",
134
+ };
135
+ }
136
+ }
137
+ }
138
+ }
139
+ else {
140
+ return {
141
+ success: false,
142
+ funscript: null,
143
+ error: "Invalid script data: either URL or content must be provided",
144
+ };
145
+ }
146
+ // Validate we have actions
147
+ if (!funscript.actions || funscript.actions.length === 0) {
148
+ return {
149
+ success: false,
150
+ funscript: null,
151
+ error: "Invalid funscript: no actions found",
152
+ };
153
+ }
154
+ // Apply inversion if requested
155
+ if (options === null || options === void 0 ? void 0 : options.invertScript) {
156
+ funscript = invertFunscript(funscript);
157
+ }
158
+ // Sort actions by timestamp
159
+ funscript.actions.sort((a, b) => a.at - b.at);
160
+ return {
161
+ success: true,
162
+ funscript,
163
+ };
164
+ }
165
+ catch (error) {
166
+ return {
167
+ success: false,
168
+ funscript: null,
169
+ error: `Script loading error: ${error instanceof Error ? error.message : String(error)}`,
170
+ };
171
+ }
172
+ }