What Do Devices Do?
We now know that devices are connected to the client, but how do we figure out what we can do with them? In this section, we'll discover what inputs and ouputs a device has, and how we can use them.
Example Device
We'll be presenting a fairly standard, simple device here just to get things going. As of this writing, buttplug supports something like 750 devices with myriad outputs and inputs. We'll cover the full picture of everything the library supports and specific usecases in the Device Outputs portion of the Winning Ways Section later, once the basics are understood.
The device we'll be discussing here has the following features:
- 2 vibration motors, individually controllable, each with 50 steps of vibration speed.
- A battery that we can query for how much power it has left
This is vaguely similar to something like a Lovense Edge, but form factor isn't really an issue here. We just want an example of something we can control.
As an app developer, this is really all you need to know about the device. Buttplug hides most information about how things are connected (bluetooth, usb, etc) so you don't have to worry about them.
What even is a Buttplug Device?
Whenever we get a DeviceAdded() event, it'll usually come with some sort of structure representing the device. This will include:
- The Device Index
- A unique 32-bit unsigned integer that identifies the device to the server. As long as the user does not clear their server configuration, these indexes can be considered to be stable and usable for saving configuration options across sessions of your application.
- The Device Name, in 2 forms
- The canonical device name, as set in the Buttplug Device Config
- The user/display device name, which is a name users can set for a device so they can differentiate it from other devices.
- A message gap duration, in milliseconds
- This refers to the amount of time that the Buttplug Server will put between two messages, so that we don't end up with queued messages. This will be discussed more in the device control section.
- A set of features
- This denotes what a device actually does, we'll spend the rest of this section talking about these.
Device Features
Devices in Buttplug are made up of features.
Features contain:
- The Feature Index
- Similar to the Device Index. Unique (in the scope of the device) 32-bit unsigned integer that specifies which feature is which. A combination of Device Index and Feature Index are used to write command messages, outlined in the next section.
- A feature description
- Describes what the feature does, in English. Useful for showing in UI.
- A feature type
- This is the main function of the feature, though it may support multiple ways of doing things (i.e. a stroker that can work with positions or can just oscillate between two positions, a motor with an encoder so position can be both set and read, etc...). Feature types are only for getting a general idea of what a feature does, and are not used in commands.
- Outputs
- Things that the device does. Vibration, rotation, stroking, etc..., are all outputs.
- Output information will contain the OutputTypes a feature exposes, as well as the amount of steps an output can handle. For instance, with the device example we laid out above, steps would be 50 by default. However, servers like Intiface Central allow users to set upper limits that may be lower than the maximum a device can handle, so steps may be listed as lower than 50.
- Inputs
- Things a device can sense and relay information to us about. Battery levels, RSSI for radio connections, button presses, pressure sensors, are all types of inputs.
- Like Outputs, Input information will contain the InputTypes a feature exposes, and will also contain information about the possible values they can return. For instance, batteries will always return a number between 0-100, representing the percentage of power they have left. Other input types, like various pressure sensors, may vary in their output range.
They did! In prior versions of the library, we had MessageAttributes because each message denoted a sort of output type, which was horrible and complicated and very difficult to talk about, much less write useful documentation for. Parts of the v3 api moved us to using the actuator/sensor terminology. This was kept through the early parts of v4 api development.
Then I realized that the project is mostly referred to as buttplug dot io these days, thanks to the shitpost of a domain I got when I started all this. So why not name them Outputs and Inputs?
Is it less clear? Possibly. Is it both more on brand and hilarious? ABSOLUTELY.
Thus, Outputs and Inputs it is.
Will it be any easier to write documentation for? I refer you to the previous point about hilarity, which will hopefully hide any issues with documentation complexity from here on out.
Here's a few examples of what a device feature can look like:
- A single vibrator with 20 steps of vibration
- This will be a feature with a single Output, of type Vibrate, with steps set to 20. Pretty easy.
- A stroker
- This will be a feature with two output types: One that allows us to send the device to a position over the duration of time (HwPositionWithDuration), and another type that allows us to set the speed of oscillation between two points (Oscillate)
- A motor with an encoder
- This will be a feature with 1 output type of HwPositionWithDuration, and 1 input type of Position where we can read the current position of the motor at any time for setting up our own control loops.
- Note: The Position input type is just a handy example for the reason a feature might have both outputs and inputs. It does not actually exist in the library yet. Buttplug does not support any toys that actually tell us their current position, speed, or anything else. Not because we just haven't had time to support them, but because no toys exposing that information exist that we know of. I hate working with sex toys so fucking much. Why am I writing this library.
For our example above, we should expect:
- A single device, with 3 Features
- A feature for the first vibrator, exposing one Output of type Vibrate with 50 steps
- A feature for the second vibrator, exposing one Output of type Vibrate with 50 steps
- A feature for the battery, exposing one Input of type Battery. The battery type is implicitly assumed to have a range of 0 <= x <= 100, so no range information is sent.
Querying Device Feature Information
We'll start from where we left off in the last section. You've established a connection to a
Buttplug Server, you've set up your event handlers, and you've just gotten a DeviceAdded() event.
We know there's a new device, but how can we tell what it does?
This code block shows how we can query the device and see what's available.
- Rust
- C#
- Javascript (Web)
- TypeScript
- Python
use buttplug_client::{
ButtplugClient,
connector::ButtplugRemoteClientConnector,
serializer::ButtplugClientJSONSerializer,
};
use buttplug_core::message::{InputType, 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("Device Info Example");
client.connect(connector).await?;
// Scan for devices
client.start_scanning().await?;
println!("Scanning for devices. Press Enter when ready...");
wait_for_input().await;
client.stop_scanning().await?;
// Iterate through all connected devices
for (_, device) in client.devices() {
println!("\n=== Device: {} ===", device.name());
println!("Index: {}", device.index());
println!("Display Name: {:?}", device.display_name());
// Get all features for this device
let features = device.device_features();
println!("\nFeatures ({} total):", features.len());
for (feature_index, feature) in features {
let feature_def = feature.feature();
println!("\n Feature {}:", feature_index);
println!(" Description: {:?}", feature_def.description());
println!(" Type: {:?}", feature_def.feature_type());
// Check for outputs (things the device can do)
if let Some(output) = feature_def.output() {
println!(" Outputs:");
for output_type in OutputType::iter() {
if output.contains(output_type) {
println!(" - {:?} (steps: {:?})", output_type, output.steps());
}
}
}
// Check for inputs (things the device can sense)
if let Some(input) = feature_def.input() {
println!(" Inputs:");
for input_type in InputType::iter() {
if input.contains(input_type) {
println!(" - {:?}", input_type);
}
}
}
}
// Convenience methods for checking specific capabilities
println!("\nCapability summary:");
let vibrate_features = device.vibrate_features();
if !vibrate_features.is_empty() {
println!(" - {} vibrator(s)", vibrate_features.len());
}
let rotate_features = device.rotate_features();
if !rotate_features.is_empty() {
println!(" - {} rotator(s)", rotate_features.len());
}
let linear_features = device.linear_features();
if !linear_features.is_empty() {
println!(" - {} linear actuator(s)", linear_features.len());
}
}
println!("\nPress Enter to disconnect...");
wait_for_input().await;
client.disconnect().await?;
Ok(())
}
// Buttplug C# - Device Info Example
//
// This example demonstrates how to query device capabilities,
// including features, outputs, and inputs.
using Buttplug.Client;
using Buttplug.Core.Messages;
var client = new ButtplugClient("Device Info Example");
// Connect and scan for devices
Console.WriteLine("Connecting...");
await client.ConnectAsync("ws://127.0.0.1:12345");
Console.WriteLine("Connected! Scanning for devices...");
await client.StartScanningAsync();
Console.WriteLine("Turn on a device, then press Enter...");
Console.ReadLine();
await client.StopScanningAsync();
// Iterate through all connected devices
foreach (var device in client.Devices)
{
Console.WriteLine($"\n=== Device: {device.Name} ===");
Console.WriteLine($"Index: {device.Index}");
Console.WriteLine($"Display Name: {device.DisplayName}");
// Iterate through all features on this device
Console.WriteLine($"\nFeatures ({device.Features.Count} total):");
foreach (var feature in device.Features.Values)
{
Console.WriteLine($"\n Feature {feature.FeatureIndex}:");
Console.WriteLine($" Description: {feature.FeatureDescriptor}");
// Check for outputs (things the device can do)
var outputs = feature.FeatureDefinition.Output;
if (outputs != null && outputs.Count > 0)
{
Console.WriteLine(" Outputs:");
foreach (var outputType in outputs)
{
var steps = feature.FeatureDefinition.OutputSteps;
Console.WriteLine($" - {outputType} (steps: {steps})");
}
}
// Check for inputs (things the device can sense)
var inputs = feature.FeatureDefinition.Input;
if (inputs != null && inputs.Count > 0)
{
Console.WriteLine(" Inputs:");
foreach (var inputType in inputs)
{
Console.WriteLine($" - {inputType}");
}
}
}
// Use convenience methods to check specific capabilities
Console.WriteLine("\nCapability summary:");
if (device.HasOutput(OutputType.Vibrate))
{
var vibrateFeatures = device.GetFeaturesWithOutput(OutputType.Vibrate).ToList();
Console.WriteLine($" - {vibrateFeatures.Count} vibrator(s)");
}
if (device.HasOutput(OutputType.Rotate))
{
var rotateFeatures = device.GetFeaturesWithOutput(OutputType.Rotate).ToList();
Console.WriteLine($" - {rotateFeatures.Count} rotator(s)");
}
if (device.HasOutput(OutputType.Position))
{
var positionFeatures = device.GetFeaturesWithOutput(OutputType.Position).ToList();
Console.WriteLine($" - {positionFeatures.Count} linear actuator(s)");
}
if (device.HasInput(InputType.Battery))
{
Console.WriteLine(" - Battery level sensor");
}
if (device.HasInput(InputType.RSSI))
{
Console.WriteLine(" - Signal strength (RSSI) sensor");
}
}
Console.WriteLine("\nPress Enter to disconnect...");
Console.ReadLine();
await client.DisconnectAsync();
Console.WriteLine("Disconnected.");
// Buttplug Web - Device Info Example
//
// This example demonstrates how to introspect device features
// and capabilities in detail using the v4 API.
//
// Include Buttplug via CDN:
// <script src="https://cdn.jsdelivr.net/npm/buttplug@4.0.0/dist/web/buttplug.min.js"></script>
function printDeviceInfo(device) {
console.log("==================================================");
console.log(`Device: ${device.name}`);
if (device.displayName) {
console.log(`Display Name: ${device.displayName}`);
}
console.log(`Index: ${device.index}`);
if (device.messageTimingGap !== undefined) {
console.log(`Message Timing Gap: ${device.messageTimingGap}ms`);
}
console.log("==================================================");
// Collect output capabilities
const outputTypes = [];
if (device.hasOutput(Buttplug.OutputType.Vibrate)) outputTypes.push("Vibrate");
if (device.hasOutput(Buttplug.OutputType.Rotate)) outputTypes.push("Rotate");
if (device.hasOutput(Buttplug.OutputType.Oscillate)) outputTypes.push("Oscillate");
if (device.hasOutput(Buttplug.OutputType.Position)) outputTypes.push("Position");
if (device.hasOutput(Buttplug.OutputType.Constrict)) outputTypes.push("Constrict");
if (device.hasOutput(Buttplug.OutputType.Inflate)) outputTypes.push("Inflate");
if (device.hasOutput(Buttplug.OutputType.Temperature)) outputTypes.push("Temperature");
if (device.hasOutput(Buttplug.OutputType.Led)) outputTypes.push("LED");
if (outputTypes.length > 0) {
console.log(`\nOutput Capabilities: ${outputTypes.join(", ")}`);
}
// Collect input capabilities
const inputTypes = [];
if (device.hasInput(Buttplug.InputType.Battery)) inputTypes.push("Battery");
if (device.hasInput(Buttplug.InputType.RSSI)) inputTypes.push("RSSI");
if (device.hasInput(Buttplug.InputType.Button)) inputTypes.push("Button");
if (device.hasInput(Buttplug.InputType.Pressure)) inputTypes.push("Pressure");
if (inputTypes.length > 0) {
console.log(`Input Capabilities: ${inputTypes.join(", ")}`);
}
// Detailed feature breakdown
console.log("\nDetailed Features:");
for (const [index, feature] of device.features) {
// Access the underlying feature definition
const def = feature._feature;
console.log(`\n Feature ${index}: ${def.FeatureDescriptor}`);
if (def.Output) {
console.log(" Outputs:");
for (const [type, config] of Object.entries(def.Output)) {
console.log(` - ${type}: steps ${config.Value[0]}-${config.Value[1]}`);
}
}
if (def.Input) {
console.log(" Inputs:");
for (const [type, config] of Object.entries(def.Input)) {
console.log(` - ${type}: commands [${config.Command.join(", ")}]`);
}
}
}
}
async function runDeviceInfoExample() {
const client = new Buttplug.ButtplugClient("Device Info Example");
// Connect to the server
const connector = new Buttplug.ButtplugBrowserWebsocketClientConnector("ws://127.0.0.1:12345");
console.log("Connecting...");
await client.connect(connector);
console.log("Connected! Scanning for devices...");
console.log("Turn on your devices now. Info will be printed as they connect.\n");
// Set up device event to print info when devices connect
client.addListener("deviceadded", (device) => {
printDeviceInfo(device);
});
await client.startScanning();
// After a few seconds, also show summary of all connected devices
setTimeout(() => {
console.log("\n\n========== SUMMARY ==========");
if (client.devices.size === 0) {
console.log("No devices connected.");
} else {
console.log(`Found ${client.devices.size} device(s):`);
for (const [index, device] of client.devices) {
console.log(` ${index}: ${device.name}`);
}
}
}, 5000);
}
// Buttplug TypeScript - Device Info Example
//
// This example demonstrates how to introspect device features
// and capabilities in detail.
//
// Prerequisites:
// 1. Install Intiface Central: https://intiface.com/central
// 2. Start the server in Intiface Central
// 3. Run: npx ts-node --esm device-info-example.ts
import {
ButtplugClient,
ButtplugNodeWebsocketClientConnector,
ButtplugClientDevice,
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 printDeviceInfo(device: ButtplugClientDevice): void {
console.log(`\n${'='.repeat(50)}`);
console.log(`Device: ${device.name}`);
if (device.displayName) {
console.log(`Display Name: ${device.displayName}`);
}
console.log(`Index: ${device.index}`);
if (device.messageTimingGap !== undefined) {
console.log(`Message Timing Gap: ${device.messageTimingGap}ms`);
}
console.log(`${'='.repeat(50)}`);
// Collect output capabilities
const outputTypes: string[] = [];
if (device.hasOutput(OutputType.Vibrate)) outputTypes.push('Vibrate');
if (device.hasOutput(OutputType.Rotate)) outputTypes.push('Rotate');
if (device.hasOutput(OutputType.Oscillate)) outputTypes.push('Oscillate');
if (device.hasOutput(OutputType.Position)) outputTypes.push('Position');
if (device.hasOutput(OutputType.Constrict)) outputTypes.push('Constrict');
if (device.hasOutput(OutputType.Inflate)) outputTypes.push('Inflate');
if (device.hasOutput(OutputType.Temperature)) outputTypes.push('Temperature');
if (device.hasOutput(OutputType.Led)) outputTypes.push('LED');
if (outputTypes.length > 0) {
console.log(`\nOutput Capabilities: ${outputTypes.join(', ')}`);
}
// Collect input capabilities
const inputTypes: string[] = [];
if (device.hasInput(InputType.Battery)) inputTypes.push('Battery');
if (device.hasInput(InputType.RSSI)) inputTypes.push('RSSI');
if (device.hasInput(InputType.Button)) inputTypes.push('Button');
if (device.hasInput(InputType.Pressure)) inputTypes.push('Pressure');
if (inputTypes.length > 0) {
console.log(`Input Capabilities: ${inputTypes.join(', ')}`);
}
// Detailed feature breakdown
console.log('\nDetailed Features:');
for (const [index, feature] of device.features) {
// Access the underlying feature definition
const def = (feature as any)._feature;
console.log(`\n Feature ${index}: ${def.FeatureDescriptor}`);
if (def.Output) {
console.log(' Outputs:');
for (const [type, config] of Object.entries(def.Output)) {
const cfg = config as { Value: number[] };
console.log(
` - ${type}: steps ${cfg.Value[0]}-${cfg.Value[1]}`
);
}
}
if (def.Input) {
console.log(' Inputs:');
for (const [type, config] of Object.entries(def.Input)) {
const cfg = config as { Value: number[]; Command: string[] };
console.log(
` - ${type}: commands [${cfg.Command.join(', ')}]`
);
}
}
}
}
async function main(): Promise<void> {
const client = new ButtplugClient('Device Info Example');
// Connect
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 your devices, then press Enter...');
await client.stopScanning();
// Display info for all connected devices
if (client.devices.size === 0) {
console.log('No devices found.');
} else {
console.log(`\nFound ${client.devices.size} device(s):`);
for (const [_, device] of client.devices) {
printDeviceInfo(device);
}
}
await waitForEnter('\nPress Enter to disconnect...');
await client.disconnect();
console.log('Disconnected.');
}
main().catch(console.error);
"""Device Info - Inspect device capabilities.
This example shows how to inspect device features and capabilities:
- List all available features
- Check output types (vibrate, rotate, position)
- Check input types (battery, sensors)
- Access individual motors on multi-motor devices
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_info.py
"""
import asyncio
from buttplug import ButtplugClient, OutputType
async def main() -> None:
client = ButtplugClient("Device Info Example")
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
# Inspect each device's features in detail
for device in client.devices.values():
print(f"\n{'=' * 50}")
print(f"Device: {device.name}")
print(f"Index: {device.index}")
print(f"Display Name: {device.display_name or '(none)'}")
print(f"Timing Gap: {device.message_timing_gap}ms")
print(f"{'=' * 50}")
# List all features
print(f"\nFeatures ({len(device.features)}):")
for feature in device.features.values():
print(f"\n Feature {feature.index}: {feature.description or '(no description)'}")
# Show outputs
if feature.outputs:
print(" Outputs:")
for output_type in feature.outputs:
value_range = feature.get_output_range(output_type)
duration_range = feature.get_output_duration_range(output_type)
print(f" - {output_type}: values {value_range}", end="")
if duration_range:
print(f", duration {duration_range}ms", end="")
print()
# Show inputs
if feature.inputs:
print(" Inputs:")
for input_type, input_def in feature.inputs.items():
print(f" - {input_type}: commands {input_def.command}")
# Show multi-motor info
vibrate_features = device.get_features_with_output(OutputType.VIBRATE)
if len(vibrate_features) > 1:
print(f"\nThis device has {len(vibrate_features)} independent vibrators!")
print("Use device.send_output() to control them individually.")
await client.disconnect()
print("\nDone!")
if __name__ == "__main__":
asyncio.run(main())