Device Control
We've connected, we've enumerated, now it's time for the important stuff. Device Control!
Device Capabilities
The devices Buttplug supports can do many different things. They may vibrate, stroke, rotate, some combination of all of these, or possibly something completely different.
In order to trigger these different mechanisms, OutputCmd Messages are used. For now we'll just look at vibrating and stopping, but there's descriptions of other messages in the Winning Ways section.
When a device is added, it comes with a list of features that OutputCmd can refer to, as well as certain parameters for those features. For instance, if you have a vibrating buttplug (an actual buttplug toy) can be accessed using the following messages:
- OutputCmd
- This will take a feature index, feature type (for this, Vibrate), and a value in steps/percent, which the client library is expected to turn into a message for you.
- StopCmd
- This command takes no arguments, and simply stops a feature/device from whatever its doing. The Buttplug Server has enough information to know what actions a device or feature can perform, so it handles making sure all of those actions are stopped.
You'll usually interact with devices with Device instances, which will be different than the Buttplug Client. While the Client handles things like scanning and device lists, a Device instance will let you command a specific device.
Yes!
And it was really complicated!
And almost no one used that functionality!
And something like < 10% of the devices we support even have > 1 output features!
In the Buttplug v4 message spec, we now put the burden of messages aggregation on the Server. Users send one command for one feature of one device, and we do the work on the backend to smash all of those commands together into the smallest possible packet to go to the hardware. If you're curious about the way this works, check the Devices and Commands section of this guide.
Sending Device Messages
As a user of a Buttplug Client API, you should never be expected to send raw Buttplug Messages. Most Client APIs will provide message sending functions for you, usually attached to device objects or structures. If the device accepts the message type represented by the function you call, it should be sent to the device. Otherwise, you'll receive an error about message compatibility.
- Rust
- C#
- Javascript (Web)
- TypeScript
- Python
use buttplug_client::{
ButtplugClient, ButtplugClientError, connector::ButtplugRemoteClientConnector, device::{ClientDeviceCommandValue, ClientDeviceOutputCommand}, serializer::ButtplugClientJSONSerializer
};
use buttplug_core::message::OutputType;
use buttplug_transport_websocket_tungstenite::ButtplugWebsocketClientTransport;
use strum::IntoEnumIterator;
use tokio::io::{self, AsyncBufReadExt, BufReader};
async fn wait_for_input() {
BufReader::new(io::stdin())
.lines()
.next_line()
.await
.unwrap();
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let connector = ButtplugRemoteClientConnector::<
ButtplugWebsocketClientTransport,
ButtplugClientJSONSerializer,
>::new(ButtplugWebsocketClientTransport::new_insecure_connector(
"ws://127.0.0.1:12345",
));
let client = ButtplugClient::new("Example Client");
client.connect(connector).await?;
println!("Connected!");
// You usually shouldn't run Start/Stop scanning back-to-back like
// this, but with TestDevice we know our device will be found when we
// call StartScanning, so we can get away with it.
client.start_scanning().await?;
client.stop_scanning().await?;
println!("Client currently knows about these devices:");
let mut device_index: i32 = -1;
for (i, device) in client.devices() {
device_index = i as i32;
println!("- {}", device.name());
}
wait_for_input().await;
for (_, device) in client.devices() {
println!("{} supports these outputs:", device.name());
for output_type in OutputType::iter() {
for feature in device.device_features().values() {
if let Some(output) = feature.feature().output()
&& output.contains(output_type)
{
println!("- {}", output_type);
}
}
}
}
println!("Sending commands");
// Now that we know the message types for our connected device, we
// can send a message over! Seeing as we want to stick with the
// modern generic messages, we'll go with VibrateCmd.
//
// There's a couple of ways to send this message.
let devices = client.devices();
let test_client_device = devices.get(&(device_index as u32)).unwrap();
// We can use the convenience functions on ButtplugClientDevice to
// send the message. This version sets all of the motors on a
// vibrating device to the same speed.
test_client_device.run_output(&ClientDeviceOutputCommand::Vibrate(ClientDeviceCommandValue::Percent(0.5f64))).await?;
// If we wanted to just set one motor on and the other off, we could
// try this version that uses an array. It'll throw an exception if
// the array isn't the same size as the number of motors available as
// denoted by FeatureCount, though.
//
// You can get the vibrator count using the following code, though we
// know it's 2 so we don't really have to use it.
let vibrator_count = test_client_device.outputs(OutputType::Vibrate).len();
println!(
"{} has {} vibrators.",
test_client_device.name(),
vibrator_count,
);
// Just set all of the vibrators to full speed.
if vibrator_count > 0 {
test_client_device.run_output(&ClientDeviceOutputCommand::Vibrate(ClientDeviceCommandValue::Steps(10))).await?;
} else {
println!("Device does not have > 1 vibrators, not running multiple vibrator test.");
}
wait_for_input().await;
println!("Disconnecting");
// And now we disconnect as usual.
client.disconnect().await?;
println!("Trying error");
// If we try to send a command to a device after the client has
// disconnected, we'll get an exception thrown.
let vibrate_result = test_client_device.run_output(&ClientDeviceOutputCommand::Vibrate(ClientDeviceCommandValue::Steps(30))).await;
if let Err(ButtplugClientError::ButtplugConnectorError(error)) = vibrate_result {
println!("Tried to send after disconnection! Error: ");
println!("{}", error);
}
wait_for_input().await;
Ok(())
}
// Buttplug C# - Device Control Example
//
// This example demonstrates how to send commands to devices,
// query device capabilities, and use the command builder API.
using Buttplug.Client;
using Buttplug.Core.Messages;
var client = new ButtplugClient("Device Control Example");
// Connect and scan for devices
await client.ConnectAsync("ws://127.0.0.1:12345");
await client.StartScanningAsync();
Console.WriteLine("Turn on a device, then press Enter...");
Console.ReadLine();
await client.StopScanningAsync();
// Check if we have any devices
if (client.Devices.Length == 0)
{
Console.WriteLine("No devices found. Exiting.");
await client.DisconnectAsync();
return;
}
// Get the first device
var device = client.Devices[0];
Console.WriteLine($"\nUsing device: {device.Name}");
// Show what output types this device supports
Console.WriteLine("\nSupported output types:");
foreach (var feature in device.Features.Values)
{
var outputs = feature.FeatureDefinition.Output;
if (outputs != null)
{
foreach (var outputType in outputs)
{
Console.WriteLine($" - {outputType} (Feature {feature.FeatureIndex}: {feature.FeatureDescription})");
}
}
}
// Check for vibration support and demonstrate commands
if (device.HasOutput(OutputType.Vibrate))
{
var vibrateFeatures = device.GetFeaturesWithOutput(OutputType.Vibrate).ToList();
Console.WriteLine($"\nDevice has {vibrateFeatures.Count} vibrator(s).");
// Method 1: Use the convenience extension method
await device.RunOutputAsync(DeviceOutput.Vibrate.Percent(0.5));
await Task.Delay(1000);
// Method 2: Use the command builder API for more control
await device.RunOutputAsync(DeviceOutput.Vibrate.Percent(0.75));
await Task.Delay(1000);
// Method 3: Send command to a specific feature
if (vibrateFeatures.Count > 0)
{
var firstVibrator = vibrateFeatures[0];
await device.RunOutputAsync(firstVibrator.FeatureIndex, DeviceOutput.Vibrate.Percent(0.25));
await Task.Delay(1000);
}
// Stop the device
await device.StopAsync();
}
else
{
Console.WriteLine("\nDevice does not support vibration.");
}
// Demonstrate other output types if available
if (device.HasOutput(OutputType.Rotate))
{
await device.RunOutputAsync(DeviceOutput.Rotate.Percent(0.5));
await Task.Delay(1000);
await device.StopAsync();
}
if (device.HasOutput(OutputType.Position))
{
await device.RunOutputAsync(DeviceOutput.PositionWithDuration.Percent(1.0, 500));
await Task.Delay(1000);
await device.RunOutputAsync(DeviceOutput.PositionWithDuration.Percent(0.0, 500));
}
// Read battery level if supported
if (device.HasInput(InputType.Battery))
{
var battery = await device.BatteryAsync();
Console.WriteLine($"Battery: {battery * 100:F0}%");
}
Console.WriteLine("\nPress Enter to disconnect...");
Console.ReadLine();
// Disconnect - this automatically stops all devices
await client.DisconnectAsync();
Console.WriteLine("Disconnected.");
// Buttplug Web - Device Control Example
//
// This example demonstrates how to send commands to devices,
// query device capabilities, and use the v4 command builder API.
//
// Include Buttplug via CDN:
// <script src="https://cdn.jsdelivr.net/npm/buttplug@4.0.0/dist/web/buttplug.min.js"></script>
async function runDeviceControlExample() {
const connector = new Buttplug.ButtplugBrowserWebsocketClientConnector("ws://127.0.0.1:12345");
const client = new Buttplug.ButtplugClient("Device Control Example");
// Set up event handlers before connecting
client.addListener("deviceadded", async (device) => {
console.log(`Device connected: ${device.name}`);
// Show currently connected devices (client.devices is a Map in v4)
console.log("Currently connected devices:");
for (const [index, dev] of client.devices) {
console.log(` - ${dev.name} (Index: ${index})`);
}
// Check if device supports vibration using v4 API
if (!device.hasOutput(Buttplug.OutputType.Vibrate)) {
console.log("Device does not support vibration, skipping control demo.");
return;
}
console.log("Device supports vibration. Sending commands...");
try {
// Use the v4 command builder API
console.log("Vibrating at 100%...");
await device.runOutput(Buttplug.DeviceOutput.Vibrate.percent(1.0));
await new Promise(r => setTimeout(r, 1000));
console.log("Vibrating at 50%...");
await device.runOutput(Buttplug.DeviceOutput.Vibrate.percent(0.5));
await new Promise(r => setTimeout(r, 1000));
console.log("Stopping device...");
await device.stop();
} catch (e) {
console.log("Error sending command:", e);
if (e instanceof Buttplug.ButtplugDeviceError) {
console.log("This is a device error - device may have disconnected.");
}
}
// Check for battery support using v4 API
if (device.hasInput(Buttplug.InputType.Battery)) {
try {
const level = await device.battery();
console.log(`${device.name} Battery Level: ${(level * 100).toFixed(0)}%`);
} catch (e) {
console.log("Could not read battery level:", e);
}
}
// Demonstrate other output types if available
if (device.hasOutput(Buttplug.OutputType.Rotate)) {
console.log("Device supports rotation. Rotating at 50%...");
await device.runOutput(Buttplug.DeviceOutput.Rotate.percent(0.5));
await new Promise(r => setTimeout(r, 1000));
await device.stop();
}
if (device.hasOutput(Buttplug.OutputType.Position)) {
console.log("Device supports position control. Moving...");
await device.runOutput(Buttplug.DeviceOutput.PositionWithDuration.percent(1.0, 500));
await new Promise(r => setTimeout(r, 1000));
await device.runOutput(Buttplug.DeviceOutput.PositionWithDuration.percent(0.0, 500));
}
});
client.addListener("deviceremoved", (device) => {
console.log(`Device disconnected: ${device.name}`);
});
console.log("Connecting...");
await client.connect(connector);
console.log("Connected! Scanning for devices...");
await client.startScanning();
}
// Buttplug TypeScript - Device Control Example
//
// This example demonstrates how to send commands to devices,
// query device capabilities, and use the command builder API.
//
// Prerequisites:
// 1. Install Intiface Central: https://intiface.com/central
// 2. Start the server in Intiface Central
// 3. Run: npx ts-node --esm device-control-example.ts
import {
ButtplugClient,
ButtplugNodeWebsocketClientConnector,
ButtplugClientDevice,
DeviceOutput,
OutputType,
InputType,
} from 'buttplug';
import * as readline from 'readline';
async function waitForEnter(prompt: string): Promise<void> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(prompt, () => {
rl.close();
resolve();
});
});
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function main(): Promise<void> {
const client = new ButtplugClient('Device Control Example');
// Connect and scan for devices
const connector = new ButtplugNodeWebsocketClientConnector(
'ws://127.0.0.1:12345'
);
console.log('Connecting...');
await client.connect(connector);
console.log('Connected! Scanning for devices...');
await client.startScanning();
await waitForEnter('Turn on a device, then press Enter...');
await client.stopScanning();
// Check if we have any devices
if (client.devices.size === 0) {
console.log('No devices found. Exiting.');
await client.disconnect();
return;
}
// Get the first device
const device: ButtplugClientDevice = client.devices.values().next().value!;
console.log(`\nUsing device: ${device.name}`);
// Show what output types this device supports
console.log('\nSupported output types:');
for (const [index, feature] of device.features) {
const def = (feature as any)._feature;
if (def.Output) {
for (const outputType of Object.keys(def.Output)) {
console.log(
` - ${outputType} (Feature ${index}: ${def.FeatureDescriptor})`
);
}
}
}
// Check for vibration support and demonstrate commands
if (device.hasOutput(OutputType.Vibrate)) {
console.log('\nDevice supports vibration.');
// Use the command builder API
console.log('Vibrating at 50% using command builder...');
await device.runOutput(DeviceOutput.Vibrate.percent(0.5));
await delay(1000);
console.log('Vibrating at 75%...');
await device.runOutput(DeviceOutput.Vibrate.percent(0.75));
await delay(1000);
console.log('Vibrating at 25%...');
await device.runOutput(DeviceOutput.Vibrate.percent(0.25));
await delay(1000);
// Stop the device
console.log('Stopping device...');
await device.stop();
} else {
console.log('\nDevice does not support vibration.');
}
// Demonstrate other output types if available
if (device.hasOutput(OutputType.Rotate)) {
console.log('\nDevice supports rotation. Rotating at 50%...');
await device.runOutput(DeviceOutput.Rotate.percent(0.5));
await delay(1000);
await device.stop();
}
if (device.hasOutput(OutputType.Position)) {
console.log('\nDevice supports position control. Moving to 100% over 500ms...');
await device.runOutput(DeviceOutput.PositionWithDuration.percent(1.0, 500));
await delay(1000);
await device.runOutput(DeviceOutput.PositionWithDuration.percent(0.0, 500));
}
// Try reading battery level if supported
if (device.hasInput(InputType.Battery)) {
console.log('\nReading battery level...');
const battery = await device.battery();
console.log(`Battery: ${(battery * 100).toFixed(0)}%`);
}
await waitForEnter('\nPress Enter to disconnect...');
// Disconnect - this automatically stops all devices
await client.disconnect();
console.log('Disconnected.');
}
main().catch(console.error);
"""Device Control - Vibrate, rotate, and position commands.
This example shows how to control different types of devices:
- Vibrators: Set vibration intensity
- Rotators: Set rotation speed
- Strokers: Move to position over time
The example checks what each device supports before sending commands,
so it will work with any device type.
Prerequisites:
1. Install Intiface Central: https://intiface.com/central/
2. Start Intiface Central and click "Start Server"
3. Have a supported device connected
4. Run this script: python device_control.py
"""
import asyncio
from buttplug import ButtplugClient, DeviceOutputCommand, OutputType
async def main() -> None:
client = ButtplugClient("Device Control Example")
# Set up event handlers to see devices as they connect
client.on_device_added = lambda d: print(f"Device connected: {d.name}")
client.on_device_removed = lambda d: print(f"Device disconnected: {d.name}")
print("Connecting to server...")
await client.connect("ws://127.0.0.1:12345")
print("Scanning for devices (5 seconds)...")
await client.start_scanning()
await asyncio.sleep(5)
await client.stop_scanning()
if not client.devices:
print("No devices found!")
await client.disconnect()
return
# Control each device based on its capabilities
for device in client.devices.values():
print(f"\nControlling: {device.name}")
# Vibration
if device.has_output(OutputType.VIBRATE):
print(" Starting vibration at 25%...")
await device.run_output(DeviceOutputCommand(OutputType.VIBRATE, 0.25))
await asyncio.sleep(1)
print(" Increasing to 50%...")
await device.run_output(DeviceOutputCommand(OutputType.VIBRATE, 0.5))
await asyncio.sleep(1)
print(" Full power (100%)...")
await device.run_output(DeviceOutputCommand(OutputType.VIBRATE, 1.0))
await asyncio.sleep(1)
# Rotation
if device.has_output(OutputType.ROTATE):
print(" Rotating at 50%...")
await device.run_output(DeviceOutputCommand(OutputType.ROTATE, 0.5))
await asyncio.sleep(2)
# Position (strokers/linear devices)
if device.has_output(OutputType.POSITION_WITH_DURATION):
print(" Moving to top position...")
await device.run_output(
DeviceOutputCommand(OutputType.POSITION_WITH_DURATION, 1.0, duration=500)
)
await asyncio.sleep(1)
print(" Moving to bottom position...")
await device.run_output(
DeviceOutputCommand(OutputType.POSITION_WITH_DURATION, 0.0, duration=500)
)
await asyncio.sleep(1)
print(" Moving to middle...")
await device.run_output(
DeviceOutputCommand(OutputType.POSITION_WITH_DURATION, 0.5, duration=250)
)
await asyncio.sleep(1)
# Stop the device
print(" Stopping device...")
await device.stop()
print("\nAll done!")
await client.disconnect()
if __name__ == "__main__":
asyncio.run(main())