Files
sexy.pivoine.art/packages/buttplug/src/lib.rs
Sebastian Krüger 6ea4ed1933
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 7m30s
feat: upgrade buttplug package to protocol v4 and WASM v10
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>
2026-02-06 14:46:47 +01:00

137 lines
4.2 KiB
Rust

#[macro_use]
extern crate tracing;
#[macro_use]
extern crate futures;
mod webbluetooth;
use js_sys;
use tokio_stream::StreamExt;
use crate::webbluetooth::{WebBluetoothCommunicationManagerBuilder};
use buttplug_core::{
message::{ButtplugServerMessageCurrent, BUTTPLUG_CURRENT_API_MAJOR_VERSION, serializer::{ButtplugSerializedMessage, ButtplugMessageSerializer}},
util::async_manager,
};
use buttplug_server::{
ButtplugServerBuilder, ButtplugServer,
device::ServerDeviceManagerBuilder,
message::{ButtplugServerMessageVariant, serializer::ButtplugServerJSONSerializer},
};
use buttplug_server_device_config::{DeviceConfigurationManager, load_protocol_configs};
type FFICallback = js_sys::Function;
type FFICallbackContext = u32;
#[derive(Clone, Copy)]
pub struct FFICallbackContextWrapper(FFICallbackContext);
unsafe impl Send for FFICallbackContextWrapper {
}
unsafe impl Sync for FFICallbackContextWrapper {
}
use console_error_panic_hook;
use tracing_subscriber::{layer::SubscriberExt, Registry};
use tracing_wasm::{WASMLayer, WASMLayerConfig};
use wasm_bindgen::prelude::*;
use std::sync::Arc;
use js_sys::Uint8Array;
pub type ButtplugWASMServer = Arc<ButtplugServer>;
pub fn send_server_message(
message: &ButtplugServerMessageCurrent,
callback: &FFICallback,
) {
let serializer = ButtplugServerJSONSerializer::default();
serializer.force_message_version(&BUTTPLUG_CURRENT_API_MAJOR_VERSION);
let json_msg = serializer.serialize(&[ButtplugServerMessageVariant::V4(message.clone())]);
if let ButtplugSerializedMessage::Text(json) = json_msg {
let buf = json.as_bytes();
let this = JsValue::null();
let uint8buf = unsafe { Uint8Array::new(&Uint8Array::view(buf)) };
callback.call1(&this, &JsValue::from(uint8buf));
}
}
#[no_mangle]
pub fn create_test_dcm(_allow_raw_messages: bool) -> DeviceConfigurationManager {
load_protocol_configs(&None, &None, false)
.expect("If this fails, the whole library goes with it.")
.finish()
.expect("If this fails, the whole library goes with it.")
}
#[no_mangle]
#[wasm_bindgen]
pub fn buttplug_create_embedded_wasm_server(
callback: &FFICallback,
) -> *mut ButtplugWASMServer {
console_error_panic_hook::set_once();
let dcm = create_test_dcm(false);
let mut sdm = ServerDeviceManagerBuilder::new(dcm);
sdm.comm_manager(WebBluetoothCommunicationManagerBuilder::default());
let builder = ButtplugServerBuilder::new(sdm.finish().unwrap());
let server = Arc::new(builder.finish().unwrap());
let event_stream = server.server_version_event_stream();
let callback = callback.clone();
async_manager::spawn(async move {
pin_mut!(event_stream);
while let Some(message) = event_stream.next().await {
send_server_message(&message, &callback);
}
});
Box::into_raw(Box::new(server))
}
#[no_mangle]
#[wasm_bindgen]
pub fn buttplug_free_embedded_wasm_server(ptr: *mut ButtplugWASMServer) {
if !ptr.is_null() {
unsafe {
let _ = Box::from_raw(ptr);
}
}
}
#[no_mangle]
#[wasm_bindgen]
pub fn buttplug_client_send_json_message(
server_ptr: *mut ButtplugWASMServer,
buf: &[u8],
callback: &FFICallback,
) {
let server = unsafe {
assert!(!server_ptr.is_null());
&mut *server_ptr
};
let callback = callback.clone();
let serializer = ButtplugServerJSONSerializer::default();
serializer.force_message_version(&BUTTPLUG_CURRENT_API_MAJOR_VERSION);
let input_msg = serializer.deserialize(&ButtplugSerializedMessage::Text(std::str::from_utf8(buf).unwrap().to_owned())).unwrap();
async_manager::spawn(async move {
let msg = input_msg[0].clone();
let response = server.parse_message(msg).await.unwrap();
let json_msg = serializer.serialize(&[response]);
if let ButtplugSerializedMessage::Text(json) = json_msg {
let buf = json.as_bytes();
let this = JsValue::null();
let uint8buf = unsafe { Uint8Array::new(&Uint8Array::view(buf)) };
callback.call1(&this, &JsValue::from(uint8buf));
}
});
}
#[no_mangle]
#[wasm_bindgen]
pub fn buttplug_activate_env_logger(_max_level: &str) {
tracing::subscriber::set_global_default(
Registry::default()
//.with(EnvFilter::new(max_level))
.with(WASMLayer::new(WASMLayerConfig::default())),
)
.expect("default global");
}