ive-connect 0.1.2 → 0.3.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 CHANGED
@@ -1,109 +1,109 @@
1
- # ive-connect
2
-
3
- A universal haptic device control library that provides a consistent interface for managing various haptic devices (Handy, Buttplug, etc.).
4
-
5
- ## Features
6
-
7
- - Unified device control interface
8
- - Support for multiple device types
9
- - Event-based state management
10
- - TypeScript support
11
-
12
- ## Installation
13
-
14
- ```bash
15
- npm install ive-connect
16
- ```
17
-
18
- ## Quick Start
19
-
20
- ```typescript
21
- import { DeviceManager, HandyDevice } from "ive-connect";
22
-
23
- // Create a device manager
24
- const manager = new DeviceManager();
25
-
26
- // Create and register a Handy device
27
- const handyDevice = new HandyDevice({
28
- connectionKey: "your-connection-key",
29
- });
30
- manager.registerDevice(handyDevice);
31
-
32
- // Connect to the device
33
- await handyDevice.connect();
34
-
35
- // Load a script
36
- await handyDevice.loadScript({
37
- type: "funscript",
38
- url: "https://example.com/script.funscript",
39
- });
40
-
41
- // Start playback
42
- await handyDevice.play(0, 1.0, false);
43
-
44
- // Later, stop playback
45
- await handyDevice.stop();
46
-
47
- // Disconnect when done
48
- await handyDevice.disconnect();
49
- ```
50
-
51
- ## Supported Devices
52
-
53
- ### Handy
54
-
55
- ```typescript
56
- import { HandyDevice } from "ive-connect";
57
-
58
- const handy = new HandyDevice({
59
- connectionKey: "your-connection-key",
60
- // Optional custom configuration
61
- baseV3Url: "https://www.handyfeeling.com/api/v3",
62
- baseV2Url: "https://www.handyfeeling.com/api/v2",
63
- applicationId: "YourAppName",
64
- });
65
-
66
- // Connect to the device
67
- await handy.connect();
68
-
69
- // Update configuration
70
- await handy.updateConfig({
71
- offset: -200, // Timing offset in milliseconds
72
- stroke: {
73
- min: 0.1, // Min stroke position (0.0 to 1.0)
74
- max: 0.9, // Max stroke position (0.0 to 1.0)
75
- },
76
- });
77
-
78
- // Listen for events
79
- handy.on("connected", (deviceInfo) => {
80
- console.log("Connected to Handy:", deviceInfo);
81
- });
82
-
83
- handy.on("playbackStateChanged", (state) => {
84
- console.log("Playback state changed:", state.isPlaying);
85
- });
86
- ```
87
-
88
- ### Using the Device Manager
89
-
90
- ```typescript
91
- import { DeviceManager, HandyDevice } from "ive-connect";
92
-
93
- // Create a device manager
94
- const manager = new DeviceManager();
95
-
96
- // Register devices
97
- const handy = new HandyDevice({
98
- connectionKey: "your-connection-key",
99
- });
100
- manager.registerDevice(handy);
101
- ```
102
-
103
- ## License
104
-
105
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
106
-
107
- ## Acknowledgments
108
-
109
- This project uses [buttplug.io](https://buttplug.io) (BSD 3-Clause License) for device communication.
1
+ # ive-connect
2
+
3
+ A universal haptic device control library that provides a consistent interface for managing various haptic devices (Handy, Buttplug, etc.).
4
+
5
+ ## Features
6
+
7
+ - Unified device control interface
8
+ - Support for multiple device types
9
+ - Event-based state management
10
+ - TypeScript support
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install ive-connect
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```typescript
21
+ import { DeviceManager, HandyDevice } from "ive-connect";
22
+
23
+ // Create a device manager
24
+ const manager = new DeviceManager();
25
+
26
+ // Create and register a Handy device
27
+ const handyDevice = new HandyDevice({
28
+ connectionKey: "your-connection-key",
29
+ });
30
+ manager.registerDevice(handyDevice);
31
+
32
+ // Connect to the device
33
+ await handyDevice.connect();
34
+
35
+ // Load a script
36
+ await handyDevice.loadScript({
37
+ type: "funscript",
38
+ url: "https://example.com/script.funscript",
39
+ });
40
+
41
+ // Start playback
42
+ await handyDevice.play(0, 1.0, false);
43
+
44
+ // Later, stop playback
45
+ await handyDevice.stop();
46
+
47
+ // Disconnect when done
48
+ await handyDevice.disconnect();
49
+ ```
50
+
51
+ ## Supported Devices
52
+
53
+ ### Handy
54
+
55
+ ```typescript
56
+ import { HandyDevice } from "ive-connect";
57
+
58
+ const handy = new HandyDevice({
59
+ connectionKey: "your-connection-key",
60
+ // Optional custom configuration
61
+ baseV3Url: "https://www.handyfeeling.com/api/v3",
62
+ baseV2Url: "https://www.handyfeeling.com/api/v2",
63
+ applicationId: "YourAppName",
64
+ });
65
+
66
+ // Connect to the device
67
+ await handy.connect();
68
+
69
+ // Update configuration
70
+ await handy.updateConfig({
71
+ offset: -200, // Timing offset in milliseconds
72
+ stroke: {
73
+ min: 0.1, // Min stroke position (0.0 to 1.0)
74
+ max: 0.9, // Max stroke position (0.0 to 1.0)
75
+ },
76
+ });
77
+
78
+ // Listen for events
79
+ handy.on("connected", (deviceInfo) => {
80
+ console.log("Connected to Handy:", deviceInfo);
81
+ });
82
+
83
+ handy.on("playbackStateChanged", (state) => {
84
+ console.log("Playback state changed:", state.isPlaying);
85
+ });
86
+ ```
87
+
88
+ ### Using the Device Manager
89
+
90
+ ```typescript
91
+ import { DeviceManager, HandyDevice } from "ive-connect";
92
+
93
+ // Create a device manager
94
+ const manager = new DeviceManager();
95
+
96
+ // Register devices
97
+ const handy = new HandyDevice({
98
+ connectionKey: "your-connection-key",
99
+ });
100
+ manager.registerDevice(handy);
101
+ ```
102
+
103
+ ## License
104
+
105
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
106
+
107
+ ## Acknowledgments
108
+
109
+ This project uses [buttplug.io](https://buttplug.io) (BSD 3-Clause License) for device communication.
@@ -43,7 +43,7 @@ const buttplug_1 = require("buttplug");
43
43
  const buttplug_2 = require("buttplug");
44
44
  const events_1 = require("../../core/events");
45
45
  const types_1 = require("./types");
46
- const DEBUG_WEBSOCKET = true;
46
+ const DEBUG_WEBSOCKET = false;
47
47
  class ButtplugApi extends events_1.EventEmitter {
48
48
  constructor(clientName = "IVE-Connect") {
49
49
  super();
@@ -12,6 +12,7 @@ const buttplug_api_1 = require("./buttplug-api");
12
12
  const types_1 = require("./types");
13
13
  const buttplug_server_1 = require("./buttplug-server");
14
14
  const command_helpers_1 = require("./command-helpers");
15
+ const parseCSVToFunscript_1 = require("../../utils/parseCSVToFunscript");
15
16
  /**
16
17
  * Default Buttplug configuration
17
18
  */
@@ -193,7 +194,7 @@ class ButtplugDevice extends events_1.EventEmitter {
193
194
  * Load a script for playback
194
195
  */
195
196
  async loadScript(scriptData) {
196
- var _a;
197
+ var _a, _b, _c;
197
198
  if (!this.isConnected) {
198
199
  this.emit("error", "Cannot load script: Not connected to a server");
199
200
  return false;
@@ -213,8 +214,34 @@ class ButtplugDevice extends events_1.EventEmitter {
213
214
  if (!response.ok) {
214
215
  throw new Error(`Failed to fetch script: ${response.status} ${response.statusText}`);
215
216
  }
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);
217
+ // Determine if it's a CSV or JSON (funscript) based on file extension
218
+ const fileExtension = scriptData.url.toLowerCase().split(".").pop();
219
+ if (fileExtension === "csv") {
220
+ // Handle CSV file
221
+ const csvText = await response.text();
222
+ scriptContent = (0, parseCSVToFunscript_1.parseCSVToFunscript)(csvText);
223
+ console.log(`[BUTTPLUG-SCRIPT] CSV loaded and converted to funscript format, actions:`, (_a = scriptContent.actions) === null || _a === void 0 ? void 0 : _a.length);
224
+ }
225
+ else {
226
+ // Handle JSON file (funscript)
227
+ try {
228
+ scriptContent = await response.json();
229
+ console.log(`[BUTTPLUG-SCRIPT] Script loaded successfully, actions:`, (_b = scriptContent.actions) === null || _b === void 0 ? void 0 : _b.length);
230
+ }
231
+ catch (parseError) {
232
+ // If JSON parsing fails, try as CSV
233
+ const text = await response.text();
234
+ try {
235
+ // First try to parse as JSON again with some cleanup
236
+ scriptContent = JSON.parse(text.trim());
237
+ }
238
+ catch (_d) {
239
+ // If that fails, try CSV parsing
240
+ scriptContent = (0, parseCSVToFunscript_1.parseCSVToFunscript)(text);
241
+ }
242
+ console.log(`[BUTTPLUG-SCRIPT] File loaded and parsed as CSV, actions:`, (_c = scriptContent.actions) === null || _c === void 0 ? void 0 : _c.length);
243
+ }
244
+ }
218
245
  }
219
246
  catch (error) {
220
247
  this.emit("error", `Failed to fetch script: ${error instanceof Error ? error.message : String(error)}`);
@@ -382,6 +409,7 @@ class ButtplugDevice extends events_1.EventEmitter {
382
409
  * Process script actions based on current time
383
410
  */
384
411
  _processActions(executor) {
412
+ var _a;
385
413
  if (!this._isPlaying || !this._currentScriptActions.length) {
386
414
  return;
387
415
  }
@@ -392,7 +420,7 @@ class ButtplugDevice extends events_1.EventEmitter {
392
420
  const actionIndex = this._findActionIndexForTime(elapsedMs);
393
421
  // If we reached the end of the script
394
422
  if (actionIndex === this._currentScriptActions.length - 1 &&
395
- elapsedMs > this._currentScriptActions[actionIndex].at + 1000) {
423
+ elapsedMs > ((_a = this._currentScriptActions[actionIndex]) === null || _a === void 0 ? void 0 : _a.at) + 1000) {
396
424
  if (this._loopPlayback) {
397
425
  // Reset for loop playback
398
426
  this._playbackStartTime = Date.now();
@@ -411,11 +439,11 @@ class ButtplugDevice extends events_1.EventEmitter {
411
439
  const prevAction = actionIndex > 0
412
440
  ? this._currentScriptActions[actionIndex - 1]
413
441
  : { pos: 0 };
414
- // Calculate duration for linear movement based on time to next action
442
+ // Calculate duration for linear movement based on time with previous action
415
443
  let durationMs = 500; // Default duration if we can't determine
416
444
  if (actionIndex < this._currentScriptActions.length - 1) {
417
- const nextAction = this._currentScriptActions[actionIndex + 1];
418
- durationMs = nextAction.at - action.at;
445
+ const prevAction = this._currentScriptActions[actionIndex - 1];
446
+ durationMs = (action === null || action === void 0 ? void 0 : action.at) - (prevAction === null || prevAction === void 0 ? void 0 : prevAction.at);
419
447
  // Enforce a minimum duration to prevent erratic movement
420
448
  durationMs = Math.max(100, durationMs);
421
449
  }
@@ -458,7 +486,12 @@ class ButtplugDevice extends events_1.EventEmitter {
458
486
  high = mid - 1;
459
487
  }
460
488
  }
461
- return bestIndex;
489
+ // When returning bestIndex, we're always getting
490
+ // the last action that has a timestamp <= timeMs
491
+ // Let's return the next action instead
492
+ return bestIndex < this._currentScriptActions.length - 1
493
+ ? bestIndex + 1
494
+ : bestIndex;
462
495
  }
463
496
  /**
464
497
  * Set up event handlers for the Buttplug API
@@ -166,7 +166,7 @@ class HandyApi {
166
166
  const response = await this.request("/hssp/play", {
167
167
  method: "PUT",
168
168
  body: JSON.stringify({
169
- start_time: Math.round(videoTime * 1000),
169
+ start_time: Math.round(videoTime),
170
170
  server_time: this.estimateServerTime(),
171
171
  playback_rate: playbackRate,
172
172
  loop,
@@ -203,7 +203,7 @@ class HandyApi {
203
203
  const response = await this.request("/hssp/synctime", {
204
204
  method: "PUT",
205
205
  body: JSON.stringify({
206
- current_time: Math.round(videoTime * 1000),
206
+ current_time: Math.round(videoTime),
207
207
  server_time: this.estimateServerTime(),
208
208
  filter,
209
209
  }),
@@ -205,8 +205,32 @@ class HandyDevice extends events_1.EventEmitter {
205
205
  let scriptUrl;
206
206
  // Handle script data based on type
207
207
  if (scriptData.url) {
208
- // If URL is provided, use it directly
209
- scriptUrl = scriptData.url;
208
+ // If URL ends with .funscript and it's a direct URL, fetch and upload it
209
+ if (scriptData.url.toLowerCase().endsWith(".funscript")) {
210
+ try {
211
+ const response = await fetch(scriptData.url);
212
+ if (!response.ok) {
213
+ throw new Error(`Failed to fetch funscript: ${response.status}`);
214
+ }
215
+ const funscriptContent = await response.json();
216
+ const blob = new Blob([JSON.stringify(funscriptContent)], {
217
+ type: "application/json",
218
+ });
219
+ const uploadedUrl = await this._api.uploadScript(blob);
220
+ if (!uploadedUrl) {
221
+ throw new Error("Failed to upload funscript");
222
+ }
223
+ scriptUrl = uploadedUrl;
224
+ }
225
+ catch (error) {
226
+ console.error("Error processing funscript URL:", error);
227
+ // Fall back to using the original URL
228
+ scriptUrl = scriptData.url;
229
+ }
230
+ }
231
+ else {
232
+ scriptUrl = scriptData.url;
233
+ }
210
234
  }
211
235
  else if (scriptData.content) {
212
236
  // If content is provided, upload it
@@ -0,0 +1,7 @@
1
+ export declare const parseCSVToFunscript: (csvText: string) => {
2
+ actions: {
3
+ at: number;
4
+ pos: number;
5
+ }[];
6
+ name: string;
7
+ };
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseCSVToFunscript = void 0;
4
+ const parseCSVToFunscript = (csvText) => {
5
+ try {
6
+ // Split the CSV by lines and filter out empty lines
7
+ const lines = csvText
8
+ .split(/\r?\n/)
9
+ .filter((line) => line.trim().length > 0);
10
+ const actions = [];
11
+ // Check if there's a header line (contains non-numeric characters in first column)
12
+ let startIndex = 0;
13
+ if (isNaN(parseFloat(lines[0].split(",")[0]))) {
14
+ startIndex = 1;
15
+ }
16
+ // Parse each line
17
+ for (let i = startIndex; i < lines.length; i++) {
18
+ const line = lines[i].trim();
19
+ if (!line)
20
+ continue;
21
+ const columns = line.split(",");
22
+ actions.push({
23
+ at: Math.round(parseFloat(columns[0].trim())),
24
+ pos: Math.min(100, Math.max(0, Math.round(parseFloat(columns[1].trim())))),
25
+ });
26
+ }
27
+ return {
28
+ actions: actions,
29
+ name: "Converted from CSV",
30
+ };
31
+ }
32
+ catch (error) {
33
+ console.error("Error parsing CSV:", error);
34
+ throw new Error("Failed to parse CSV file");
35
+ }
36
+ };
37
+ exports.parseCSVToFunscript = parseCSVToFunscript;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ive-connect",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "A universal haptic device control library for interactive experiences",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,8 @@
10
10
  "prebuild": "npm run clean",
11
11
  "prepare": "npm run build",
12
12
  "test": "jest",
13
- "lint": "eslint src --ext .ts"
13
+ "lint": "eslint src --ext .ts",
14
+ "release": "release-it"
14
15
  },
15
16
  "keywords": [
16
17
  "haptic",
@@ -37,6 +38,7 @@
37
38
  "@typescript-eslint/parser": "^8.31.0",
38
39
  "eslint": "^9.25.1",
39
40
  "jest": "^29.7.0",
41
+ "release-it": "^19.0.2",
40
42
  "rimraf": "^6.0.1"
41
43
  },
42
44
  "engines": {