Skip to main content

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.

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?

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.

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(())
}

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.

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
}

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.