@zendrex/buttplug.js 0.1.0 → 0.1.1
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 +29 -4
- package/dist/index.cjs +261 -328
- package/dist/index.d.cts +41 -384
- package/dist/index.d.ts +41 -384
- package/dist/index.js +260 -291
- package/package.json +40 -5
package/dist/index.cjs
CHANGED
|
@@ -32,60 +32,24 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
ButtplugClient: () => ButtplugClient,
|
|
34
34
|
ButtplugError: () => ButtplugError,
|
|
35
|
-
ClientMessageSchema: () => ClientMessageSchema,
|
|
36
35
|
ConnectionError: () => ConnectionError,
|
|
37
|
-
DEFAULT_CLIENT_NAME: () => DEFAULT_CLIENT_NAME,
|
|
38
|
-
DEFAULT_PING_INTERVAL: () => DEFAULT_PING_INTERVAL,
|
|
39
|
-
DEFAULT_REQUEST_TIMEOUT: () => DEFAULT_REQUEST_TIMEOUT,
|
|
40
36
|
Device: () => Device,
|
|
41
37
|
DeviceError: () => DeviceError,
|
|
42
|
-
DeviceFeaturesSchema: () => DeviceFeaturesSchema,
|
|
43
38
|
EASING_FUNCTIONS: () => EASING_FUNCTIONS,
|
|
44
39
|
EASING_VALUES: () => EASING_VALUES,
|
|
45
40
|
ErrorCode: () => ErrorCode,
|
|
46
|
-
FeatureValueSchema: () => FeatureValueSchema,
|
|
47
41
|
HandshakeError: () => HandshakeError,
|
|
48
|
-
HwPositionOutputDataSchema: () => HwPositionOutputDataSchema,
|
|
49
42
|
INPUT_TYPES: () => INPUT_TYPES,
|
|
50
|
-
INPUT_TYPE_VALUES: () => INPUT_TYPE_VALUES,
|
|
51
|
-
InputCommandTypeSchema: () => InputCommandTypeSchema,
|
|
52
|
-
InputDataSchema: () => InputDataSchema,
|
|
53
|
-
InputFeatureSchema: () => InputFeatureSchema,
|
|
54
|
-
InputReadingSchema: () => InputReadingSchema,
|
|
55
|
-
InputTypeSchema: () => InputTypeSchema,
|
|
56
|
-
KeyframeSchema: () => KeyframeSchema,
|
|
57
|
-
MAX_MESSAGE_ID: () => MAX_MESSAGE_ID,
|
|
58
43
|
OUTPUT_TYPES: () => OUTPUT_TYPES,
|
|
59
|
-
OUTPUT_TYPE_VALUES: () => OUTPUT_TYPE_VALUES,
|
|
60
|
-
OutputCommandSchema: () => OutputCommandSchema,
|
|
61
|
-
OutputFeatureSchema: () => OutputFeatureSchema,
|
|
62
|
-
OutputTypeSchema: () => OutputTypeSchema,
|
|
63
44
|
PRESETS: () => PRESETS,
|
|
64
45
|
PRESET_NAMES: () => PRESET_NAMES,
|
|
65
|
-
PROTOCOL_VERSION_MAJOR: () => PROTOCOL_VERSION_MAJOR,
|
|
66
|
-
PROTOCOL_VERSION_MINOR: () => PROTOCOL_VERSION_MINOR,
|
|
67
|
-
PatternDescriptorSchema: () => PatternDescriptorSchema,
|
|
68
46
|
PatternEngine: () => PatternEngine,
|
|
69
|
-
PingManager: () => PingManager,
|
|
70
|
-
PositionValueSchema: () => PositionValueSchema,
|
|
71
47
|
ProtocolError: () => ProtocolError,
|
|
72
|
-
ReconnectDefaults: () => ReconnectDefaults,
|
|
73
|
-
ReconnectHandler: () => ReconnectHandler,
|
|
74
|
-
RotateWithDirectionOutputDataSchema: () => RotateWithDirectionOutputDataSchema,
|
|
75
|
-
RotationValueSchema: () => RotationValueSchema,
|
|
76
|
-
SensorValueSchema: () => SensorValueSchema,
|
|
77
|
-
ServerInfoSchema: () => ServerInfoSchema,
|
|
78
|
-
ServerMessageSchema: () => ServerMessageSchema,
|
|
79
|
-
SignedScalarOutputDataSchema: () => SignedScalarOutputDataSchema,
|
|
80
48
|
TimeoutError: () => TimeoutError,
|
|
81
|
-
UnsignedScalarOutputDataSchema: () => UnsignedScalarOutputDataSchema,
|
|
82
|
-
WebSocketTransport: () => WebSocketTransport,
|
|
83
49
|
consoleLogger: () => consoleLogger,
|
|
84
50
|
formatError: () => formatError,
|
|
85
|
-
getLogger: () => getLogger,
|
|
86
51
|
getPresetInfo: () => getPresetInfo,
|
|
87
|
-
noopLogger: () => noopLogger
|
|
88
|
-
runWithLogger: () => runWithLogger
|
|
52
|
+
noopLogger: () => noopLogger
|
|
89
53
|
});
|
|
90
54
|
module.exports = __toCommonJS(index_exports);
|
|
91
55
|
|
|
@@ -127,92 +91,6 @@ function createLogger(prefix) {
|
|
|
127
91
|
}
|
|
128
92
|
var consoleLogger = createLogger("buttplug");
|
|
129
93
|
|
|
130
|
-
// src/lib/context.ts
|
|
131
|
-
var currentLogger;
|
|
132
|
-
function getLogger() {
|
|
133
|
-
return currentLogger ?? noopLogger;
|
|
134
|
-
}
|
|
135
|
-
function runWithLogger(logger, fn) {
|
|
136
|
-
const prev = currentLogger;
|
|
137
|
-
currentLogger = logger;
|
|
138
|
-
try {
|
|
139
|
-
return fn();
|
|
140
|
-
} finally {
|
|
141
|
-
currentLogger = prev;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// src/core/utils.ts
|
|
146
|
-
function raceTimeout(promise, ms) {
|
|
147
|
-
return Promise.race([
|
|
148
|
-
promise,
|
|
149
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), ms))
|
|
150
|
-
]);
|
|
151
|
-
}
|
|
152
|
-
function outputFeaturesEqual(a, b) {
|
|
153
|
-
const sortedA = [...a].sort((x, y) => x.index - y.index);
|
|
154
|
-
const sortedB = [...b].sort((x, y) => x.index - y.index);
|
|
155
|
-
for (const [i, ao] of sortedA.entries()) {
|
|
156
|
-
const bo = sortedB[i];
|
|
157
|
-
if (!bo || ao.type !== bo.type || ao.index !== bo.index || ao.description !== bo.description || ao.range[0] !== bo.range[0] || ao.range[1] !== bo.range[1] || ao.durationRange?.[0] !== bo.durationRange?.[0] || ao.durationRange?.[1] !== bo.durationRange?.[1]) {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
function inputFeaturesEqual(a, b) {
|
|
164
|
-
const sortedA = [...a].sort((x, y) => x.index - y.index);
|
|
165
|
-
const sortedB = [...b].sort((x, y) => x.index - y.index);
|
|
166
|
-
for (const [i, ai] of sortedA.entries()) {
|
|
167
|
-
const bi = sortedB[i];
|
|
168
|
-
if (!bi || ai.type !== bi.type || ai.index !== bi.index || ai.description !== bi.description || ai.canRead !== bi.canRead || ai.canSubscribe !== bi.canSubscribe || ai.range[0] !== bi.range[0] || ai.range[1] !== bi.range[1]) {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
function featuresEqual(a, b) {
|
|
175
|
-
if (a.outputs.length !== b.outputs.length || a.inputs.length !== b.inputs.length) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
return outputFeaturesEqual(a.outputs, b.outputs) && inputFeaturesEqual(a.inputs, b.inputs);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// src/core/device-reconciler.ts
|
|
182
|
-
function reconcileDevices(options) {
|
|
183
|
-
const { currentDevices, incomingRaw, createDevice, callbacks } = options;
|
|
184
|
-
const logger = getLogger();
|
|
185
|
-
const incomingIndices = new Set(incomingRaw.map((d) => d.DeviceIndex));
|
|
186
|
-
const currentIndices = new Set(currentDevices.keys());
|
|
187
|
-
for (const index of currentIndices) {
|
|
188
|
-
if (!incomingIndices.has(index)) {
|
|
189
|
-
const device = currentDevices.get(index);
|
|
190
|
-
if (device) {
|
|
191
|
-
logger.debug(`Device removed: ${device.name} (index ${index})`);
|
|
192
|
-
currentDevices.delete(index);
|
|
193
|
-
callbacks.onRemoved(device);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
for (const rawDevice of incomingRaw) {
|
|
198
|
-
if (currentIndices.has(rawDevice.DeviceIndex)) {
|
|
199
|
-
const existingDevice = currentDevices.get(rawDevice.DeviceIndex);
|
|
200
|
-
const newDevice = createDevice(rawDevice);
|
|
201
|
-
if (existingDevice && !featuresEqual(existingDevice.features, newDevice.features)) {
|
|
202
|
-
currentDevices.set(rawDevice.DeviceIndex, newDevice);
|
|
203
|
-
logger.debug(`Device updated: ${newDevice.name} (index ${newDevice.index})`);
|
|
204
|
-
callbacks.onUpdated(newDevice, existingDevice);
|
|
205
|
-
}
|
|
206
|
-
} else {
|
|
207
|
-
const device = createDevice(rawDevice);
|
|
208
|
-
currentDevices.set(rawDevice.DeviceIndex, device);
|
|
209
|
-
logger.debug(`Device added: ${device.name} (index ${device.index})`);
|
|
210
|
-
callbacks.onAdded(device);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
callbacks.onList(Array.from(currentDevices.values()));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
94
|
// src/lib/errors.ts
|
|
217
95
|
var ErrorCode = {
|
|
218
96
|
UNKNOWN: 0,
|
|
@@ -286,12 +164,82 @@ function formatError(err) {
|
|
|
286
164
|
return err instanceof Error ? err.message : String(err);
|
|
287
165
|
}
|
|
288
166
|
|
|
167
|
+
// src/core/utils.ts
|
|
168
|
+
function raceTimeout(promise, ms) {
|
|
169
|
+
return Promise.race([
|
|
170
|
+
promise,
|
|
171
|
+
new Promise((_, reject) => setTimeout(() => reject(new TimeoutError("raceTimeout", ms)), ms))
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
function outputFeaturesEqual(a, b) {
|
|
175
|
+
const sortedA = [...a].sort((x, y) => x.index - y.index);
|
|
176
|
+
const sortedB = [...b].sort((x, y) => x.index - y.index);
|
|
177
|
+
for (const [i, ao] of sortedA.entries()) {
|
|
178
|
+
const bo = sortedB[i];
|
|
179
|
+
if (!bo || ao.type !== bo.type || ao.index !== bo.index || ao.description !== bo.description || ao.range[0] !== bo.range[0] || ao.range[1] !== bo.range[1] || ao.durationRange?.[0] !== bo.durationRange?.[0] || ao.durationRange?.[1] !== bo.durationRange?.[1]) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
function inputFeaturesEqual(a, b) {
|
|
186
|
+
const sortedA = [...a].sort((x, y) => x.index - y.index);
|
|
187
|
+
const sortedB = [...b].sort((x, y) => x.index - y.index);
|
|
188
|
+
for (const [i, ai] of sortedA.entries()) {
|
|
189
|
+
const bi = sortedB[i];
|
|
190
|
+
if (!bi || ai.type !== bi.type || ai.index !== bi.index || ai.description !== bi.description || ai.canRead !== bi.canRead || ai.canSubscribe !== bi.canSubscribe || ai.range[0] !== bi.range[0] || ai.range[1] !== bi.range[1]) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
function featuresEqual(a, b) {
|
|
197
|
+
if (a.outputs.length !== b.outputs.length || a.inputs.length !== b.inputs.length) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return outputFeaturesEqual(a.outputs, b.outputs) && inputFeaturesEqual(a.inputs, b.inputs);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/core/device-reconciler.ts
|
|
204
|
+
function reconcileDevices(options) {
|
|
205
|
+
const { currentDevices, incomingRaw, createDevice, callbacks } = options;
|
|
206
|
+
const logger = options.logger ?? noopLogger;
|
|
207
|
+
const incomingIndices = new Set(incomingRaw.map((d) => d.DeviceIndex));
|
|
208
|
+
const currentIndices = new Set(currentDevices.keys());
|
|
209
|
+
for (const index of currentIndices) {
|
|
210
|
+
if (!incomingIndices.has(index)) {
|
|
211
|
+
const device = currentDevices.get(index);
|
|
212
|
+
if (device) {
|
|
213
|
+
logger.debug(`Device removed: ${device.name} (index ${index})`);
|
|
214
|
+
currentDevices.delete(index);
|
|
215
|
+
callbacks.onRemoved(device);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const rawDevice of incomingRaw) {
|
|
220
|
+
if (currentIndices.has(rawDevice.DeviceIndex)) {
|
|
221
|
+
const existingDevice = currentDevices.get(rawDevice.DeviceIndex);
|
|
222
|
+
const newDevice = createDevice(rawDevice);
|
|
223
|
+
if (existingDevice && !featuresEqual(existingDevice.features, newDevice.features)) {
|
|
224
|
+
currentDevices.set(rawDevice.DeviceIndex, newDevice);
|
|
225
|
+
logger.debug(`Device updated: ${newDevice.name} (index ${newDevice.index})`);
|
|
226
|
+
callbacks.onUpdated(newDevice, existingDevice);
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
const device = createDevice(rawDevice);
|
|
230
|
+
currentDevices.set(rawDevice.DeviceIndex, device);
|
|
231
|
+
logger.debug(`Device added: ${device.name} (index ${device.index})`);
|
|
232
|
+
callbacks.onAdded(device);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
callbacks.onList(Array.from(currentDevices.values()));
|
|
236
|
+
}
|
|
237
|
+
|
|
289
238
|
// src/protocol/constants.ts
|
|
290
239
|
var PROTOCOL_VERSION_MAJOR = 4;
|
|
291
240
|
var PROTOCOL_VERSION_MINOR = 0;
|
|
292
241
|
var DEFAULT_CLIENT_NAME = "buttplug.js";
|
|
293
242
|
var DEFAULT_REQUEST_TIMEOUT = 1e4;
|
|
294
|
-
var DEFAULT_PING_INTERVAL = 1e3;
|
|
295
243
|
var MAX_MESSAGE_ID = 4294967295;
|
|
296
244
|
|
|
297
245
|
// src/protocol/messages.ts
|
|
@@ -332,7 +280,7 @@ function createDisconnect(id) {
|
|
|
332
280
|
}
|
|
333
281
|
function createStopCmd(id, options) {
|
|
334
282
|
if (options?.featureIndex !== void 0 && options.deviceIndex === void 0) {
|
|
335
|
-
throw new
|
|
283
|
+
throw new ProtocolError(ErrorCode.MESSAGE, "StopCmd: featureIndex requires deviceIndex to be set");
|
|
336
284
|
}
|
|
337
285
|
return {
|
|
338
286
|
StopCmd: {
|
|
@@ -344,9 +292,6 @@ function createStopCmd(id, options) {
|
|
|
344
292
|
}
|
|
345
293
|
};
|
|
346
294
|
}
|
|
347
|
-
function serializeMessage(message) {
|
|
348
|
-
return JSON.stringify([message]);
|
|
349
|
-
}
|
|
350
295
|
function serializeMessages(messages) {
|
|
351
296
|
return JSON.stringify(messages);
|
|
352
297
|
}
|
|
@@ -649,11 +594,7 @@ var MessageRouter = class {
|
|
|
649
594
|
this.#onInputReading = options.onInputReading;
|
|
650
595
|
this.#onError = options.onError;
|
|
651
596
|
}
|
|
652
|
-
/**
|
|
653
|
-
* Returns the next message ID, wrapping around at {@link MAX_MESSAGE_ID}.
|
|
654
|
-
*
|
|
655
|
-
* @returns A monotonically increasing ID for use in outgoing messages
|
|
656
|
-
*/
|
|
597
|
+
/** Returns the next auto-incrementing message ID. */
|
|
657
598
|
nextId() {
|
|
658
599
|
this.#messageId = this.#messageId % MAX_MESSAGE_ID + 1;
|
|
659
600
|
return this.#messageId;
|
|
@@ -681,6 +622,13 @@ var MessageRouter = class {
|
|
|
681
622
|
this.#pending.delete(id);
|
|
682
623
|
reject(new TimeoutError(`Request (ID ${id})`, this.#timeout));
|
|
683
624
|
}, this.#timeout);
|
|
625
|
+
const existing = this.#pending.get(id);
|
|
626
|
+
if (existing) {
|
|
627
|
+
if (existing.timeout !== null) {
|
|
628
|
+
clearTimeout(existing.timeout);
|
|
629
|
+
}
|
|
630
|
+
existing.reject(new ProtocolError(ErrorCode.MESSAGE, `Request ${id} superseded by new request`));
|
|
631
|
+
}
|
|
684
632
|
this.#pending.set(id, {
|
|
685
633
|
resolve,
|
|
686
634
|
reject: (err) => {
|
|
@@ -866,6 +814,7 @@ var SensorHandler = class {
|
|
|
866
814
|
* @param key - Composite sensor key from {@link sensorKey}
|
|
867
815
|
* @param callback - Function invoked with the sensor value on each reading
|
|
868
816
|
* @param info - Device index, feature index, and input type for the sensor
|
|
817
|
+
* @throws {Error} if a subscription already exists for the given key
|
|
869
818
|
*/
|
|
870
819
|
register(key, callback, info) {
|
|
871
820
|
if (this.#subscriptions.has(key)) {
|
|
@@ -969,11 +918,7 @@ var SensorHandler = class {
|
|
|
969
918
|
function validateRange(value, range) {
|
|
970
919
|
const [min, max] = range;
|
|
971
920
|
const rounded = Math.round(value);
|
|
972
|
-
|
|
973
|
-
if (rounded !== clamped) {
|
|
974
|
-
getLogger().debug(`Value ${value} clamped to ${clamped} (range [${min}, ${max}])`);
|
|
975
|
-
}
|
|
976
|
-
return clamped;
|
|
921
|
+
return Math.max(min, Math.min(max, rounded));
|
|
977
922
|
}
|
|
978
923
|
|
|
979
924
|
// src/builders/commands.ts
|
|
@@ -1006,12 +951,9 @@ function buildPositionMessages(options) {
|
|
|
1006
951
|
throw new DeviceError(deviceIndex, "Values array must not be empty");
|
|
1007
952
|
}
|
|
1008
953
|
for (const p of position) {
|
|
1009
|
-
const feature = features
|
|
954
|
+
const feature = features.find((f) => f.index === p.index);
|
|
1010
955
|
if (!feature) {
|
|
1011
|
-
throw new DeviceError(
|
|
1012
|
-
deviceIndex,
|
|
1013
|
-
`Invalid position index ${p.index} (device has ${features.length} position feature(s))`
|
|
1014
|
-
);
|
|
956
|
+
throw new DeviceError(deviceIndex, `Position feature index ${p.index} not found on device`);
|
|
1015
957
|
}
|
|
1016
958
|
messages.push(
|
|
1017
959
|
buildPositionMessage({
|
|
@@ -1039,12 +981,9 @@ function buildRotateMessages(options) {
|
|
|
1039
981
|
throw new DeviceError(deviceIndex, "Values array must not be empty");
|
|
1040
982
|
}
|
|
1041
983
|
for (const r of speed) {
|
|
1042
|
-
const feature = features
|
|
984
|
+
const feature = features.find((f) => f.index === r.index);
|
|
1043
985
|
if (!feature) {
|
|
1044
|
-
throw new DeviceError(
|
|
1045
|
-
deviceIndex,
|
|
1046
|
-
`Invalid rotation index ${r.index} (device has ${features.length} rotation feature(s))`
|
|
1047
|
-
);
|
|
986
|
+
throw new DeviceError(deviceIndex, `Rotation feature index ${r.index} not found on device`);
|
|
1048
987
|
}
|
|
1049
988
|
const validatedValue = validateRange(r.speed, feature.range);
|
|
1050
989
|
const command = rotationType === "RotateWithDirection" ? { RotateWithDirection: { Value: validatedValue, Clockwise: r.clockwise } } : { Rotate: { Value: validatedValue } };
|
|
@@ -1083,12 +1022,9 @@ function buildScalarOutputMessages(options) {
|
|
|
1083
1022
|
}
|
|
1084
1023
|
const messages2 = [];
|
|
1085
1024
|
for (const entry of values) {
|
|
1086
|
-
const feature = features
|
|
1025
|
+
const feature = features.find((f) => f.index === entry.index);
|
|
1087
1026
|
if (!feature) {
|
|
1088
|
-
throw new DeviceError(
|
|
1089
|
-
deviceIndex,
|
|
1090
|
-
`Invalid ${errorLabel} index ${entry.index} (device has ${features.length} ${errorLabel} feature(s))`
|
|
1091
|
-
);
|
|
1027
|
+
throw new DeviceError(deviceIndex, `${errorLabel} feature index ${entry.index} not found on device`);
|
|
1092
1028
|
}
|
|
1093
1029
|
const validatedValue = validateRange(entry.value, feature.range);
|
|
1094
1030
|
const id = client.nextId();
|
|
@@ -1172,11 +1108,10 @@ function getInputIndex(features) {
|
|
|
1172
1108
|
}
|
|
1173
1109
|
var KNOWN_OUTPUT_KEYS = new Set(OUTPUT_TYPES);
|
|
1174
1110
|
var KNOWN_INPUT_KEYS = new Set(INPUT_TYPES);
|
|
1175
|
-
function collectOutputs(feature) {
|
|
1111
|
+
function collectOutputs(feature, logger) {
|
|
1176
1112
|
if (!feature.Output) {
|
|
1177
1113
|
return [];
|
|
1178
1114
|
}
|
|
1179
|
-
const logger = getLogger();
|
|
1180
1115
|
const results = [];
|
|
1181
1116
|
for (const key of Object.keys(feature.Output)) {
|
|
1182
1117
|
if (!KNOWN_OUTPUT_KEYS.has(key)) {
|
|
@@ -1191,11 +1126,10 @@ function collectOutputs(feature) {
|
|
|
1191
1126
|
}
|
|
1192
1127
|
return results;
|
|
1193
1128
|
}
|
|
1194
|
-
function collectInputs(feature) {
|
|
1129
|
+
function collectInputs(feature, logger) {
|
|
1195
1130
|
if (!feature.Input) {
|
|
1196
1131
|
return [];
|
|
1197
1132
|
}
|
|
1198
|
-
const logger = getLogger();
|
|
1199
1133
|
const results = [];
|
|
1200
1134
|
for (const key of Object.keys(feature.Input)) {
|
|
1201
1135
|
if (!KNOWN_INPUT_KEYS.has(key)) {
|
|
@@ -1210,16 +1144,16 @@ function collectInputs(feature) {
|
|
|
1210
1144
|
}
|
|
1211
1145
|
return results;
|
|
1212
1146
|
}
|
|
1213
|
-
function parseFeatures(raw) {
|
|
1147
|
+
function parseFeatures(raw, logger = noopLogger) {
|
|
1214
1148
|
const outputs = [];
|
|
1215
1149
|
const inputs = [];
|
|
1216
1150
|
const features = raw.DeviceFeatures ?? {};
|
|
1217
1151
|
const sortedFeatures = Object.values(features).sort((a, b) => a.FeatureIndex - b.FeatureIndex);
|
|
1218
1152
|
for (const feature of sortedFeatures) {
|
|
1219
|
-
for (const output of collectOutputs(feature)) {
|
|
1153
|
+
for (const output of collectOutputs(feature, logger)) {
|
|
1220
1154
|
outputs.push(output);
|
|
1221
1155
|
}
|
|
1222
|
-
for (const input of collectInputs(feature)) {
|
|
1156
|
+
for (const input of collectInputs(feature, logger)) {
|
|
1223
1157
|
inputs.push(input);
|
|
1224
1158
|
}
|
|
1225
1159
|
}
|
|
@@ -1272,8 +1206,8 @@ var Device = class {
|
|
|
1272
1206
|
constructor(options) {
|
|
1273
1207
|
this.#client = options.client;
|
|
1274
1208
|
this.#raw = options.raw;
|
|
1275
|
-
this.#logger = (options.logger ??
|
|
1276
|
-
this.#features = parseFeatures(options.raw);
|
|
1209
|
+
this.#logger = (options.logger ?? noopLogger).child("device");
|
|
1210
|
+
this.#features = parseFeatures(options.raw, this.#logger);
|
|
1277
1211
|
}
|
|
1278
1212
|
/**
|
|
1279
1213
|
* Sets vibration intensity on all or individual motors.
|
|
@@ -1496,6 +1430,7 @@ var Device = class {
|
|
|
1496
1430
|
*
|
|
1497
1431
|
* @param type - The sensor type to unsubscribe from
|
|
1498
1432
|
* @param sensorIndex - Index of the sensor if the device has multiple of the same type
|
|
1433
|
+
* @throws {DeviceError} if the sensor does not exist at the given index
|
|
1499
1434
|
*/
|
|
1500
1435
|
async unsubscribe(type, sensorIndex = 0) {
|
|
1501
1436
|
const features = getInputsByType(this.#features, type);
|
|
@@ -1912,6 +1847,8 @@ var PingManager = class {
|
|
|
1912
1847
|
#pingTimer = null;
|
|
1913
1848
|
/** Tracks whether a ping request is currently awaiting a response. */
|
|
1914
1849
|
#pingInFlight = false;
|
|
1850
|
+
/** Whether the manager is in a stopped state. */
|
|
1851
|
+
#stopped = true;
|
|
1915
1852
|
/** Maximum time in ms the server allows between pings. */
|
|
1916
1853
|
#maxPingTime = 0;
|
|
1917
1854
|
constructor(options) {
|
|
@@ -1939,6 +1876,7 @@ var PingManager = class {
|
|
|
1939
1876
|
this.#cancelPing(new TimeoutError("Ping", 0));
|
|
1940
1877
|
}
|
|
1941
1878
|
this.stop();
|
|
1879
|
+
this.#stopped = false;
|
|
1942
1880
|
this.#maxPingTime = maxPingTime;
|
|
1943
1881
|
if (!this.#autoPing || maxPingTime <= 0) {
|
|
1944
1882
|
return;
|
|
@@ -1961,6 +1899,7 @@ var PingManager = class {
|
|
|
1961
1899
|
}
|
|
1962
1900
|
/** Stops the ping timer and resets in-flight state. */
|
|
1963
1901
|
stop() {
|
|
1902
|
+
this.#stopped = true;
|
|
1964
1903
|
if (this.#pingTimer !== null) {
|
|
1965
1904
|
clearInterval(this.#pingTimer);
|
|
1966
1905
|
this.#pingTimer = null;
|
|
@@ -1970,14 +1909,23 @@ var PingManager = class {
|
|
|
1970
1909
|
}
|
|
1971
1910
|
/** Sends a single ping and handles timeout or failure. */
|
|
1972
1911
|
async #doPing() {
|
|
1912
|
+
if (this.#stopped) {
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1973
1915
|
this.#logger.debug("Sending ping");
|
|
1974
1916
|
const maxPingTime = this.#maxPingTime || DEFAULT_PING_TIMEOUT_MS;
|
|
1975
1917
|
const timer = setTimeout(() => {
|
|
1918
|
+
if (this.#stopped) {
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1976
1921
|
this.#cancelPing(new TimeoutError("Ping", maxPingTime));
|
|
1977
1922
|
}, maxPingTime);
|
|
1978
1923
|
try {
|
|
1979
1924
|
await this.#sendPing();
|
|
1980
1925
|
} catch (err) {
|
|
1926
|
+
if (this.#stopped) {
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1981
1929
|
const isTimeout = err instanceof TimeoutError;
|
|
1982
1930
|
this.#logger.error(`Ping failed: ${formatError(err)}`);
|
|
1983
1931
|
this.#onError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -2071,7 +2019,12 @@ var ReconnectHandler = class {
|
|
|
2071
2019
|
/** Safely invokes a user callback, catching and logging any errors. */
|
|
2072
2020
|
#safeCallback(name, fn) {
|
|
2073
2021
|
try {
|
|
2074
|
-
fn();
|
|
2022
|
+
const result = fn();
|
|
2023
|
+
if (result instanceof Promise) {
|
|
2024
|
+
result.catch((err) => {
|
|
2025
|
+
this.#logger.error(`Error in async ${name} callback: ${formatError(err)}`);
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2075
2028
|
} catch (err) {
|
|
2076
2029
|
this.#logger.error(`Error in ${name} callback: ${formatError(err)}`);
|
|
2077
2030
|
}
|
|
@@ -2146,6 +2099,7 @@ var ButtplugClient = class extends import_emittery.default {
|
|
|
2146
2099
|
#serverInfo = null;
|
|
2147
2100
|
#connectPromise = null;
|
|
2148
2101
|
#isHandshaking = false;
|
|
2102
|
+
#disconnecting = false;
|
|
2149
2103
|
constructor(url, options = {}) {
|
|
2150
2104
|
super();
|
|
2151
2105
|
this.#url = url;
|
|
@@ -2157,9 +2111,10 @@ var ButtplugClient = class extends import_emittery.default {
|
|
|
2157
2111
|
this.#messageRouter.handleMessage(data);
|
|
2158
2112
|
});
|
|
2159
2113
|
this.#transport.on("close", (_code, reason) => {
|
|
2160
|
-
this.#safetyStop();
|
|
2161
2114
|
this.#pingManager.stop();
|
|
2162
|
-
this
|
|
2115
|
+
if (!this.#disconnecting) {
|
|
2116
|
+
this.emit("disconnected", { reason });
|
|
2117
|
+
}
|
|
2163
2118
|
});
|
|
2164
2119
|
this.#transport.on("error", (error) => {
|
|
2165
2120
|
this.emit("error", { error });
|
|
@@ -2168,20 +2123,18 @@ var ButtplugClient = class extends import_emittery.default {
|
|
|
2168
2123
|
send: (data) => this.#transport.send(data),
|
|
2169
2124
|
timeout: options.requestTimeout,
|
|
2170
2125
|
logger: this.#baseLogger,
|
|
2171
|
-
onDeviceList: (devices) =>
|
|
2172
|
-
this.#
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
})
|
|
2184
|
-
),
|
|
2126
|
+
onDeviceList: (devices) => reconcileDevices({
|
|
2127
|
+
currentDevices: this.#devices,
|
|
2128
|
+
incomingRaw: devices,
|
|
2129
|
+
createDevice: (raw) => new Device({ client: this, raw, logger: this.#baseLogger }),
|
|
2130
|
+
logger: this.#logger,
|
|
2131
|
+
callbacks: {
|
|
2132
|
+
onAdded: (d) => this.emit("deviceAdded", { device: d }),
|
|
2133
|
+
onRemoved: (d) => this.emit("deviceRemoved", { device: d }),
|
|
2134
|
+
onUpdated: (d, old) => this.emit("deviceUpdated", { device: d, previousDevice: old }),
|
|
2135
|
+
onList: (list) => this.emit("deviceList", { devices: list })
|
|
2136
|
+
}
|
|
2137
|
+
}),
|
|
2185
2138
|
onScanningFinished: () => {
|
|
2186
2139
|
this.#scanning = false;
|
|
2187
2140
|
this.emit("scanningFinished", void 0);
|
|
@@ -2282,34 +2235,45 @@ var ButtplugClient = class extends import_emittery.default {
|
|
|
2282
2235
|
* @param reason - Optional human-readable reason for the disconnection
|
|
2283
2236
|
*/
|
|
2284
2237
|
async disconnect(reason) {
|
|
2285
|
-
|
|
2286
|
-
|
|
2238
|
+
this.#disconnecting = true;
|
|
2239
|
+
try {
|
|
2240
|
+
const disconnectReason = reason ?? "Client disconnected";
|
|
2241
|
+
let emitted = false;
|
|
2242
|
+
if (this.#reconnectHandler?.active) {
|
|
2243
|
+
this.#reconnectHandler.cancel();
|
|
2244
|
+
this.#pingManager.stop();
|
|
2245
|
+
this.emit("disconnected", { reason: disconnectReason });
|
|
2246
|
+
emitted = true;
|
|
2247
|
+
}
|
|
2248
|
+
if (!this.connected) {
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
this.#logger.info(`Disconnecting${reason ? `: ${reason}` : ""}`);
|
|
2287
2252
|
this.#pingManager.stop();
|
|
2288
|
-
this
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2253
|
+
this.#reconnectHandler?.cancel();
|
|
2254
|
+
if (this.#serverInfo !== null && !this.#isHandshaking) {
|
|
2255
|
+
try {
|
|
2256
|
+
await raceTimeout(this.stopAll(), STOP_DEVICES_TIMEOUT_MS);
|
|
2257
|
+
} catch {
|
|
2258
|
+
this.#logger.warn("Stop all devices timed out during disconnect");
|
|
2259
|
+
}
|
|
2260
|
+
try {
|
|
2261
|
+
await raceTimeout(
|
|
2262
|
+
this.#messageRouter.send(createDisconnect(this.#messageRouter.nextId())),
|
|
2263
|
+
DISCONNECT_TIMEOUT_MS
|
|
2264
|
+
);
|
|
2265
|
+
} catch {
|
|
2266
|
+
this.#logger.warn("Disconnect message failed or timed out");
|
|
2267
|
+
}
|
|
2301
2268
|
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
);
|
|
2307
|
-
} catch {
|
|
2308
|
-
this.#logger.warn("Disconnect message failed or timed out");
|
|
2269
|
+
this.#messageRouter.cancelAll(new ConnectionError("Client disconnected"));
|
|
2270
|
+
await this.#transport.disconnect();
|
|
2271
|
+
if (!emitted) {
|
|
2272
|
+
this.emit("disconnected", { reason: disconnectReason });
|
|
2309
2273
|
}
|
|
2274
|
+
} finally {
|
|
2275
|
+
this.#disconnecting = false;
|
|
2310
2276
|
}
|
|
2311
|
-
this.#messageRouter.cancelAll(new ConnectionError("Client disconnected"));
|
|
2312
|
-
await this.#transport.disconnect();
|
|
2313
2277
|
}
|
|
2314
2278
|
/**
|
|
2315
2279
|
* Begins scanning for devices on the server.
|
|
@@ -2354,20 +2318,18 @@ var ButtplugClient = class extends import_emittery.default {
|
|
|
2354
2318
|
for (const response of responses) {
|
|
2355
2319
|
if (isDeviceList(response)) {
|
|
2356
2320
|
const deviceList = getDeviceList(response);
|
|
2357
|
-
|
|
2358
|
-
this.#
|
|
2359
|
-
()
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
})
|
|
2370
|
-
);
|
|
2321
|
+
reconcileDevices({
|
|
2322
|
+
currentDevices: this.#devices,
|
|
2323
|
+
incomingRaw: Object.values(deviceList.Devices),
|
|
2324
|
+
createDevice: (raw) => new Device({ client: this, raw, logger: this.#baseLogger }),
|
|
2325
|
+
logger: this.#logger,
|
|
2326
|
+
callbacks: {
|
|
2327
|
+
onAdded: (d) => this.emit("deviceAdded", { device: d }),
|
|
2328
|
+
onRemoved: (d) => this.emit("deviceRemoved", { device: d }),
|
|
2329
|
+
onUpdated: (d, old) => this.emit("deviceUpdated", { device: d, previousDevice: old }),
|
|
2330
|
+
onList: (list) => this.emit("deviceList", { devices: list })
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2371
2333
|
}
|
|
2372
2334
|
}
|
|
2373
2335
|
}
|
|
@@ -2433,16 +2395,18 @@ var ButtplugClient = class extends import_emittery.default {
|
|
|
2433
2395
|
get devices() {
|
|
2434
2396
|
return Array.from(this.#devices.values());
|
|
2435
2397
|
}
|
|
2436
|
-
/**
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2398
|
+
/**
|
|
2399
|
+
* Disposes the client, clearing all event listeners and internal state.
|
|
2400
|
+
*
|
|
2401
|
+
* Callers should {@link disconnect} first if still connected.
|
|
2402
|
+
* Subsequent usage of the client after disposal is undefined behavior.
|
|
2403
|
+
*/
|
|
2404
|
+
dispose() {
|
|
2405
|
+
this.clearListeners();
|
|
2406
|
+
this.#pingManager.stop();
|
|
2407
|
+
this.#sensorHandler.clear();
|
|
2408
|
+
this.#reconnectHandler?.cancel();
|
|
2409
|
+
this.#devices.clear();
|
|
2446
2410
|
}
|
|
2447
2411
|
/**
|
|
2448
2412
|
* Validates connection state before performing an action.
|
|
@@ -2502,59 +2466,6 @@ var ButtplugClient = class extends import_emittery.default {
|
|
|
2502
2466
|
}
|
|
2503
2467
|
};
|
|
2504
2468
|
|
|
2505
|
-
// src/patterns/easing.ts
|
|
2506
|
-
var clamp = (t) => Math.min(1, Math.max(0, t));
|
|
2507
|
-
var linear = (t) => clamp(t);
|
|
2508
|
-
var easeIn = (t) => clamp(t) ** 3;
|
|
2509
|
-
var easeOut = (t) => 1 - (1 - clamp(t)) ** 3;
|
|
2510
|
-
var easeInOut = (t) => {
|
|
2511
|
-
const c = clamp(t);
|
|
2512
|
-
return c < 0.5 ? 4 * c ** 3 : 1 - (-2 * c + 2) ** 3 / 2;
|
|
2513
|
-
};
|
|
2514
|
-
var step = (t) => clamp(t) < 1 ? 0 : 1;
|
|
2515
|
-
var EASING_FUNCTIONS = {
|
|
2516
|
-
linear,
|
|
2517
|
-
easeIn,
|
|
2518
|
-
easeOut,
|
|
2519
|
-
easeInOut,
|
|
2520
|
-
step
|
|
2521
|
-
};
|
|
2522
|
-
var ease = (t, easing) => {
|
|
2523
|
-
const fn = EASING_FUNCTIONS[easing];
|
|
2524
|
-
return fn ? fn(t) : clamp(t);
|
|
2525
|
-
};
|
|
2526
|
-
|
|
2527
|
-
// src/patterns/types.ts
|
|
2528
|
-
var import_zod2 = require("zod");
|
|
2529
|
-
var EASING_VALUES = ["linear", "easeIn", "easeOut", "easeInOut", "step"];
|
|
2530
|
-
var EasingSchema = import_zod2.z.enum(EASING_VALUES);
|
|
2531
|
-
var KeyframeSchema = import_zod2.z.object({
|
|
2532
|
-
value: import_zod2.z.number().min(0).max(1),
|
|
2533
|
-
duration: import_zod2.z.number().int().nonnegative(),
|
|
2534
|
-
easing: EasingSchema.optional()
|
|
2535
|
-
});
|
|
2536
|
-
var TrackSchema = import_zod2.z.object({
|
|
2537
|
-
featureIndex: import_zod2.z.number().int().nonnegative(),
|
|
2538
|
-
keyframes: import_zod2.z.array(KeyframeSchema).min(1),
|
|
2539
|
-
clockwise: import_zod2.z.boolean().optional(),
|
|
2540
|
-
outputType: OutputTypeSchema.optional()
|
|
2541
|
-
});
|
|
2542
|
-
var PRESET_NAMES = ["pulse", "wave", "ramp_up", "ramp_down", "heartbeat", "surge", "stroke"];
|
|
2543
|
-
var PresetPatternSchema = import_zod2.z.object({
|
|
2544
|
-
type: import_zod2.z.literal("preset"),
|
|
2545
|
-
preset: import_zod2.z.enum(PRESET_NAMES),
|
|
2546
|
-
intensity: import_zod2.z.number().min(0).max(1).optional(),
|
|
2547
|
-
speed: import_zod2.z.number().min(0.25).max(4).optional(),
|
|
2548
|
-
loop: import_zod2.z.union([import_zod2.z.boolean(), import_zod2.z.number().int().positive()]).optional()
|
|
2549
|
-
});
|
|
2550
|
-
var CustomPatternSchema = import_zod2.z.object({
|
|
2551
|
-
type: import_zod2.z.literal("custom"),
|
|
2552
|
-
tracks: import_zod2.z.array(TrackSchema).min(1),
|
|
2553
|
-
intensity: import_zod2.z.number().min(0).max(1).optional(),
|
|
2554
|
-
loop: import_zod2.z.union([import_zod2.z.boolean(), import_zod2.z.number().int().positive()]).optional()
|
|
2555
|
-
});
|
|
2556
|
-
var PatternDescriptorSchema = import_zod2.z.discriminatedUnion("type", [PresetPatternSchema, CustomPatternSchema]);
|
|
2557
|
-
|
|
2558
2469
|
// src/patterns/presets.ts
|
|
2559
2470
|
var MOTOR_OUTPUT_TYPES = ["Vibrate", "Rotate", "RotateWithDirection", "Oscillate", "Constrict"];
|
|
2560
2471
|
var POSITION_OUTPUT_TYPES = ["Position", "HwPositionWithDuration"];
|
|
@@ -2662,6 +2573,28 @@ function getPresetInfo() {
|
|
|
2662
2573
|
}));
|
|
2663
2574
|
}
|
|
2664
2575
|
|
|
2576
|
+
// src/patterns/easing.ts
|
|
2577
|
+
var clamp = (t) => Math.min(1, Math.max(0, t));
|
|
2578
|
+
var linear = (t) => clamp(t);
|
|
2579
|
+
var easeIn = (t) => clamp(t) ** 3;
|
|
2580
|
+
var easeOut = (t) => 1 - (1 - clamp(t)) ** 3;
|
|
2581
|
+
var easeInOut = (t) => {
|
|
2582
|
+
const c = clamp(t);
|
|
2583
|
+
return c < 0.5 ? 4 * c ** 3 : 1 - (-2 * c + 2) ** 3 / 2;
|
|
2584
|
+
};
|
|
2585
|
+
var step = (t) => clamp(t) < 1 ? 0 : 1;
|
|
2586
|
+
var EASING_FUNCTIONS = {
|
|
2587
|
+
linear,
|
|
2588
|
+
easeIn,
|
|
2589
|
+
easeOut,
|
|
2590
|
+
easeInOut,
|
|
2591
|
+
step
|
|
2592
|
+
};
|
|
2593
|
+
var ease = (t, easing) => {
|
|
2594
|
+
const fn = EASING_FUNCTIONS[easing];
|
|
2595
|
+
return fn ? fn(t) : clamp(t);
|
|
2596
|
+
};
|
|
2597
|
+
|
|
2665
2598
|
// src/patterns/scheduler.ts
|
|
2666
2599
|
function interpolateKeyframes(keyframes, elapsed) {
|
|
2667
2600
|
let accumulated = 0;
|
|
@@ -2702,8 +2635,14 @@ function buildScalarCommand(track, value) {
|
|
|
2702
2635
|
return { Constrict: { Value: value } };
|
|
2703
2636
|
case "Position":
|
|
2704
2637
|
return { Position: { Value: value } };
|
|
2638
|
+
case "Spray":
|
|
2639
|
+
return { Spray: { Value: value } };
|
|
2640
|
+
case "Temperature":
|
|
2641
|
+
return { Temperature: { Value: value } };
|
|
2642
|
+
case "Led":
|
|
2643
|
+
return { Led: { Value: value } };
|
|
2705
2644
|
default:
|
|
2706
|
-
|
|
2645
|
+
throw new DeviceError(0, `Unsupported output type in pattern: ${track.outputType}`);
|
|
2707
2646
|
}
|
|
2708
2647
|
}
|
|
2709
2648
|
function getCycleDuration(tracks) {
|
|
@@ -2727,7 +2666,10 @@ function evaluateScalarTrack(state, track, elapsed, device, buildCommand, onErro
|
|
|
2727
2666
|
return;
|
|
2728
2667
|
}
|
|
2729
2668
|
const command = buildCommand(track, mapped);
|
|
2730
|
-
device.output({ featureIndex, command }).catch((err) =>
|
|
2669
|
+
device.output({ featureIndex, command }).catch((err) => {
|
|
2670
|
+
state.lastSentValues.delete(featureIndex);
|
|
2671
|
+
onError(state, err);
|
|
2672
|
+
});
|
|
2731
2673
|
state.lastSentValues.set(featureIndex, mapped);
|
|
2732
2674
|
}
|
|
2733
2675
|
function evaluateHwPositionTrack(state, track, elapsed, device, onError) {
|
|
@@ -2854,6 +2796,37 @@ function resolveCustomTracks(device, descriptor) {
|
|
|
2854
2796
|
return tracks;
|
|
2855
2797
|
}
|
|
2856
2798
|
|
|
2799
|
+
// src/patterns/types.ts
|
|
2800
|
+
var import_zod2 = require("zod");
|
|
2801
|
+
var EASING_VALUES = ["linear", "easeIn", "easeOut", "easeInOut", "step"];
|
|
2802
|
+
var EasingSchema = import_zod2.z.enum(EASING_VALUES);
|
|
2803
|
+
var KeyframeSchema = import_zod2.z.object({
|
|
2804
|
+
value: import_zod2.z.number().min(0).max(1),
|
|
2805
|
+
duration: import_zod2.z.number().int().nonnegative(),
|
|
2806
|
+
easing: EasingSchema.optional()
|
|
2807
|
+
});
|
|
2808
|
+
var TrackSchema = import_zod2.z.object({
|
|
2809
|
+
featureIndex: import_zod2.z.number().int().nonnegative(),
|
|
2810
|
+
keyframes: import_zod2.z.array(KeyframeSchema).min(1),
|
|
2811
|
+
clockwise: import_zod2.z.boolean().optional(),
|
|
2812
|
+
outputType: OutputTypeSchema.optional()
|
|
2813
|
+
});
|
|
2814
|
+
var PRESET_NAMES = ["pulse", "wave", "ramp_up", "ramp_down", "heartbeat", "surge", "stroke"];
|
|
2815
|
+
var PresetPatternSchema = import_zod2.z.object({
|
|
2816
|
+
type: import_zod2.z.literal("preset"),
|
|
2817
|
+
preset: import_zod2.z.enum(PRESET_NAMES),
|
|
2818
|
+
intensity: import_zod2.z.number().min(0).max(1).optional(),
|
|
2819
|
+
speed: import_zod2.z.number().min(0.25).max(4).optional(),
|
|
2820
|
+
loop: import_zod2.z.union([import_zod2.z.boolean(), import_zod2.z.number().int().positive()]).optional()
|
|
2821
|
+
});
|
|
2822
|
+
var CustomPatternSchema = import_zod2.z.object({
|
|
2823
|
+
type: import_zod2.z.literal("custom"),
|
|
2824
|
+
tracks: import_zod2.z.array(TrackSchema).min(1),
|
|
2825
|
+
intensity: import_zod2.z.number().min(0).max(1).optional(),
|
|
2826
|
+
loop: import_zod2.z.union([import_zod2.z.boolean(), import_zod2.z.number().int().positive()]).optional()
|
|
2827
|
+
});
|
|
2828
|
+
var PatternDescriptorSchema = import_zod2.z.discriminatedUnion("type", [PresetPatternSchema, CustomPatternSchema]);
|
|
2829
|
+
|
|
2857
2830
|
// src/patterns/engine.ts
|
|
2858
2831
|
var DEFAULT_TIMEOUT_MS = 18e5;
|
|
2859
2832
|
var MIN_TICK_INTERVAL_MS = 50;
|
|
@@ -3019,7 +2992,8 @@ var PatternEngine = class {
|
|
|
3019
2992
|
if (state.stopped) {
|
|
3020
2993
|
return;
|
|
3021
2994
|
}
|
|
3022
|
-
const
|
|
2995
|
+
const now = performance.now();
|
|
2996
|
+
const elapsed = now - state.startedAt;
|
|
3023
2997
|
const cycleDuration = getCycleDuration(state.tracks);
|
|
3024
2998
|
const cycleElapsed = cycleDuration > 0 && elapsed >= cycleDuration ? cycleDuration : elapsed;
|
|
3025
2999
|
const onError = (s, err) => this.#handleOutputError(s, err);
|
|
@@ -3043,9 +3017,9 @@ var PatternEngine = class {
|
|
|
3043
3017
|
return;
|
|
3044
3018
|
}
|
|
3045
3019
|
}
|
|
3046
|
-
const drift =
|
|
3020
|
+
const drift = now - state.expectedTickTime;
|
|
3047
3021
|
const nextDelay = Math.max(0, state.tickInterval - drift);
|
|
3048
|
-
state.expectedTickTime =
|
|
3022
|
+
state.expectedTickTime = now + nextDelay;
|
|
3049
3023
|
state.timerId = setTimeout(() => this.#tick(state, device), nextDelay);
|
|
3050
3024
|
}
|
|
3051
3025
|
/** Builds a {@link PatternDescriptor} from the shorthand pattern argument and options. */
|
|
@@ -3101,12 +3075,7 @@ var PatternEngine = class {
|
|
|
3101
3075
|
const device = this.#client.getDevice(state.deviceIndex);
|
|
3102
3076
|
if (device) {
|
|
3103
3077
|
for (const track of state.tracks) {
|
|
3104
|
-
|
|
3105
|
-
device.stop({ featureIndex: track.featureIndex }).catch(noop);
|
|
3106
|
-
} else {
|
|
3107
|
-
const command = buildScalarCommand(track, track.range[0]);
|
|
3108
|
-
device.output({ featureIndex: track.featureIndex, command }).catch(noop);
|
|
3109
|
-
}
|
|
3078
|
+
device.stop({ featureIndex: track.featureIndex }).catch(noop);
|
|
3110
3079
|
}
|
|
3111
3080
|
}
|
|
3112
3081
|
if (complete) {
|
|
@@ -3119,58 +3088,22 @@ var PatternEngine = class {
|
|
|
3119
3088
|
0 && (module.exports = {
|
|
3120
3089
|
ButtplugClient,
|
|
3121
3090
|
ButtplugError,
|
|
3122
|
-
ClientMessageSchema,
|
|
3123
3091
|
ConnectionError,
|
|
3124
|
-
DEFAULT_CLIENT_NAME,
|
|
3125
|
-
DEFAULT_PING_INTERVAL,
|
|
3126
|
-
DEFAULT_REQUEST_TIMEOUT,
|
|
3127
3092
|
Device,
|
|
3128
3093
|
DeviceError,
|
|
3129
|
-
DeviceFeaturesSchema,
|
|
3130
3094
|
EASING_FUNCTIONS,
|
|
3131
3095
|
EASING_VALUES,
|
|
3132
3096
|
ErrorCode,
|
|
3133
|
-
FeatureValueSchema,
|
|
3134
3097
|
HandshakeError,
|
|
3135
|
-
HwPositionOutputDataSchema,
|
|
3136
3098
|
INPUT_TYPES,
|
|
3137
|
-
INPUT_TYPE_VALUES,
|
|
3138
|
-
InputCommandTypeSchema,
|
|
3139
|
-
InputDataSchema,
|
|
3140
|
-
InputFeatureSchema,
|
|
3141
|
-
InputReadingSchema,
|
|
3142
|
-
InputTypeSchema,
|
|
3143
|
-
KeyframeSchema,
|
|
3144
|
-
MAX_MESSAGE_ID,
|
|
3145
3099
|
OUTPUT_TYPES,
|
|
3146
|
-
OUTPUT_TYPE_VALUES,
|
|
3147
|
-
OutputCommandSchema,
|
|
3148
|
-
OutputFeatureSchema,
|
|
3149
|
-
OutputTypeSchema,
|
|
3150
3100
|
PRESETS,
|
|
3151
3101
|
PRESET_NAMES,
|
|
3152
|
-
PROTOCOL_VERSION_MAJOR,
|
|
3153
|
-
PROTOCOL_VERSION_MINOR,
|
|
3154
|
-
PatternDescriptorSchema,
|
|
3155
3102
|
PatternEngine,
|
|
3156
|
-
PingManager,
|
|
3157
|
-
PositionValueSchema,
|
|
3158
3103
|
ProtocolError,
|
|
3159
|
-
ReconnectDefaults,
|
|
3160
|
-
ReconnectHandler,
|
|
3161
|
-
RotateWithDirectionOutputDataSchema,
|
|
3162
|
-
RotationValueSchema,
|
|
3163
|
-
SensorValueSchema,
|
|
3164
|
-
ServerInfoSchema,
|
|
3165
|
-
ServerMessageSchema,
|
|
3166
|
-
SignedScalarOutputDataSchema,
|
|
3167
3104
|
TimeoutError,
|
|
3168
|
-
UnsignedScalarOutputDataSchema,
|
|
3169
|
-
WebSocketTransport,
|
|
3170
3105
|
consoleLogger,
|
|
3171
3106
|
formatError,
|
|
3172
|
-
getLogger,
|
|
3173
3107
|
getPresetInfo,
|
|
3174
|
-
noopLogger
|
|
3175
|
-
runWithLogger
|
|
3108
|
+
noopLogger
|
|
3176
3109
|
});
|