Skip to content

Client Libraries

Besides embedding the full stack with libVIIPER, VIIPER ships a standalone server and generated TCP client libraries for several languages. This is the right choice when the device logic lives in a different process than the USBIP server, when you want multiple feeders on one machine, or when your language does not have convenient C FFI bindings.

Architecture

Feeder application (C#, C++, Rust, TS, Go, ...)
        |
        | TCP / JSON + binary device streams (port 3242)
        v
viiper server (USB-IP on port 3241)
        |
        | USBIP
        v
usbip-win2 / vhci-hcd virtual host controller
        |
        v
OS, games, audio services

The viiper binary acts as a TCP server. Management commands (list/create/remove buses and devices) are plain-text JSON requests; device input/output streams are binary. All traffic is authenticated and encrypted by default (PBKDF2-SHA256 key derivation + ChaCha20-Poly1305 session encryption), so the client libraries handle the handshake for you when you pass a password.

Running the server

Start the server with the server subcommand:

viiper server

On first run the server generates a random API password and stores it in a key file. On Windows this is %APPDATA%\VIIPER\viiper.key.txt; on Linux it is ~/.config/github.com/DualSenseClient/viiper/viiper.key.txt (or /etc/viiper/ for root). The password is also printed to the console on first start.

Key configuration options (see viiper server --help for all of them):

Flag / env Default Description
--usb.addr / VIIPER_USB_ADDR :3241 USB-IP listen address
--api.addr / VIIPER_API_ADDR :3242 TCP API listen address
--api.auto-attach-local-client / VIIPER_API_AUTO_ATTACH_LOCAL_CLIENT true Auto-attach created devices to the local USBIP client
--api.require-localhost-auth / VIIPER_API_REQUIRE_LOCALHOST_AUTH false Require the password even for localhost connections

USBIP required

The server still needs a USBIP client/driver on the machine that should see the virtual devices, exactly like libVIIPER. See Installation › Requirements.

API overview

Endpoint Description
ping Returns server + version
bus/list List all buses
bus/create [id] Create a bus (auto-assign ID if omitted)
bus/remove <id> Remove a bus and all its devices
bus/{id}/list List devices on a bus
bus/{id}/add <json> Add a device ({"type":"xbox360","deviceSpecific":{...}})
bus/{id}/remove <devId> Remove a device
bus/{busId}/{deviceid} Open the binary device stream

C# — Viiper.Client

The generated C# client library is published as a NuGet package (Viiper.Client) and its source lives in clients/csharp/.

using System.Linq;
using Viiper.Client;
using Viiper.Client.Devices.Xbox360;
using Viiper.Client.Types;

var client = new ViiperClient("localhost", 3242, "your-password");

// Find or create a bus
var list = await client.BusListAsync();
uint busId;
bool createdBus = false;
if (list.Buses.Length == 0)
{
    busId = (await client.BusCreateAsync(null)).BusID;
    createdBus = true;
}
else
{
    busId = list.Buses.Min();
}

// Add a device and open its stream
var devInfo = await client.BusDeviceAddAsync(busId, new DeviceCreateRequest { Type = "xbox360" });
var device = await client.ConnectDeviceAsync(devInfo.BusID, devInfo.DevID);

// React to host output (rumble)
device.OnOutput = async stream =>
{
    var buf = new byte[Xbox360.OutputSize];
    await stream.ReadAsync(buf, 0, buf.Length);
    Console.WriteLine($"<- Rumble: Left={buf[0]}, Right={buf[1]}");
};

// Send input at ~60 Hz
var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(16));
while (await timer.WaitForNextTickAsync())
{
    var state = new Xbox360Input
    {
        Buttons = (uint)Button.A,
        Lt = 128,
        Rt = 0,
        Lx = 20000,
        Ly = 0,
        Rx = 0,
        Ry = 0,
    };
    await device.SendAsync(state);
}

// Cleanup
await client.BusDeviceRemoveAsync(devInfo.BusID, devInfo.DevID);
if (createdBus) await client.BusRemoveAsync(busId);

Device-specific input types, output sizes and constants are under Viiper.Client.Devices.* (e.g. Xbox360Input, Dualshock4Input, DualsenseInput, KeyboardInput, MouseInput, Ns2proInput and their output helpers).

See examples/csharp/ for complete programs (virtual keyboard, mouse, Xbox 360 pad and DualShock 4 pad).

C++

The C++ client is a header-only library requiring C++20, nlohmann/json and OpenSSL. Source: clients/cpp/.

#include <viiper/viiper.hpp>
#include <thread>

int main(int argc, char** argv) {
    viiper::ViiperClient client("localhost", 3242, "your-password");

    auto buses_result = client.buslist();
    uint32_t bus_id = buses_result.value().buses[0];

    auto device_result = client.busdeviceadd(bus_id, {.type = "keyboard"});
    auto stream = client.connectDevice(device_result.value().busid,
                                       device_result.value().devid).value();

    stream->on_output(viiper::keyboard::OUTPUT_SIZE, [](const uint8_t* data, std::size_t len) {
        printf("<- LEDs: 0x%02X\n", data[0]);
    });

    viiper::keyboard::Input down = { .modifiers = 0, .keys = {viiper::keyboard::KeyEnter} };
    stream->send(down);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    stream->send(viiper::keyboard::Input{.modifiers = 0, .keys = {}});
}

See examples/cpp/ for the CMake setup and full examples.

Rust

The Rust client crate is viiper-client and supports both sync and async (Tokio) usage. Source: clients/rust/.

use viiper_client::AsyncViiperClient;
use viiper_client::devices::keyboard::*;

#[tokio::main]
async fn main() {
    let client = AsyncViiperClient::new("localhost:3242".parse().unwrap());

    let resp = client.bus_list().await.unwrap();
    let bus_id = resp.buses[0];

    let device_info = client
        .bus_device_add(bus_id, &viiper_client::types::DeviceCreateRequest {
            r#type: Some("keyboard".to_string()),
            id_vendor: None,
            id_product: None,
            device_specific: None,
        })
        .await
        .unwrap();

    let mut stream = client
        .connect_device(device_info.bus_id, &device_info.dev_id)
        .await
        .unwrap();

    stream.send(&KeyboardInput {
        modifiers: 0,
        count: 1,
        keys: vec![KEY_ENTER],
    }).await.unwrap();
}

See examples/rust/ for the complete sync and async examples.

TypeScript

The TypeScript client is published as the viiperclient npm package. Source: clients/typescript/.

import { ViiperClient, Keyboard } from "viiperclient";

const client = new ViiperClient("localhost", 3242, "your-password");

const buses = await client.buslist();
const busID = buses.buses[0];

const { device, response } = await client.addDeviceAndConnect(busID, { type: "keyboard" });

device.on("output", (buf: Buffer) => {
    console.log("<- LEDs: 0x" + buf.readUInt8(0).toString(16));
});

const down = new Keyboard.KeyboardInput({ Modifiers: 0, Count: 1, Keys: [Keyboard.Key.Enter] });
await device.send(down);

See examples/typescript/.

Go

The Go client library lives in the repository itself (viiperclient package) and reuses the device packages directly. Examples: examples/go/.

import (
    "context"
    "github.com/DualSenseClient/VIIPER/viiperclient"
    "github.com/DualSenseClient/VIIPER/device/xbox360"
)

api := viiperclient.NewWithPassword("localhost:3242", "your-password")

stream, _, err := api.AddDeviceAndConnect(context.Background(), busID, "xbox360", nil)
if err != nil { /* ... */ }
defer stream.Close()

_ = stream.WriteBinary(&xbox360.InputState{
    Buttons: xbox360.ButtonA,
    LT:      128,
    LX:      20000,
})

Choosing between libVIIPER and the TCP clients

libVIIPER Client libraries
Process model In-process (embedded) Separate viiper server process
API style Pure C (P/Invoke / FFI) Native, generated per language
Latency Lowest possible Very low (loopback TCP)
Multiple feeders One per process Many clients, one server
Remote usage No Yes (over network, encrypted)
License GPL-3.0 MIT (client libraries)