feat: upgrade buttplug package to protocol v4 and WASM v10
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 7m30s

Upgrade the buttplug TypeScript client from class-based v3 protocol to
interface-based v4 protocol, and the Rust/WASM server from the monolithic
buttplug 9.0.9 crate to the split buttplug_core/buttplug_server/
buttplug_server_device_config 10.0.0 crates.

TypeScript changes:
- Messages are now plain interfaces with msgId()/setMsgId() helpers
- ActuatorType → OutputType, SensorType → InputType
- ScalarCmd/RotateCmd/LinearCmd → OutputCmd, SensorReadCmd → InputCmd
- Client.ts → ButtplugClient.ts, new DeviceCommand/DeviceFeature files
- Devices getter returns Map instead of array
- Removed class-transformer/reflect-metadata dependencies

Rust/WASM changes:
- Split imports across buttplug_core, buttplug_server, buttplug_server_device_config
- Removed ButtplugServerDowngradeWrapper (use ButtplugServer directly)
- Replaced ButtplugFuture/ButtplugFutureStateShared with tokio::sync::oneshot
- Updated Hardware::new for new 6-arg signature
- Uses git fork (valknarthing/buttplug) to fix missing wasm deps in buttplug_core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 14:46:47 +01:00
parent fed2dd65e5
commit 6ea4ed1933
31 changed files with 1763 additions and 2441 deletions

View File

@@ -1,25 +1,17 @@
use async_trait::async_trait;
use buttplug::{
core::{
errors::ButtplugDeviceError,
message::Endpoint,
},
server::device::{
configuration::{BluetoothLESpecifier, ProtocolCommunicationSpecifier},
hardware::{
Hardware,
HardwareConnector,
HardwareEvent,
HardwareInternal,
HardwareReadCmd,
HardwareReading,
HardwareSpecializer,
HardwareSubscribeCmd,
HardwareUnsubscribeCmd,
HardwareWriteCmd,
},
},
util::future::{ButtplugFuture, ButtplugFutureStateShared},
use buttplug_core::errors::ButtplugDeviceError;
use buttplug_server_device_config::{BluetoothLESpecifier, Endpoint, ProtocolCommunicationSpecifier};
use buttplug_server::device::hardware::{
Hardware,
HardwareConnector,
HardwareEvent,
HardwareInternal,
HardwareReadCmd,
HardwareReading,
HardwareSpecializer,
HardwareSubscribeCmd,
HardwareUnsubscribeCmd,
HardwareWriteCmd,
};
use futures::future::{self, BoxFuture};
use js_sys::{DataView, Uint8Array};
@@ -28,7 +20,7 @@ use std::{
convert::TryFrom,
fmt::{self, Debug},
};
use tokio::sync::{broadcast, mpsc};
use tokio::sync::{broadcast, mpsc, oneshot};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::{spawn_local, JsFuture};
@@ -41,9 +33,6 @@ use web_sys::{
MessageEvent,
};
type WebBluetoothResultFuture = ButtplugFuture<Result<(), ButtplugDeviceError>>;
type WebBluetoothReadResultFuture = ButtplugFuture<Result<HardwareReading, ButtplugDeviceError>>;
struct BluetoothDeviceWrapper {
pub device: BluetoothDevice
}
@@ -179,7 +168,7 @@ impl HardwareSpecializer for WebBluetoothHardwareSpecializer {
receiver,
command_sender,
));
Ok(Hardware::new(&name, &address, &[], device_impl))
Ok(Hardware::new(&name, &address, &[], &None, false, device_impl))
}
WebBluetoothEvent::Disconnected => Err(
ButtplugDeviceError::DeviceCommunicationError(
@@ -202,19 +191,19 @@ pub enum WebBluetoothEvent {
pub enum WebBluetoothDeviceCommand {
Write(
HardwareWriteCmd,
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
oneshot::Sender<Result<(), ButtplugDeviceError>>,
),
Read(
HardwareReadCmd,
ButtplugFutureStateShared<Result<HardwareReading, ButtplugDeviceError>>,
oneshot::Sender<Result<HardwareReading, ButtplugDeviceError>>,
),
Subscribe(
HardwareSubscribeCmd,
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
oneshot::Sender<Result<(), ButtplugDeviceError>>,
),
Unsubscribe(
HardwareUnsubscribeCmd,
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
oneshot::Sender<Result<(), ButtplugDeviceError>>,
),
}
@@ -287,7 +276,7 @@ async fn run_webbluetooth_loop(
.await;
while let Some(msg) = device_command_receiver.recv().await {
match msg {
WebBluetoothDeviceCommand::Write(write_cmd, waker) => {
WebBluetoothDeviceCommand::Write(write_cmd, sender) => {
debug!("Writing to endpoint {:?}", write_cmd.endpoint());
let chr = char_map.get(&write_cmd.endpoint()).unwrap().clone();
spawn_local(async move {
@@ -295,10 +284,10 @@ async fn run_webbluetooth_loop(
JsFuture::from(chr.write_value_with_u8_array(&uint8buf).unwrap())
.await
.unwrap();
waker.set_reply(Ok(()));
let _ = sender.send(Ok(()));
});
}
WebBluetoothDeviceCommand::Read(read_cmd, waker) => {
WebBluetoothDeviceCommand::Read(read_cmd, sender) => {
debug!("Writing to endpoint {:?}", read_cmd.endpoint());
let chr = char_map.get(&read_cmd.endpoint()).unwrap().clone();
spawn_local(async move {
@@ -307,10 +296,10 @@ async fn run_webbluetooth_loop(
let mut body = vec![0; data_view.byte_length() as usize];
Uint8Array::new(&data_view).copy_to(&mut body[..]);
let reading = HardwareReading::new(read_cmd.endpoint(), &body);
waker.set_reply(Ok(reading));
let _ = sender.send(Ok(reading));
});
}
WebBluetoothDeviceCommand::Subscribe(subscribe_cmd, waker) => {
WebBluetoothDeviceCommand::Subscribe(subscribe_cmd, sender) => {
debug!("Subscribing to endpoint {:?}", subscribe_cmd.endpoint());
let chr = char_map.get(&subscribe_cmd.endpoint()).unwrap().clone();
let ep = subscribe_cmd.endpoint();
@@ -335,10 +324,10 @@ async fn run_webbluetooth_loop(
spawn_local(async move {
JsFuture::from(chr.start_notifications()).await.unwrap();
debug!("Endpoint subscribed");
waker.set_reply(Ok(()));
let _ = sender.send(Ok(()));
});
}
WebBluetoothDeviceCommand::Unsubscribe(_unsubscribe_cmd, _waker) => {}
WebBluetoothDeviceCommand::Unsubscribe(_unsubscribe_cmd, _sender) => {}
}
}
debug!("run_webbluetooth_loop exited!");
@@ -388,12 +377,13 @@ impl HardwareInternal for WebBluetoothHardware {
let sender = self.device_command_sender.clone();
let msg = msg.clone();
Box::pin(async move {
let fut = WebBluetoothReadResultFuture::default();
let waker = fut.get_state_clone();
sender
.send(WebBluetoothDeviceCommand::Read(msg, waker))
let (tx, rx) = oneshot::channel();
let _ = sender
.send(WebBluetoothDeviceCommand::Read(msg, tx))
.await;
fut.await
rx.await.unwrap_or(Err(ButtplugDeviceError::DeviceCommunicationError(
"Device command channel closed".to_string(),
)))
})
}
@@ -401,12 +391,13 @@ impl HardwareInternal for WebBluetoothHardware {
let sender = self.device_command_sender.clone();
let msg = msg.clone();
Box::pin(async move {
let fut = WebBluetoothResultFuture::default();
let waker = fut.get_state_clone();
sender
.send(WebBluetoothDeviceCommand::Write(msg.clone(), waker))
let (tx, rx) = oneshot::channel();
let _ = sender
.send(WebBluetoothDeviceCommand::Write(msg, tx))
.await;
fut.await
rx.await.unwrap_or(Err(ButtplugDeviceError::DeviceCommunicationError(
"Device command channel closed".to_string(),
)))
})
}
@@ -414,12 +405,13 @@ impl HardwareInternal for WebBluetoothHardware {
let sender = self.device_command_sender.clone();
let msg = msg.clone();
Box::pin(async move {
let fut = WebBluetoothResultFuture::default();
let waker = fut.get_state_clone();
sender
.send(WebBluetoothDeviceCommand::Subscribe(msg.clone(), waker))
let (tx, rx) = oneshot::channel();
let _ = sender
.send(WebBluetoothDeviceCommand::Subscribe(msg, tx))
.await;
fut.await
rx.await.unwrap_or(Err(ButtplugDeviceError::DeviceCommunicationError(
"Device command channel closed".to_string(),
)))
})
}

View File

@@ -1,14 +1,10 @@
use super::webbluetooth_hardware::WebBluetoothHardwareConnector;
use buttplug::{
core::ButtplugResultFuture,
server::device::{
configuration::{ProtocolCommunicationSpecifier},
hardware::communication::{
HardwareCommunicationManager, HardwareCommunicationManagerBuilder,
HardwareCommunicationManagerEvent,
},
}
use buttplug_core::ButtplugResultFuture;
use buttplug_server_device_config::ProtocolCommunicationSpecifier;
use buttplug_server::device::hardware::communication::{
HardwareCommunicationManager, HardwareCommunicationManagerBuilder,
HardwareCommunicationManagerEvent,
};
use futures::future;
use js_sys::Array;
@@ -69,8 +65,8 @@ impl HardwareCommunicationManager for WebBluetoothCommunicationManager {
let options = web_sys::RequestDeviceOptions::new();
let filters = Array::new();
let optional_services = Array::new();
for vals in config_manager.protocol_device_configurations().iter() {
for config in vals.1 {
for vals in config_manager.base_communication_specifiers().iter() {
for config in vals.1.iter() {
if let ProtocolCommunicationSpecifier::BluetoothLE(btle) = &config {
for name in btle.names() {
let filter = web_sys::BluetoothLeScanFilterInit::new();