API Basics
Client implementations in Buttplug are built to look as similar as possible no matter what language you're using. However, there may be instances where language options (i.e. existence of things like first-class events) change the API slightly. This section goes over how the client APIs we've provided work in a generic manner.
Buttplug Session Overview
Let's review what a Buttplug Sessions are made up of. Some of this was covered in depth in the architecture section, so this will just be an overview, while also including some example code.
Buttplug sessions (the connection lifetime between the client and server) consist of the following steps.
- Application sets up a connection via a Connector class/object and creates a Client
- Client connects to the Server
- Client negotiates Server Handshake and Device List Update
- Application uses Client to request Device Scanning
- Server communicates Device Connection events to Client/Application.
- Application uses Device Instances to control hardware in Server
- At some point, Application/Client disconnects from the Server
Library Initialization
Depending on which programming language and/or package you're using, you may have to run some code prior to creating Buttplug instances and running commands.
- Rust
- C#
- Javascript
If you're using Rust, congratulations, you don't really have much of anything to worry about. Isn't using the natively implemented system great?
If you're using C#, we try to handle most of the initialization for you in our library. This may include detecting architectures and loading libraries to make sure that we find the correct library for the architecture you're running on. Hopefully this will be something you don't have to worry about. If you do run into problems, file an issue on our FFI repo.
Sorry Javascript/Typescript/Web users, unlike the other languages who don't have much to worry about, this section is mostly for you.
While most of our native systems are fairly seamless, loading our WASM in web situations takes a bit of extra work.
For those working on the web, here's a full HTML file example of how loading Buttplug for use works:
<html>
<head></head>
<body>
<script src="[buttplug CDN url here, see buttplug-js README for location/version]"></script>
<script lang="javascript">
// After we've loaded the module above, we'll have a "Buttplug" global we can access
// methods and classes from. We'll use that to initialize the library. This is
// required because of the way we have to load our WASM code into the application.
// You'll need to call buttplugInit(), which returns a promise that will resolve
// when WASM is loaded, at which point you can go ahead and run other Buttplug
// commands.
//
// We have this call in all of our examples in this guide, to remind users that
// this must happen when running the library. If you do not call this, you'll
// get errors in your developer console (and exceptions thrown) that will
// remind you that you need to do it.
Buttplug.buttplugInit().then(() => console.log("Buttplug Loaded"));
</script>
</body>
</html>
If you're using Webpack or another web application packing system in node, things can get complicated depending on your application setup. Rather than add an example here that may need to change often, I'm just going to refer you to the buttplug-js README about webpack setup, which will have the latest information on how to do this. If you have issues, either file an issue on our FFI repo or contact us via one of the support mechanisms (preferably discord).
Client/Server Interaction
There are two types of communication between the client and the server:
- Request/Response (Client -> Server -> Client)
- Client sends a message, server replies. For instance, when a device command is sent from the client, the server will return information afterward saying whether or not that command succeeded.
- Events (Server -> Client)
- Server sends a message to the client with no expectation of response. For instance, when a new device connects to the server, the server will tell the client the device has been added, but the server doesn't expect the client to acknowledge this. These messages are considered fire and forget.
Request/Response interaction between the client and the server may be a very long process. Sometimes 100s of milliseconds, or even multiple seconds if device connection quality is poor. In languages where it is available, Client APIs try to deal with this via usage of Async/Await.
For event messages, first-class events are used, where possible. Otherwise, callbacks, promises, streams, or other methods are used depending on language and library capabilities.
- Rust
- C#
- Javascript
use buttplug::{
client::{ButtplugClient, ButtplugClientEvent},
core::{
connector::{ButtplugRemoteClientConnector, ButtplugWebsocketClientTransport},
message::serializer::ButtplugClientJSONSerializer,
},
};
use futures::StreamExt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// In Rust, anything that will block is awaited. For instance, if we're going
// to connect to a remote server, that might take some time due to the network
// connection quality, or other issues. To deal with that, we use async/await.
//
// For now, you can ignore the API calls here, since we're just talking about
// how our API works in general. Setting up a connection is discussed more in
// the Connecting section of this document.
let connector = ButtplugRemoteClientConnector::<
ButtplugWebsocketClientTransport,
ButtplugClientJSONSerializer,
>::new(ButtplugWebsocketClientTransport::new_insecure_connector(
"ws://127.0.0.1:12345",
));
// For Request/Response messages, we'll use our Connect API. Connecting to a
// server requires the client and server to send information back and forth,
// so we'll await that while those (possibly somewhat slow, depending on if
// network is being used and other factors) transfers happen.
let client = ButtplugClient::new("Example Client");
client
.connect(connector)
.await
.expect("Can't connect to Buttplug Server, exiting!");
let mut event_stream = client.event_stream();
// As an example of event messages, we'll assume the server might
// send the client notifications about new devices that it has found.
// The client will let us know about this via events.
while let Some(event) = event_stream.next().await {
if let ButtplugClientEvent::DeviceAdded(device) = event {
println!("Device {} connected", device.name());
}
}
Ok(())
}
using System;
using System.Threading.Tasks;
using Buttplug.Client;
using Buttplug.Core;
namespace AsyncExample
{
class Program
{
static void OnDeviceAdded(object o, DeviceAddedEventArgs args)
{
Console.WriteLine($"Device ${args.Device.Name} connected");
}
static async Task AwaitExample()
{
// In CSharp, anything that will block is awaited. For instance, if
// we're going to connect to a remote server, that might take some
// time due to the network connection quality, or other issues. To
// deal with that, we use async/await.
//
// For now, you can ignore the API calls here, since we're just
// talking about how our API works in general. Setting up a
// connection is discussed more in the Connecting section of this
// document.
var connector =
new ButtplugWebsocketConnector(
new Uri("ws://localhost:12345/buttplug"));
var client =
new ButtplugClient("Example Client");
// As an example of events, we'll assume the server might send the
// client notifications about new devices that it has found. The
// client will let us know about this via events.
client.DeviceAdded += OnDeviceAdded;
// As an example response/reply messages, we'll use our Connect API.
// Connecting to a server requires the client and server to send
// information back and forth, so we'll await that while those
// transfers happen. It is possible for these to be slow, depending
// on if network is being used and other factors)
//
// If something goes wrong, we throw, which breaks out of the await.
try
{
await client.ConnectAsync(connector);
}
catch (ButtplugClientConnectorException ex)
{
Console.WriteLine(
"Can't connect to Buttplug Server, exiting!" +
$"Message: {ex.InnerException.Message}");
}
catch (ButtplugHandshakeException ex)
{
Console.WriteLine(
"Handshake with Buttplug Server, exiting!" +
$"Message: {ex.InnerException.Message}");
}
// There's also no requirement that the tasks returned from these
// methods be run immediately. Each method returns a task which will
// not run until awaited, so we can store it off and run it later,
// run it on the scheduler, etc...
//
// As a rule, if you don't want to worry about all of the async task
// scheduling and what not, you can just use "await" on methods when
// you call them and they'll block until return. This is the easiest
// way to work sometimes.
var startScanningTask = client.StartScanningAsync();
try
{
await startScanningTask;
}
catch (ButtplugException ex)
{
Console.WriteLine(
$"Scanning failed: {ex.InnerException.Message}");
}
}
static void Main(string[] args)
{
AwaitExample().Wait();
}
}
}
// This example assumes Buttplug is brought in as a root namespace, via
// inclusion by a script tag, i.e.
//
// <script lang="javascript"
// src="https://cdn.jsdelivr.net/npm/buttplug@3.0.0/dist/web/buttplug.min.js">
// </script>
//
// If you're trying to load this, change the version to the latest available.
// In javascript, we'll use es6 style async/await. Remember that await calls
// return promises, so how you deal with try/catch versus .then()/.catch() is up
// to you.
//
// See the "Dealing with Errors" section of the Developer Guide for more
// info on this.
async function runAsyncExample() {
console.log("Running async load example");
// As of v3, Buttplug no longer requires initialization.
// We'll use a websocket connector here, so you'll need to have Intiface Central open on your
// system.
const connector = new Buttplug.ButtplugBrowserWebsocketClientConnector("ws://127.0.0.1:12345");
const client = new Buttplug.ButtplugClient("Buttplug Example Client");
await client.connect(connector);
console.log("Client connected");
};
Dealing With Errors
As with all technology, things in Buttplug can and often will go wrong. Due to the context of Buttplug, the user may be having sex with/via an application when things go wrong.
This means things can go very, very wrong.
With that in mind, errors are covered before providing information on how to use things, in the overly optimistic hopes that developers will keep error handling in mind when creating their applications.
Errors in Buttplug sessions come in the follow classes:
- Handshake
- Client and Server connected successfully, but something went wrong when they were negotiating the session. This could include naming problems, schema compatibility issues (see next section), or other problems.
- Message
- Something went wrong in relation to message formation or communication. For instance, a message that was only supposed to be sent by a server to a client was sent in the opposite direction.
- Device
- Something went wrong with a device. For instance, the device may no longer be connected, or a message was sent to a device that has no capabilities to handle it.
- Ping
- If the ping system is in use, this means a ping was missed and the connection is no longer valid.
- Unknown
- Reserved for instances where a newer server version is talking to an older client version, and may have error types that would not be recognized by the older client. See next section for more info on this.
Custom exceptions or errors may also be thrown by implementations of Buttplug. For instance, a Connector may throw a custom error or exception based on the type of transport it is using. For more information, see the documentation of the specific Buttplug implementation you are using.
- Rust
- C#
- Javascript
use buttplug::{client::ButtplugClientError, core::errors::ButtplugError};
#[allow(dead_code)]
fn handle_error(error: ButtplugClientError) {
match error {
ButtplugClientError::ButtplugConnectorError(_details) => {}
ButtplugClientError::ButtplugError(error) => match error {
ButtplugError::ButtplugHandshakeError(_details) => {}
ButtplugError::ButtplugDeviceError(_details) => {}
ButtplugError::ButtplugMessageError(_details) => {}
ButtplugError::ButtplugPingError(_details) => {}
ButtplugError::ButtplugUnknownError(_details) => {}
},
}
}
fn main() {
// nothing to do here
}
// See https://aka.ms/new-console-template for more information
using Buttplug.Core;
using Buttplug.Client;
namespace ExceptionExample
{
class Program
{
static void Main(string[] args)
{
// In C#, we've got the 5 main exception classes, plus
// ButtplugConnectorException. All of these are children of
// ButtplugException, so catching ButtplugException will catch all
// Buttplug errors but let all other system Exceptions bubble up.
try
{
}
catch (ButtplugHandshakeException)
{
// Something went wrong during the connection handshake. Usually
// means there was a client/server version mismatch.
}
catch (ButtplugDeviceException)
{
// Something went wrong with a device. Tried to send commands to
// a disconnected device, command was malformed or not accepted
// by the devices, etc...
}
catch (ButtplugMessageException)
{
// Something went wrong with message formation. Tried to send a
// message out of order, or that the server didn't support,
// etc...
}
catch (ButtplugPingException)
{
// Client/Server disconnected due to ping timeout.
}
catch (ButtplugClientConnectorException)
{
// Something went wrong in the connection between the client and
// the server. This will only happen with remote connectors.
}
catch (ButtplugException)
{
// If you just want to catch everything that can go wrong in the
// Buttplug library, catch this.
}
}
}
}
// This example assumes Buttplug is brought in as a root namespace, via
// inclusion by a script tag, i.e.
//
// <script lang="javascript"
// src="https://cdn.jsdelivr.net/npm/buttplug@3.0.0/dist/web/buttplug.min.js">
// </script>
//
// If you're trying to load this, change the version to the latest available.
async function runErrorExample () {
async function ThrowError() {
// All async functions in Buttplug are written to return exceptions as a
// promise rejection, meaning they work as both promise chains and
// async/await.
throw new ButtplugDeviceError("This is an exception", 0);
}
async function ButtplugErrors() {
const invalid_options = new Buttplug.ButtplugBrowserWebsocketClientConnector("ws://notadomain.local");
const client = new Buttplug.ButtplugClient("Error Example Client");
//invalid_options.Address = "this is not a websocket address";
// In javascript, there are 2 ways we can call functions and catch exceptions.
// There's promise chain catching.
client
.connect(invalid_options)
.then(() => {
console.log("If you got here, shut down Intiface Central or whatever other server you're running :P");
})
.catch(e => {
console.log("Using .catch()");
console.log(e);
});
// There's also try/catch, which is handy for async.
try {
await client.connect(invalid_options);
} catch (e) {
// However, we don't have the type of the exception we get back, so it could
// be a system exception or something else not buttplug related. If you're
// interested in Buttplug related exceptions, it's best to check for them
// here.
console.log(`${e}`);
if (e instanceof Buttplug.ButtplugError) {
console.log("this is a buttplug error");
// This will make sure we're doing something specific to Buttplug.
if (e instanceof Buttplug.ButtplugClientConnectorError) {
console.log("This is a connector error");
// And possibly even more specific.
}
} else {
console.log("Was another type of error");
}
}
// However, as all async javascript functions also return promises, so we can
// treat the call as a promise rejection.
//ThrowError().catch((e) => console.log("Got an exception back from our promise!"));
}
ButtplugErrors();
}
NOTE: You may notice that there's no way to tell exactly what an error is from this message. You get a class, but the information itself is encoded in the message, which is not standardized. Therefore it's impossible to tell whether a device disconnected, or you just send a connected device an incorrect message. This is bad, and will hopefully be fixed at some point in the future.