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

@@ -7,485 +7,203 @@
*/
// tslint:disable:max-classes-per-file
"use strict";
'use strict';
import { instanceToPlain, Type } from "class-transformer";
import "reflect-metadata";
import { ButtplugMessageError } from './Exceptions';
export const SYSTEM_MESSAGE_ID = 0;
export const DEFAULT_MESSAGE_ID = 1;
export const MAX_ID = 4294967295;
export const MESSAGE_SPEC_VERSION = 3;
export const MESSAGE_SPEC_VERSION_MAJOR = 4;
export const MESSAGE_SPEC_VERSION_MINOR = 0;
export class MessageAttributes {
public ScalarCmd?: Array<GenericDeviceMessageAttributes>;
public RotateCmd?: Array<GenericDeviceMessageAttributes>;
public LinearCmd?: Array<GenericDeviceMessageAttributes>;
public RawReadCmd?: RawDeviceMessageAttributes;
public RawWriteCmd?: RawDeviceMessageAttributes;
public RawSubscribeCmd?: RawDeviceMessageAttributes;
public SensorReadCmd?: Array<SensorDeviceMessageAttributes>;
public SensorSubscribeCmd?: Array<SensorDeviceMessageAttributes>;
public StopDeviceCmd: {};
constructor(data: Partial<MessageAttributes>) {
Object.assign(this, data);
}
public update() {
this.ScalarCmd?.forEach((x, i) => (x.Index = i));
this.RotateCmd?.forEach((x, i) => (x.Index = i));
this.LinearCmd?.forEach((x, i) => (x.Index = i));
this.SensorReadCmd?.forEach((x, i) => (x.Index = i));
this.SensorSubscribeCmd?.forEach((x, i) => (x.Index = i));
}
// Base message interfaces
export interface ButtplugMessage {
Ok?: Ok;
Ping?: Ping;
Error?: Error;
RequestServerInfo?: RequestServerInfo;
ServerInfo?: ServerInfo;
RequestDeviceList?: RequestDeviceList;
StartScanning?: StartScanning;
StopScanning?: StopScanning;
ScanningFinished?: ScanningFinished;
StopCmd?: StopCmd;
InputCmd?: InputCmd;
InputReading?: InputReading;
OutputCmd?: OutputCmd;
DeviceList?: DeviceList;
}
export enum ActuatorType {
Unknown = "Unknown",
Vibrate = "Vibrate",
Rotate = "Rotate",
Oscillate = "Oscillate",
Constrict = "Constrict",
Inflate = "Inflate",
Position = "Position",
export function msgId(msg: ButtplugMessage): number {
for (let [_, entry] of Object.entries(msg)) {
if (entry != undefined) {
return entry.Id;
}
}
throw new ButtplugMessageError(`Message ${msg} does not have an ID.`);
}
export enum SensorType {
Unknown = "Unknown",
Battery = "Battery",
RSSI = "RSSI",
Button = "Button",
Pressure = "Pressure",
// Temperature,
// Accelerometer,
// Gyro,
export function setMsgId(msg: ButtplugMessage, id: number) {
for (let [_, entry] of Object.entries(msg)) {
if (entry != undefined) {
entry.Id = id;
return;
}
}
throw new ButtplugMessageError(`Message ${msg} does not have an ID.`);
}
export class GenericDeviceMessageAttributes {
public FeatureDescriptor: string;
public ActuatorType: ActuatorType;
public StepCount: number;
public Index = 0;
constructor(data: Partial<GenericDeviceMessageAttributes>) {
Object.assign(this, data);
}
export interface Ok {
Id: number | undefined;
}
export class RawDeviceMessageAttributes {
constructor(public Endpoints: Array<string>) {}
}
export class SensorDeviceMessageAttributes {
public FeatureDescriptor: string;
public SensorType: SensorType;
public StepRange: Array<number>;
public Index = 0;
constructor(data: Partial<GenericDeviceMessageAttributes>) {
Object.assign(this, data);
}
}
export abstract class ButtplugMessage {
constructor(public Id: number) {}
// tslint:disable-next-line:ban-types
public get Type(): Function {
return this.constructor;
}
public toJSON(): string {
return JSON.stringify(this.toProtocolFormat());
}
public toProtocolFormat(): object {
const jsonObj = {};
jsonObj[(this.constructor as unknown as { Name: string }).Name] =
instanceToPlain(this);
return jsonObj;
}
public update() {}
}
export abstract class ButtplugDeviceMessage extends ButtplugMessage {
constructor(
public DeviceIndex: number,
public Id: number,
) {
super(Id);
}
}
export abstract class ButtplugSystemMessage extends ButtplugMessage {
constructor(public Id: number = SYSTEM_MESSAGE_ID) {
super(Id);
}
}
export class Ok extends ButtplugSystemMessage {
static Name = "Ok";
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
super(Id);
}
}
export class Ping extends ButtplugMessage {
static Name = "Ping";
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
super(Id);
}
export interface Ping {
Id: number | undefined;
}
export enum ErrorClass {
ERROR_UNKNOWN,
ERROR_INIT,
ERROR_PING,
ERROR_MSG,
ERROR_DEVICE,
ERROR_UNKNOWN,
ERROR_INIT,
ERROR_PING,
ERROR_MSG,
ERROR_DEVICE,
}
export class Error extends ButtplugMessage {
static Name = "Error";
constructor(
public ErrorMessage: string,
public ErrorCode: ErrorClass = ErrorClass.ERROR_UNKNOWN,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(Id);
}
get Schemversion() {
return 0;
}
export interface Error {
ErrorMessage: string;
ErrorCode: ErrorClass;
Id: number | undefined;
}
export class DeviceInfo {
public DeviceIndex: number;
public DeviceName: string;
@Type(() => MessageAttributes)
public DeviceMessages: MessageAttributes;
public DeviceDisplayName?: string;
public DeviceMessageTimingGap?: number;
constructor(data: Partial<DeviceInfo>) {
Object.assign(this, data);
}
export interface RequestDeviceList {
Id: number | undefined;
}
export class DeviceList extends ButtplugMessage {
static Name = "DeviceList";
@Type(() => DeviceInfo)
public Devices: DeviceInfo[];
public Id: number;
constructor(devices: DeviceInfo[], id: number = DEFAULT_MESSAGE_ID) {
super(id);
this.Devices = devices;
this.Id = id;
}
public update() {
for (const device of this.Devices) {
device.DeviceMessages.update();
}
}
export interface StartScanning {
Id: number | undefined;
}
export class DeviceAdded extends ButtplugSystemMessage {
static Name = "DeviceAdded";
public DeviceIndex: number;
public DeviceName: string;
@Type(() => MessageAttributes)
public DeviceMessages: MessageAttributes;
public DeviceDisplayName?: string;
public DeviceMessageTimingGap?: number;
constructor(data: Partial<DeviceAdded>) {
super();
Object.assign(this, data);
}
public update() {
this.DeviceMessages.update();
}
export interface StopScanning {
Id: number | undefined;
}
export class DeviceRemoved extends ButtplugSystemMessage {
static Name = "DeviceRemoved";
constructor(public DeviceIndex: number) {
super();
}
export interface StopAllDevices {
Id: number | undefined;
}
export class RequestDeviceList extends ButtplugMessage {
static Name = "RequestDeviceList";
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
super(Id);
}
export interface ScanningFinished {
Id: number | undefined;
}
export class StartScanning extends ButtplugMessage {
static Name = "StartScanning";
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
super(Id);
}
export interface RequestServerInfo {
ClientName: string;
ProtocolVersionMajor: number;
ProtocolVersionMinor: number;
Id: number | undefined;
}
export class StopScanning extends ButtplugMessage {
static Name = "StopScanning";
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
super(Id);
}
export interface ServerInfo {
MaxPingTime: number;
ServerName: string;
ProtocolVersionMajor: number;
ProtocolVersionMinor: number;
Id: number | undefined;
}
export class ScanningFinished extends ButtplugSystemMessage {
static Name = "ScanningFinished";
constructor() {
super();
}
export interface DeviceFeature {
FeatureDescriptor: string;
Output: { [key: string]: DeviceFeatureOutput };
Input: { [key: string]: DeviceFeatureInput };
FeatureIndex: number;
}
export class RequestServerInfo extends ButtplugMessage {
static Name = "RequestServerInfo";
constructor(
public ClientName: string,
public MessageVersion: number = 0,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(Id);
}
export interface DeviceInfo {
DeviceIndex: number;
DeviceName: string;
DeviceFeatures: { [key: number]: DeviceFeature };
DeviceDisplayName?: string;
DeviceMessageTimingGap?: number;
}
export class ServerInfo extends ButtplugSystemMessage {
static Name = "ServerInfo";
constructor(
public MessageVersion: number,
public MaxPingTime: number,
public ServerName: string,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super();
}
export interface DeviceList {
Devices: { [key: number]: DeviceInfo };
Id: number | undefined;
}
export class StopDeviceCmd extends ButtplugDeviceMessage {
static Name = "StopDeviceCmd";
constructor(
public DeviceIndex: number = -1,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
export enum OutputType {
Unknown = 'Unknown',
Vibrate = 'Vibrate',
Rotate = 'Rotate',
Oscillate = 'Oscillate',
Constrict = 'Constrict',
Inflate = 'Inflate',
Position = 'Position',
HwPositionWithDuration = 'HwPositionWithDuration',
Temperature = 'Temperature',
Spray = 'Spray',
Led = 'Led',
}
export class StopAllDevices extends ButtplugMessage {
static Name = "StopAllDevices";
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
super(Id);
}
export enum InputType {
Unknown = 'Unknown',
Battery = 'Battery',
RSSI = 'RSSI',
Button = 'Button',
Pressure = 'Pressure',
// Temperature,
// Accelerometer,
// Gyro,
}
export class GenericMessageSubcommand {
protected constructor(public Index: number) {}
export enum InputCommandType {
Read = 'Read',
Subscribe = 'Subscribe',
Unsubscribe = 'Unsubscribe',
}
export class ScalarSubcommand extends GenericMessageSubcommand {
constructor(
Index: number,
public Scalar: number,
public ActuatorType: ActuatorType,
) {
super(Index);
}
export interface DeviceFeatureInput {
Value: number[];
Command: InputCommandType[];
}
export class ScalarCmd extends ButtplugDeviceMessage {
static Name = "ScalarCmd";
constructor(
public Scalars: ScalarSubcommand[],
public DeviceIndex: number = -1,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
export interface DeviceFeatureOutput {
Value: number;
Duration?: number;
}
export class RotateSubcommand extends GenericMessageSubcommand {
constructor(
Index: number,
public Speed: number,
public Clockwise: boolean,
) {
super(Index);
}
export interface OutputCmd {
DeviceIndex: number;
FeatureIndex: number;
Command: { [key: string]: DeviceFeatureOutput };
Id: number | undefined;
}
export class RotateCmd extends ButtplugDeviceMessage {
static Name = "RotateCmd";
// Device Input Commands
public static Create(
deviceIndex: number,
commands: [number, boolean][],
): RotateCmd {
const cmdList: RotateSubcommand[] = new Array<RotateSubcommand>();
let i = 0;
for (const [speed, clockwise] of commands) {
cmdList.push(new RotateSubcommand(i, speed, clockwise));
++i;
}
return new RotateCmd(cmdList, deviceIndex);
}
constructor(
public Rotations: RotateSubcommand[],
public DeviceIndex: number = -1,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
export interface InputCmd {
DeviceIndex: number;
FeatureIndex: number;
Type: InputType;
Command: InputCommandType;
Id: number | undefined;
}
export class VectorSubcommand extends GenericMessageSubcommand {
constructor(
Index: number,
public Position: number,
public Duration: number,
) {
super(Index);
}
export interface InputValue {
Value: number;
}
export class LinearCmd extends ButtplugDeviceMessage {
static Name = "LinearCmd";
public static Create(
deviceIndex: number,
commands: [number, number][],
): LinearCmd {
const cmdList: VectorSubcommand[] = new Array<VectorSubcommand>();
let i = 0;
for (const cmd of commands) {
cmdList.push(new VectorSubcommand(i, cmd[0], cmd[1]));
++i;
}
return new LinearCmd(cmdList, deviceIndex);
}
constructor(
public Vectors: VectorSubcommand[],
public DeviceIndex: number = -1,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
export interface InputReading {
DeviceIndex: number;
FeatureIndex: number;
Reading: { [key: string]: InputValue };
Id: number | undefined;
}
export class SensorReadCmd extends ButtplugDeviceMessage {
static Name = "SensorReadCmd";
constructor(
public DeviceIndex: number,
public SensorIndex: number,
public SensorType: SensorType,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
}
export class SensorReading extends ButtplugDeviceMessage {
static Name = "SensorReading";
constructor(
public DeviceIndex: number,
public SensorIndex: number,
public SensorType: SensorType,
public Data: number[],
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
}
export class RawReadCmd extends ButtplugDeviceMessage {
static Name = "RawReadCmd";
constructor(
public DeviceIndex: number,
public Endpoint: string,
public ExpectedLength: number,
public Timeout: number,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
}
export class RawWriteCmd extends ButtplugDeviceMessage {
static Name = "RawWriteCmd";
constructor(
public DeviceIndex: number,
public Endpoint: string,
public Data: Uint8Array,
public WriteWithResponse: boolean,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
}
export class RawSubscribeCmd extends ButtplugDeviceMessage {
static Name = "RawSubscribeCmd";
constructor(
public DeviceIndex: number,
public Endpoint: string,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
}
export class RawUnsubscribeCmd extends ButtplugDeviceMessage {
static Name = "RawUnsubscribeCmd";
constructor(
public DeviceIndex: number,
public Endpoint: string,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
}
export class RawReading extends ButtplugDeviceMessage {
static Name = "RawReading";
constructor(
public DeviceIndex: number,
public Endpoint: string,
public Data: number[],
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
export interface StopCmd {
Id: number | undefined;
DeviceIndex: number | undefined;
FeatureIndex: number | undefined;
Inputs: boolean | undefined;
Outputs: boolean | undefined;
}