ive-connect
Version:
A universal haptic device control library for interactive experiences
378 lines (377 loc) • 13.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AutoblowDevice = void 0;
/**
* Autoblow Device Implementation
*
* Implements the HapticDevice interface for Autoblow devices (Ultra and Vacuglide)
* Focused on sync script playback functionality
*/
const device_interface_1 = require("../../core/device-interface");
const events_1 = require("../../core/events");
/**
* Default Autoblow configuration
*/
const DEFAULT_CONFIG = {
id: 'autoblow',
name: 'Autoblow',
enabled: true,
deviceToken: '',
offset: 0,
};
/**
* Autoblow device implementation
*/
class AutoblowDevice extends events_1.EventEmitter {
constructor(config) {
super();
this._connectionState = device_interface_1.ConnectionState.DISCONNECTED;
this._deviceInfo = null;
this._device = null;
this._deviceType = null;
this._isPlaying = false;
this._scriptPrepared = false;
this.id = 'autoblow';
this.name = 'Autoblow';
this.type = 'autoblow';
this.capabilities = [
device_interface_1.DeviceCapability.LINEAR,
device_interface_1.DeviceCapability.STROKE,
];
this._config = { ...DEFAULT_CONFIG };
if (config) {
Object.assign(this._config, config);
}
}
/**
* Get connected state
*/
get isConnected() {
return this._connectionState === device_interface_1.ConnectionState.CONNECTED;
}
/**
* Get playing state
*/
get isPlaying() {
return this._isPlaying;
}
/**
* Get the device type (ultra or vacuglide)
*/
get deviceType() {
return this._deviceType;
}
/**
* Connect to the Autoblow device
*/
async connect(config) {
try {
if (config) {
await this.updateConfig(config);
}
if (!this._config.deviceToken || this._config.deviceToken.length < 5) {
this.emit('error', 'Device token must be at least 5 characters');
return false;
}
this._connectionState = device_interface_1.ConnectionState.CONNECTING;
this.emit('connectionStateChanged', this._connectionState);
// Dynamically import the SDK
let sdk;
try {
sdk = await Promise.resolve().then(() => __importStar(require('@xsense/autoblow-sdk')));
}
catch (error) {
this.emit('error', 'Failed to load Autoblow SDK. Make sure @xsense/autoblow-sdk is installed.');
this._connectionState = device_interface_1.ConnectionState.DISCONNECTED;
this.emit('connectionStateChanged', this._connectionState);
return false;
}
// Initialize device connection
const result = await sdk.deviceInit(this._config.deviceToken);
// Store the appropriate device reference
if (result.ultra) {
this._device = result.ultra;
this._deviceType = 'autoblow-ultra';
}
else if (result.vacuglide) {
this._device = result.vacuglide;
this._deviceType = 'vacuglide';
}
else {
throw new Error('No device returned from SDK');
}
this._deviceInfo = result.deviceInfo;
this._connectionState = device_interface_1.ConnectionState.CONNECTED;
this.emit('connectionStateChanged', this._connectionState);
this.emit('connected', this.getDeviceInfo());
return true;
}
catch (error) {
console.error('Autoblow: Error connecting to device:', error);
this._connectionState = device_interface_1.ConnectionState.DISCONNECTED;
this.emit('connectionStateChanged', this._connectionState);
this.emit('error', `Connection error: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
/**
* Disconnect from the device
*/
async disconnect() {
try {
if (this._isPlaying) {
await this.stop();
}
this._device = null;
this._deviceInfo = null;
this._deviceType = null;
this._isPlaying = false;
this._scriptPrepared = false;
this._connectionState = device_interface_1.ConnectionState.DISCONNECTED;
this.emit('connectionStateChanged', this._connectionState);
this.emit('disconnected');
return true;
}
catch (error) {
console.error('Autoblow: Error disconnecting:', error);
this._connectionState = device_interface_1.ConnectionState.DISCONNECTED;
this.emit('connectionStateChanged', this._connectionState);
this.emit('disconnected');
return true;
}
}
/**
* Get current configuration
*/
getConfig() {
return { ...this._config };
}
/**
* Update configuration
*/
async updateConfig(config) {
if (config.deviceToken !== undefined) {
this._config.deviceToken = config.deviceToken;
}
if (config.offset !== undefined) {
this._config.offset = config.offset;
// Apply offset to device if connected
if (this.isConnected && this._device) {
try {
await this._device.syncScriptOffset(config.offset);
}
catch (error) {
console.error('Autoblow: Error setting offset:', error);
}
}
}
if (config.name !== undefined) {
this._config.name = config.name;
}
if (config.enabled !== undefined) {
this._config.enabled = config.enabled;
}
this.emit('configChanged', this._config);
return true;
}
/**
* Prepare a script for playback (upload to device)
* The funscript is already parsed - we just need to upload it
*
* @param funscript The parsed funscript content
*/
async prepareScript(funscript) {
if (!this.isConnected || !this._device) {
return { success: false, error: 'Device not connected' };
}
try {
// Validate funscript format
if (!funscript.actions || !Array.isArray(funscript.actions)) {
return {
success: false,
error: 'Invalid script format: Missing actions array',
};
}
// Convert to Autoblow SDK format and upload
const sdkFunscript = {
actions: funscript.actions.map((action) => ({
at: action.at,
pos: action.pos,
})),
};
await this._device.syncScriptUploadFunscriptFile(sdkFunscript);
this._scriptPrepared = true;
this.emit('scriptLoaded', {
type: 'funscript',
actions: funscript.actions.length,
});
return { success: true };
}
catch (error) {
console.error('Autoblow: Error preparing script:', error);
return {
success: false,
error: `Script preparation error: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
/**
* Start playback at the specified time
*/
async play(timeMs, _playbackRate = 1.0, _loop = false) {
if (!this.isConnected || !this._device) {
this.emit('error', 'Cannot play: Device not connected');
return false;
}
if (!this._scriptPrepared) {
this.emit('error', 'Cannot play: No script prepared');
return false;
}
try {
// Apply offset before starting
if (this._config.offset !== 0) {
await this._device.syncScriptOffset(this._config.offset);
}
await this._device.syncScriptStart(timeMs);
this._isPlaying = true;
this.emit('playbackStateChanged', {
isPlaying: this._isPlaying,
timeMs,
});
return true;
}
catch (error) {
console.error('Autoblow: Error starting playback:', error);
this._isPlaying = false;
this.emit('error', `Playback error: ${error instanceof Error ? error.message : String(error)}`);
this.emit('playbackStateChanged', { isPlaying: false });
return false;
}
}
/**
* Stop playback
*/
async stop() {
if (!this.isConnected || !this._device) {
this.emit('error', 'Cannot stop: Device not connected');
return false;
}
try {
await this._device.syncScriptStop();
this._isPlaying = false;
this.emit('playbackStateChanged', { isPlaying: false });
return true;
}
catch (error) {
console.error('Autoblow: Error stopping playback:', error);
this._isPlaying = false;
this.emit('error', `Stop error: ${error instanceof Error ? error.message : String(error)}`);
this.emit('playbackStateChanged', { isPlaying: false });
return false;
}
}
/**
* Sync time - Autoblow handles this via syncScriptStart
* We restart playback at the new time position
*/
async syncTime(timeMs, _filter) {
if (!this.isConnected || !this._isPlaying || !this._device) {
return false;
}
try {
// Autoblow doesn't have a direct sync method, restart at new position
await this._device.syncScriptStart(timeMs);
return true;
}
catch (error) {
console.error('Autoblow: Error syncing time:', error);
return false;
}
}
/**
* Set the sync script offset
*/
async setOffset(offsetMs) {
if (!this.isConnected || !this._device) {
this.emit('error', 'Cannot set offset: Device not connected');
return false;
}
try {
await this._device.syncScriptOffset(offsetMs);
this._config.offset = offsetMs;
this.emit('configChanged', this._config);
return true;
}
catch (error) {
console.error('Autoblow: Error setting offset:', error);
return false;
}
}
/**
* Get device state
*/
async getState() {
if (!this.isConnected || !this._device) {
return null;
}
try {
return await this._device.getState();
}
catch (error) {
console.error('Autoblow: Error getting state:', error);
return null;
}
}
/**
* Get device information
*/
getDeviceInfo() {
if (!this._deviceInfo)
return null;
return {
id: this.id,
name: this.name,
type: this.type,
deviceType: this._deviceType,
firmware: String(this._deviceInfo.firmwareVersion),
firmwareBranch: this._deviceInfo.firmwareBranch,
hardware: this._deviceInfo.hardwareVersion,
firmwareStatus: this._deviceInfo.firmwareStatus,
mac: this._deviceInfo.mac,
};
}
}
exports.AutoblowDevice = AutoblowDevice;