Usage
Complete Setup first. This guide starts with Basic Sample and then shows how to use the same client API in an application.
To run Basic Sample or an SDK application on another PC, follow Connect over a trusted LAN.
Sample scenes
| Scene | Use |
|---|---|
BasicSample.unity | Inspect DivingStation data, input and body vibration; display skeletons with spheres and cylinders |
HandVisualizer.unity | Imported separately from Unity’s official XR Hands package; displays 26-joint hands supplied by an OpenXR runtime |
Basic Sample shows left/right joint-angle panels on the left, a hand preview at the upper right, and the MAGNETRA input panel below it. The hand preview draws the skeleton with spheres and cylinders.
Read the display
- LEFT HAND / RIGHT HAND shows 20 joint angles in degrees.
- MAGNETRA shows six button states and analog values. Yellow indicates touching; green indicates pressed. Grip force is normalized to
0–1. - The hand preview displays the selected raw or retargeted skeleton. Inspect its tracking status as well as its shape.
- The per-side status shows the chosen glove ID, battery and external-power state. An unavailable battery value is shown as
battery invalid; external power does not by itself mean charging. - The session status shows connection diagnosis, bound receive/send ports, queued/lost notifications and parse errors. When the glove disconnects and data stops, it shows
Stale; after the glove reconnects, it returns toReceivingwithout any action.
Missing or stale input clears the sample's bars, axis markers and skeleton display. This prevents old values from looking current. The sample's FreshnessSeconds=0.5 is an adjustable demonstration policy, not a measured device timing guarantee.
Skeleton color indicates the tracking state of the selected stream.
| Skeleton display | Meaning |
|---|---|
| Gray | Tracked or NotAttempted |
| Gold | IkFailed: IK failed; diagnostic display only, not for control, interpolation or velocity estimation |
| Hidden | Data is missing or stale, or no glove is selected |
Detailed states and generation timestamps remain available through the API's Tracking and GeneratedAt properties.
Render pipelines and materials
Basic Sample's skeleton display supports Built-in, URP and HDRP. Built-in uses the supplied materials; URP/HDRP automatically use matching Lit materials. The gray/gold color settings are preserved, although lighting and exposure affect their appearance. The supplied material assets and project rendering settings are unchanged. Install and configure URP/HDRP in your project when using those pipelines.
To change the display colors, set TrackedMaterial and DiagnosticMaterial on each hand's SkeletonRenderer. Built-in Standard materials are adapted automatically without manual conversion. For custom shaders, assign materials compatible with your project's render pipeline. For the official HandVisualizer's VR configuration, see Setup.
Select a glove for each side
Each dropdown has an Auto (one glove) option. Auto resolves only when exactly one connected glove of that side is present on the selected host. With two left gloves, for example, select the intended left ID explicitly.
If an explicitly selected ID disappears, the sample keeps that selection and marks it unavailable. It does not silently switch to another glove. Selection resumes when the same ID supplies current data again. PairId is metadata; API access uses the individual glove's ID.
Send body vibration
- Press F for the selected left glove or J for the selected right glove.
- Alternatively, click the corresponding vibration button.
- After the sample establishes an initial controller baseline, a new A-button press on a selected glove also triggers vibration.
The sample submits 160 Hz, amplitude 0.2, duration 0.1 s. The result text confirms local UDP submission or reports an error. It cannot confirm physical actuation. A device must be connected and report vibration capability.
The sample follows the project's enabled input backend. Legacy Input Manager or Both uses the legacy keyboard path. New-input-only uses the installed Input System and its UI module. The SDK does not change Active Input Handling. Give the Game view or Player keyboard focus when testing F/J.
Compare raw and retargeted skeletons
Use the adjoining Raw skeleton / Retargeted skeleton buttons to select the skeleton shown in the hand preview. The selected button is blue. Retargeted is the skeleton transformed by the active DivingStation retarget profile. Raw is the skeleton after tracking and joint calculations, before retargeting. This selection does not switch the Ergonomics angles on the left or MAGNETRA input.
The streams can report different tracking states. IkFailed and NotAttempted values can support diagnosis, but must not drive control, interpolation or velocity estimation. Tracked is the only tracking state valid for those uses, and applications must also enforce freshness.
Retarget-profile information is fetched automatically as soon as skeleton data arrives after connection, then refreshed about every ten seconds. A profile change triggers an immediate request. A shared left/right profile needs one request; different profiles show separate left/right names. The display indicates waiting or loading until a response provides the name. If notifications are lost right after startup, loading may be shown until the next refresh. Names and descriptions are optional metadata; rendering does not wait for the response. A timeout is distinct from a server response saying the profile was not found.
Stop and restart
Stop / start disposes and recreates its client. Stopping clears old displayed data and closes the UDP sockets. A new session needs fresh data and may need DivingStation approval again. Use API Resume(hostId) or matching Connect(options) retries when you want to retain an existing session while waiting for approval or data.
Preview with simulated data
Without a glove, start the Unity Editor or a Player with the command-line argument -contactglove-mock to view the sample with developer-only simulated data. SIMULATED DATA identifies it. It does not connect to a physical glove, so it does not demonstrate physical tracking or vibration.
SDK skeletons and XR Hands are different data paths
Basic Sample and the official HandVisualizer obtain hand data through different paths.
| Item | SDK Basic Sample | Official HandVisualizer |
|---|---|---|
| Data source | DivingStation Protocol v2 | The OpenXR runtime's XRHandSubsystem |
| Joint positions | 25 positions in meters, relative to the wrist | 26 joints including Wrist and Palm |
| Rotations | 19 rotations relative to parent joints | Each joint's pose |
| Display placement | A hand-display root placed by the application | The runtime's tracking origin and XR Origin |
When displaying SDK values yourself, follow coordinate conversion and display placement. XR Hands poses are already in Unity coordinates, so do not apply UnityPoseConverter to them. Account for XR Origin when placing them in Unity world space. See Unity's hand data model.
Use each API's joint mapping for the SDK's 25 joints and XR Hands' 26 joints; do not exchange the arrays directly. Check SDK raw/retargeted data, controller input and body vibration in Basic Sample, and VR hand rendering in HandVisualizer.
Access the API from a scene
ContactGloveBehaviour owns a ContactGloveClient, pumps notifications in Update and stops it when its owner is disabled or destroyed. Assign the existing component in your script's Inspector and read its Client property after startup:
using MeltInterfaceTechnologies.ContactGlove.V2;
using MeltInterfaceTechnologies.ContactGlove.V2.Unity;
using UnityEngine;
public sealed class ReadGlove : MonoBehaviour
{
public ContactGloveBehaviour Connection;
public string HostId = "localhost";
public string GloveId;
private void Update()
{
var client = Connection != null ? Connection.Client : null;
if (client == null || string.IsNullOrEmpty(GloveId)) return;
var key = new DeviceKey(HostId, GloveId);
if (!client.TryGetDevice(key, out var device) ||
device.Connection != ConnectionState.Connected) return;
if (client.TryGetErgonomics(key, out var angles) &&
angles.Tracking == TrackingState.Tracked &&
angles.AgeSeconds <= 0.5)
{
float indexDipDegrees = angles.Angles[6];
// Consume a fresh, tracked value in your application.
}
}
}Set GloveId to an ID returned by GetDevices(HostId) or shown in the sample. This example has no automatic first-device selection. The 0.5 threshold is an example policy; choose an appropriate value for your application.
Do not call PumpNotifications a second time from this script: ContactGloveBehaviour already owns dispatch. If its client is stopped and recreated, subscribe to the new client and dispose old subscription tokens.
Multiple devices and hosts
DeviceKey combines HostId and GloveId. Two hosts can report the same glove ID without colliding. Add distinct HostOptions entries to ContactGloveBehaviour.Hosts and use the corresponding host ID for all reads and commands.
GetDevices(hostId) enumerates Device metadata received from the specified host. Check the metadata's Connection and each stream's availability, age and tracking state separately.
Change UDP ports
Edit the host's ServerPort, ReceivePort and SendPort before starting the session. ServerPort belongs to DivingStation; the other two are local SDK ports. A 0 local port is allocated once and retained for the life of the session.
Changing an Inspector value does not reconfigure a running session. An explicit stop/start applies new settings. To retry without changing the endpoint, call Resume or Connect with the original host ID, IP and configured ports. Passing the allocated numeric port instead of the original configured 0 is a different setting and is rejected.
Read controller input
if (client.TryGetController(key, out var input) && input.AgeSeconds <= 0.5)
{
ButtonState a = input.A;
float joystickX = input.JoystickX;
float gripForce = input.GripForce;
}Button states are Unpressed, Touching and Pressed. For one-shot actions, compare a current state with a previously observed state. Treat the first snapshot as a baseline rather than a new press. Clear latched actions after a disconnect, stale input or notification loss; lost UDP edges cannot be reconstructed.
Submit a vibration command
OperationResult result = client.Vibrate(key, 160f, 0.2f, 0.1f);
if (!result.Succeeded)
Debug.LogWarning($"Vibration rejected: {result.Code} {result.Detail}");The parameter order is frequency, amplitude, duration. Accepted ranges are 100–500 Hz, 0–1, and 0.001–1 s, including endpoints. Out-of-range values are rejected rather than clamped. Commands are not replayed after reconnect.
Read a skeleton and profile
if (client.TryGetSkeleton(key, out var skeleton))
{
string profileUuid = skeleton.ProfileUuid;
Vector3 indexTip = UnityPoseConverter.Position(skeleton.Positions[9]);
// indexTip is relative to the wrist, in meters, in Unity coordinates.
// Check Tracking and AgeSeconds before using it for control.
}Use TryGetRawSkeleton for the pre-retarget source skeleton and TryGetRawSensorTransforms for the five estimated sensor-center poses. Place the hand-display root in your application. To use SDK skeletons in VR, your application must obtain a root pose from external tracking and account for mounting offsets and coordinate spaces.
For profile names, subscribe to SubscribeProfileResponse and SubscribeProfileTimeout, then call RequestRetargetProfileInfo(hostId, profileUuid, out requestId). Keep the returned subscription tokens while you need them and dispose them afterward. The API reference describes correlation and status values.
Minimal standalone client component
Use this alternative when your own component should own the session. Save it as ContactGloveQuickStart.cs, add it to a GameObject, and set an explicit GloveId after discovering available IDs. Do not also create a ContactGloveBehaviour for this same client.
using System;
using MeltInterfaceTechnologies.ContactGlove.V2;
using UnityEngine;
public sealed class ContactGloveQuickStart : MonoBehaviour
{
public string IpAddress = "127.0.0.1";
public string GloveId = "";
private const string HostId = "local";
private ContactGloveClient client;
private IDisposable deviceSubscription;
private double nextLogAt;
private void OnEnable()
{
client = new ContactGloveClient();
var result = client.Connect(new HostOptions
{
HostId = HostId,
IpAddress = IpAddress,
ServerPort = 25791,
ReceivePort = 0,
SendPort = 0
});
if (!result.Succeeded)
{
Debug.LogError($"ContactGlove: {result.Code} {result.Detail}");
StopClient();
return;
}
deviceSubscription = client.SubscribeDevice(HostId, device =>
Debug.Log($"Glove {device.Key.GloveId}: {device.Connection}"));
}
private void Update()
{
if (client == null) return;
client.PumpNotifications();
if (string.IsNullOrEmpty(GloveId) || MonotonicTime.Now < nextLogAt)
return;
nextLogAt = MonotonicTime.Now + 1.0;
var key = new DeviceKey(HostId, GloveId);
if (client.TryGetDevice(key, out var device) &&
device.Connection == ConnectionState.Connected &&
client.TryGetErgonomics(key, out var angles) &&
angles.Tracking == TrackingState.Tracked &&
angles.AgeSeconds <= 0.5)
{
Debug.Log($"Index DIP: {angles.Angles[6]} degrees");
}
}
// Call deliberately from your UI, rather than on every Update.
public void VibrateSelected()
{
if (client == null || string.IsNullOrEmpty(GloveId)) return;
var result = client.Vibrate(
new DeviceKey(HostId, GloveId), 160f, 0.2f, 0.1f);
Debug.Log($"Vibration submission: {result.Code}");
}
public void RetryRegistration()
{
if (client == null) return;
var result = client.Resume(HostId);
Debug.Log($"Registration retry: {result.Code} {result.Detail}");
}
private void OnDisable() => StopClient();
private void OnDestroy() => StopClient();
private void OnApplicationQuit() => StopClient();
private void StopClient()
{
deviceSubscription?.Dispose();
deviceSubscription = null;
client?.Dispose();
client = null;
}
}Connect starts local communication; receipt is asynchronous. Device IDs appear in the Console only when Device records arrive. The example reports one joint at most once per second and does not automatically actuate hardware.