Skip to content

Commit 60da90b

Browse files
committed
feat: more fixes, robustness
1 parent c577941 commit 60da90b

File tree

6 files changed

+144
-126
lines changed

6 files changed

+144
-126
lines changed

packages/plugin-buttplug/src/enviroment.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export const buttplugEnvSchema = z
55
.object({
66
INTIFACE_URL: z.string().default("ws://localhost:12345"),
77
INTIFACE_NAME: z.string().default("Eliza Buttplug Client"),
8-
DEVICE_NAME: z.string().default("Eliza Buttplug Device"),
8+
DEVICE_NAME: z.string().default("Lovense Nora"),
99
})
1010
.refine(
1111
(data) => {

packages/plugin-buttplug/src/index.ts

+98-113
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import type {
99
State,
1010
} from "@ai16z/eliza";
1111
import { Service, ServiceType } from "@ai16z/eliza";
12-
import { isPortAvailable, startIntifaceEngine } from "./utils";
12+
import {
13+
isPortAvailable,
14+
startIntifaceEngine,
15+
shutdownIntifaceEngine,
16+
} from "./utils";
1317

1418
export interface IButtplugService extends Service {
1519
vibrate(strength: number, duration: number): Promise<void>;
@@ -44,6 +48,22 @@ export class ButtplugService extends Service implements IButtplugService {
4448
"deviceremoved",
4549
this.handleDeviceRemoved.bind(this)
4650
);
51+
52+
// Add cleanup handlers
53+
process.on("SIGINT", this.cleanup.bind(this));
54+
process.on("SIGTERM", this.cleanup.bind(this));
55+
process.on("exit", this.cleanup.bind(this));
56+
}
57+
58+
private async cleanup() {
59+
try {
60+
if (this.connected) {
61+
await this.client.disconnect();
62+
}
63+
await shutdownIntifaceEngine();
64+
} catch (error) {
65+
console.error("[ButtplugService] Cleanup error:", error);
66+
}
4767
}
4868

4969
getInstance(): IButtplugService {
@@ -68,7 +88,6 @@ export class ButtplugService extends Service implements IButtplugService {
6888
if (portAvailable) {
6989
try {
7090
await startIntifaceEngine();
71-
await new Promise((resolve) => setTimeout(resolve, 1000));
7291
} catch (error) {
7392
console.error("Failed to start Intiface Engine:", error);
7493
throw error;
@@ -79,32 +98,78 @@ export class ButtplugService extends Service implements IButtplugService {
7998
);
8099
}
81100

82-
const connector = new ButtplugNodeWebsocketClientConnector(
83-
this.config.INTIFACE_URL
84-
);
101+
let retries = 5;
102+
while (retries > 0) {
103+
try {
104+
const connector = new ButtplugNodeWebsocketClientConnector(
105+
this.config.INTIFACE_URL
106+
);
85107

86-
try {
87-
await this.client.connect(connector);
88-
this.connected = true;
89-
await this.client.startScanning();
90-
91-
// Wait for device discovery
92-
await new Promise((r) => setTimeout(r, 5000));
93-
console.log("Scanning for devices...");
94-
95-
// Store discovered devices in the map
96-
this.client.devices.forEach((device) => {
97-
this.devices.set(device.name, device);
98-
console.log(`- ${device.name} (${device.index})`);
99-
});
108+
await this.client.connect(connector);
109+
this.connected = true;
110+
await this.scanAndGrabDevices();
111+
return;
112+
} catch (error) {
113+
retries--;
114+
if (retries > 0) {
115+
console.log(
116+
`Connection attempt failed, retrying... (${retries} attempts left)`
117+
);
118+
await new Promise((r) => setTimeout(r, 2000));
119+
} else {
120+
console.error(
121+
"Failed to connect to Buttplug server after all retries:",
122+
error
123+
);
124+
throw error;
125+
}
126+
}
127+
}
128+
}
129+
130+
private async scanAndGrabDevices() {
131+
await this.client.startScanning();
132+
console.log("Scanning for devices...");
133+
await new Promise((r) => setTimeout(r, 2000));
134+
135+
this.client.devices.forEach((device) => {
136+
this.devices.set(device.name, device);
137+
console.log(`- ${device.name} (${device.index})`);
138+
});
139+
140+
if (this.devices.size === 0) {
141+
console.log("No devices found");
142+
}
143+
}
144+
145+
private async ensureDeviceAvailable() {
146+
if (!this.connected) {
147+
throw new Error("Not connected to Buttplug server");
148+
}
149+
150+
if (this.devices.size === 0) {
151+
await this.scanAndGrabDevices();
152+
}
100153

101-
if (this.devices.size === 0) {
102-
console.log("No devices found");
154+
const devices = this.getDevices();
155+
if (devices.length === 0) {
156+
throw new Error("No devices available");
157+
}
158+
159+
let targetDevice;
160+
if (this.preferredDeviceName) {
161+
targetDevice = this.devices.get(this.preferredDeviceName);
162+
if (!targetDevice) {
163+
console.warn(
164+
`Preferred device ${this.preferredDeviceName} not found, using first available device`
165+
);
166+
targetDevice = devices[0];
103167
}
104-
} catch (error) {
105-
console.error("Failed to connect to Buttplug server:", error);
106-
throw error;
168+
} else {
169+
targetDevice = devices[0];
107170
}
171+
172+
return targetDevice;
108173
}
109174

110175
async disconnect() {
@@ -150,27 +215,7 @@ export class ButtplugService extends Service implements IButtplugService {
150215
}
151216

152217
private async handleVibrate(event: VibrateEvent) {
153-
if (!this.connected) {
154-
throw new Error("Not connected to Buttplug server");
155-
}
156-
157-
const devices = this.getDevices();
158-
if (devices.length === 0) {
159-
throw new Error("No devices available");
160-
}
161-
162-
let targetDevice;
163-
if (this.preferredDeviceName) {
164-
targetDevice = this.devices.get(this.preferredDeviceName);
165-
if (!targetDevice) {
166-
console.warn(
167-
`Preferred device ${this.preferredDeviceName} not found, using first available device`
168-
);
169-
targetDevice = devices[0];
170-
}
171-
} else {
172-
targetDevice = devices[0];
173-
}
218+
const targetDevice = await this.ensureDeviceAvailable();
174219

175220
if (this.rampUpAndDown) {
176221
const steps = this.rampSteps;
@@ -206,56 +251,16 @@ export class ButtplugService extends Service implements IButtplugService {
206251
}
207252

208253
async vibrate(strength: number, duration: number): Promise<void> {
209-
if (this.preferredDeviceName) {
210-
const device = this.devices.get(this.preferredDeviceName);
211-
if (!device) {
212-
console.log(
213-
`Preferred device ${this.preferredDeviceName} not found, using first available device`
214-
);
215-
const devices = this.getDevices();
216-
if (devices.length > 0) {
217-
await this.addToVibrateQueue({
218-
strength,
219-
duration,
220-
deviceId: devices[0].id,
221-
});
222-
} else {
223-
throw new Error("No devices available");
224-
}
225-
} else {
226-
await this.addToVibrateQueue({
227-
strength,
228-
duration,
229-
deviceId: device.id,
230-
});
231-
}
232-
} else {
233-
await this.addToVibrateQueue({ strength, duration });
234-
}
254+
const targetDevice = await this.ensureDeviceAvailable();
255+
await this.addToVibrateQueue({
256+
strength,
257+
duration,
258+
deviceId: targetDevice.id,
259+
});
235260
}
236261

237262
async getBatteryLevel(): Promise<number> {
238-
if (!this.connected) {
239-
throw new Error("Not connected to Buttplug server");
240-
}
241-
242-
const devices = this.getDevices();
243-
if (devices.length === 0) {
244-
throw new Error("No devices available");
245-
}
246-
247-
let targetDevice;
248-
if (this.preferredDeviceName) {
249-
targetDevice = this.devices.get(this.preferredDeviceName);
250-
if (!targetDevice) {
251-
console.warn(
252-
`Preferred device ${this.preferredDeviceName} not found, using first available device`
253-
);
254-
targetDevice = devices[0];
255-
}
256-
} else {
257-
targetDevice = devices[0];
258-
}
263+
const targetDevice = await this.ensureDeviceAvailable();
259264

260265
try {
261266
const battery = await targetDevice.battery();
@@ -270,27 +275,7 @@ export class ButtplugService extends Service implements IButtplugService {
270275
}
271276

272277
async rotate(strength: number, duration: number): Promise<void> {
273-
if (!this.connected) {
274-
throw new Error("Not connected to Buttplug server");
275-
}
276-
277-
const devices = this.getDevices();
278-
if (devices.length === 0) {
279-
throw new Error("No devices available");
280-
}
281-
282-
let targetDevice;
283-
if (this.preferredDeviceName) {
284-
targetDevice = this.devices.get(this.preferredDeviceName);
285-
if (!targetDevice) {
286-
console.warn(
287-
`Preferred device ${this.preferredDeviceName} not found, using first available device`
288-
);
289-
targetDevice = devices[0];
290-
}
291-
} else {
292-
targetDevice = devices[0];
293-
}
278+
const targetDevice = await this.ensureDeviceAvailable();
294279

295280
// Check if device supports rotation
296281
if (!targetDevice.rotateCmd) {

packages/plugin-buttplug/src/utils.ts

+24-11
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,22 @@ export async function isPortAvailable(port: number): Promise<boolean> {
2222
}
2323

2424
export async function startIntifaceEngine(): Promise<void> {
25+
const configPath = path.join(
26+
__dirname,
27+
"../src/buttplug-user-device-config.json"
28+
);
2529
try {
26-
intifaceProcess = spawn(
30+
const child = spawn(
2731
path.join(__dirname, "../intiface-engine/intiface-engine"),
2832
[
2933
"--websocket-port",
3034
"12345",
3135
"--use-bluetooth-le",
3236
"--server-name",
3337
"Eliza Buttplugin Server",
34-
"--log",
35-
"debug",
3638
"--use-device-websocket-server",
37-
"--device-websocket-server-port",
38-
"54817",
39+
"--user-device-config-file",
40+
configPath,
3941
],
4042
{
4143
detached: false,
@@ -44,12 +46,8 @@ export async function startIntifaceEngine(): Promise<void> {
4446
}
4547
);
4648

47-
// Set up cleanup handler
48-
process.on("SIGINT", cleanup);
49-
process.on("SIGTERM", cleanup);
50-
process.on("exit", cleanup);
51-
52-
// Wait briefly to ensure the process starts
49+
child.unref();
50+
intifaceProcess = child;
5351
await new Promise((resolve) => setTimeout(resolve, 5000));
5452
console.log("[utils] Intiface Engine started");
5553
} catch (error) {
@@ -102,3 +100,18 @@ async function cleanup() {
102100

103101
// Export cleanup for manual shutdown if needed
104102
export { cleanup as shutdownIntifaceEngine };
103+
104+
// Start Intiface Engine if run directly
105+
if (import.meta.url === new URL(import.meta.url).href) {
106+
console.log("[utils] Starting Intiface Engine service");
107+
startIntifaceEngine().catch((error) => {
108+
console.error("[utils] Failed to start Intiface Engine:", error);
109+
process.exit(1);
110+
});
111+
112+
process.on("SIGINT", async () => {
113+
console.log("[utils] Shutting down Intiface Engine");
114+
await cleanup();
115+
process.exit(0);
116+
});
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"version": {
3+
"major": 2,
4+
"minor": 6
5+
},
6+
"user-configs": {
7+
"specifiers": {
8+
"lovense": {
9+
"websocket": {
10+
"names": ["LVSDevice"]
11+
}
12+
},
13+
"tcode-v03": {
14+
"websocket": {
15+
"names": ["TCodeDevice"]
16+
}
17+
}
18+
}
19+
}
20+
}

packages/plugin-buttplug/test/simulate.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export async function startIntifaceEngine(): Promise<void> {
123123
"--device-websocket-server-port",
124124
WEBSOCKET_PORT.toString(),
125125
"--user-device-config-file",
126-
path.join(__dirname, "buttplug-user-device-config.json"),
126+
path.join(__dirname, "buttplug-user-device-config-test.json"),
127127
],
128128
{
129129
detached: true,

0 commit comments

Comments
 (0)