API reference
Core types are in MeltInterfaceTechnologies.ContactGlove.V2. Unity adapters are in .Unity. The API operates on immutable received snapshots and explicit host/glove keys.
Client and connection options
var client = new ContactGloveClient();
OperationResult result = client.Connect(new HostOptions
{
HostId = "localhost",
IpAddress = "127.0.0.1",
ServerPort = 25791,
ReceivePort = 0,
SendPort = 0
});HostOptions field | Default | Constraint or meaning |
|---|---|---|
HostId | localhost | Nonempty, non-whitespace application-local session key |
DisplayName | DivingStation | Host label |
IpAddress | 127.0.0.1 | Numeric IP accepted by IPAddress.TryParse; DNS names are not resolved |
ServerPort | 25791 | 1–65535 |
ReceivePort | 25788 | 0–65535; 0 allocates a local port |
SendPort | 0 | 0–65535; 0 allocates a local port |
NotificationCapacity | 256 | 8–65536 queued notifications per host |
StaleAfterSeconds | 0 | Finite nonnegative seconds; 0 disables session-level stale diagnosis |
ProfileTimeoutSeconds | 5 | Finite positive local request timeout |
Options are copied at session creation. Use GetDiagnostics(hostId) to inspect the actual bound ports. Explicit ports are bound exclusively; conflicts return TransportUnavailable.
| Client method | Purpose |
|---|---|
Connect(HostOptions) | Start a session or retry registration on an existing matching session |
Resume(string hostId) | Retry registration using the retained session |
Disconnect(string hostId) | End one session and invalidate its data and subscriptions |
GetDiagnostics(string hostId) | Return session diagnostics, or null if absent |
GetDevices(string hostId) | Return Device metadata snapshots; empty for an absent host |
PumpNotifications(int maximumPerHost = 256) | Dispatch queued callbacks on the owner thread |
Dispose() | End all sessions; safe to call repeatedly |
Device identity and availability
var key = new DeviceKey("localhost", "the-glove-id");HostId and GloveId together identify a device. Polling returns false and a null snapshot if data is unavailable. It never supplies a fabricated zero frame.
| Getter | Snapshot | Contents |
|---|---|---|
TryGetDevice(key, out value) | DeviceSnapshot | Device metadata |
TryGetController(key, out value) | ControllerSnapshot | Button and analog input |
TryGetErgonomics(key, out value) | ErgonomicsSnapshot | Twenty angles and tracking state |
TryGetSkeleton(key, out value) | SkeletonSnapshot | Retargeted hand skeleton and profile UUID |
TryGetRawSkeleton(key, out value) | RawSkeletonSnapshot | Source hand skeleton before retargeting |
TryGetRawSensorTransforms(key, out value) | RawSensorSnapshot | Five sensor-center poses |
GetDataStatus(key, stream) distinguishes Success, UnknownHost, UnknownDevice, DataUnavailable and InvalidArgument. It applies to the six cached streams Device through RawSensorTransforms; ProfileResponse is not a pollable stream.
Device metadata and individual streams can arrive independently. A metadata record alone does not prove current tracking. Unknown and Pairing metadata can remain cached. An Unconnected record removes the device's cached streams and notifies Device subscribers. Up to 16 IDs are retained per host per stream; diagnostics count capacity drops.
DeviceSnapshot
Properties include Key, IsLeft, PairId, Name, Family, Color, Connection, BatteryPercentage, ExternalPowerPresent and SupportsHaptics.
PairId is the counterpart glove's device ID, or an empty string if there is no counterpart. Family identifies the device type, and Color is its display-color setting, of type DeviceColor.
BatteryPercentage is 0–100 or -1 for an invalid/unavailable estimate. ExternalPowerPresent reports detected external power, not a charging guarantee. SupportsHaptics refers to body-vibration capability in this API.
ConnectionState: Unknown=0 (unknown), Connected=1 (connected), Unconnected=2 (disconnected), Pairing=3 (connection in progress). The device type is DeviceFamily.ContactGlove3=3; the current Unity product target is ContactGlove3 Pro.
ConnectionState describes the glove's connection. Use SessionState to check communication between the SDK and DivingStation.
ControllerSnapshot
| Properties | Values |
|---|---|
A, B, System, TriggerButton, JoystickButton, TrackpadButton | ButtonState.Unpressed=0, Touching=1, Pressed=2 |
Trigger, GripValue, GripForce | 0–1 |
JoystickX, JoystickY, TrackpadX, TrackpadY | -1–1 |
Values have DivingStation's calibration/deadzone processing applied. GripForce is normalized input, not newtons. A callback reports an accepted input record; it is not a guaranteed button-edge event. Establish a baseline and clear latched input after interruption or notification loss.
TriggerButton is the three-state button input; Trigger is the analog trigger value. GripValue and GripForce are separate analog inputs. The current ContactGlove3 trackpad output is discrete: X is 0, and Y is -1, 0 or 1.
ErgonomicsSnapshot
Angles contains 20 values in degrees. Tracking describes the IK (inverse kinematics) calculation used to obtain the skeleton before retargeting.
| Indices | Finger | Order |
|---|---|---|
0–3 | Thumb | CMC, MCP, IP, tilt |
4–7 | Index | MCP, PIP, DIP, tilt |
8–11 | Middle | MCP, PIP, DIP, tilt |
12–15 | Ring | MCP, PIP, DIP, tilt |
16–19 | Little | MCP, PIP, DIP, tilt |
For example, Angles[6] is index-finger DIP flexion in degrees.
The CMC/MCP/IP/PIP/DIP values are flexion angles measured from the extended position. Each tilt is a tilt angle with the default pose as 0°.
SkeletonSnapshot and RawSkeletonSnapshot
Both inherit HandSnapshot and contain Positions (25), Rotations (19), Tracking and GeneratedAt. SkeletonSnapshot additionally contains ProfileUuid.
The position indices are:
| Indices | Nodes in order |
|---|---|
0 | wrist |
1–4 | thumb CMC, MCP, IP, tip |
5–9 | index CMC, MCP, PIP, DIP, tip |
10–14 | middle CMC, MCP, PIP, DIP, tip |
15–19 | ring CMC, MCP, PIP, DIP, tip |
20–24 | little CMC, MCP, PIP, DIP, tip |
The rotations correspond to position-node indices 1,2,3,5,6,7,8,10,11,12,13,15,16,17,18,20,21,22,23. HandTopology.Parents and HandTopology.RotationNodes provide the fixed mapping.
Raw Skeleton is the skeleton after tracking and joint calculations, before retargeting. The raw and retargeted stream tracking states may differ.
RawSensorSnapshot
Poses contains five WirePose values, ordered thumb, index, middle, ring, little. Each is an estimated sensor-center position and orientation relative to the wrist. Use GeneratedAt for generation time and AgeSeconds for time since reception.
Positions locate the sensor centers; rotations describe each sensor's orientation within the hand model. To read positions and rotations for individual joints, use SkeletonSnapshot or RawSkeletonSnapshot. RawSensorSnapshot has no Tracking property.
Time, freshness and tracking
ReceivedAt is the reception time in seconds from a local monotonic clock; AgeSeconds is the elapsed time since reception, in seconds. Skeleton, angle and sensor data also expose GeneratedAt, the generation time in Unix milliseconds. DeviceSnapshot and ControllerSnapshot have no generation timestamp.
The cache updates in arrival order. Streams arrive independently, so check their timestamps and freshness when combining them.
| Tracking state | Allowed use |
|---|---|
Tracked=0 | Current IK result; check freshness before control |
IkFailed=1 | Display/diagnosis only |
NotAttempted=2 | Display/diagnosis only |
NotAttempted means IK was not performed. It includes both cases where preceding processing failed or input was missing and cases where pose calculation was deemed unnecessary, such as update-interval control. This state does not itself indicate an IK failure.
Do not use IkFailed or NotAttempted for control, velocity estimation or interpolation. A Receiving session can still contain failed tracking, and a Connected glove can still have missing or stale streams. Session-level stale diagnosis is separate from the device's reported connection state.
Coordinates and Unity visualization
Positions and Rotations contain the coordinates and rotations received from DivingStation. Positions are in meters within a hand model whose origin is the wrist. Both hands use the same axis convention.
SkeletonSnapshot uses the retarget profile's hand model; RawSkeletonSnapshot uses the hand model before retargeting. Both have their origin at the wrist, but their coordinates are not interchangeable because the models differ. Sensor positions and orientations use the same hand-model frame as the retargeted skeleton.
| Received-coordinate axis | Direction |
|---|---|
+X | From the fingertips toward the wrist; a straight middle finger extends approximately along -X |
+Y | From the palm toward the back of the hand |
Z | Across the hand width |
For your own visualization, use UnityPoseConverter to convert received values to Unity coordinates. For positions, it keeps X and Y and reverses the sign of Z. Lengths remain in meters.
Vector3 position = UnityPoseConverter.Position(wirePosition);
Quaternion rotation = UnityPoseConverter.Rotation(wireRotation);Positions map to (x, y, -z). Received quaternion components are ordered (w,x,y,z) and map to Unity's constructor as Quaternion(-x,-y,z,w). In this example, wirePosition and wireRotation are variable names for the received values being converted.
A converted position is relative to the hand-display root. If all 25 joints are children of that same root, use it as each joint's localPosition. If you create a bone hierarchy instead, first calculate positions relative to the parent joint. For world coordinates, use displayRoot.TransformPoint(position), where displayRoot is the display root's Transform. Place that root in your application and keep its Scale and its ancestors' Scale at (1,1,1) to preserve lengths in meters.
The 19 rotations are relative to their parent joints. UnityPoseConverter.ModelRotations(snapshot, destination) combines them from parent to child to calculate 25 orientations relative to the hand-display root. Pass a Quaternion array with at least 25 elements as destination. The root uses Quaternion.identity; each fingertip inherits its parent's orientation.
For the standard display, pass the received snapshot directly to SkeletonRenderer.Apply(snapshot). It converts positions internally and displays 25 spheres and 24 cylinders. Converting positions before this call would apply the conversion twice, so pass the snapshot unchanged. Apply(null) hides the display. Call it on Unity's main thread and check Tracking and AgeSeconds in the calling code.
Do not apply the same conversion again to positions or rotations you have already converted for your own display. XR Hands poses are already in Unity coordinates, so use them without UnityPoseConverter.
Body vibration
OperationResult result = client.Vibrate(key, frequency, amplitude, duration);Parameters are frequency in Hz (100–500), amplitude (0–1) and duration in seconds (0.001–1). Endpoints are allowed. Nonfinite and out-of-range values are rejected rather than clamped.
The device must have metadata, be connected and report SupportsHaptics. A valid cached device ID can still be invalid for a vibration address: empty IDs, control characters, spaces, /, #, or OSC pattern characters *?[]{}, are rejected as InvalidArgument before transmission.
Success means local UDP submission. There is no device-actuation acknowledgement, and commands are never replayed after reconnect.
Retarget-profile display information
IDisposable responseToken = client.SubscribeProfileResponse(hostId, response =>
Debug.Log($"{response.RequestId}: {response.Status} {response.Name}"));
IDisposable timeoutToken = client.SubscribeProfileTimeout(hostId, timeout =>
Debug.LogWarning($"Profile request {timeout.RequestId} timed out"));
OperationResult result = client.RequestRetargetProfileInfo(
hostId, profileUuid, out int requestId);ProfileResponseSnapshot supplies HostId, RequestId, ProfileUuid, Status, Name, Description and ReceivedAt.
ProfileStatus | Meaning |
|---|---|
Success=0 | Profile exists; display name and description are available |
NotFound=1 | Well-formed UUID is absent |
InvalidRequest=2 | Server reports an invalid request |
Local invalid arguments and local timeouts are separate from server statuses. Requests are correlated by the SDK; unmatched responses are counted in diagnostics. There may be up to 128 pending requests per host; beyond that, the request returns TransportUnavailable. The default local timeout is five seconds. A profile-information timeout does not stop skeleton delivery or decoding.
Dispose both tokens when you no longer need notifications. Subscriptions must be installed after the host is started.
Notifications and ownership
The following subscription methods return IDisposable tokens: SubscribeDevice, SubscribeController, SubscribeErgonomics, SubscribeSkeleton, SubscribeRawSkeleton, SubscribeRawSensorTransforms, SubscribeProfileResponse, SubscribeProfileTimeout and SubscribeNotificationLoss.
Receive workers decode and cache data, then enqueue notifications. Subscribers run only when the owner calls PumpNotifications, normally in Unity Update. Use one owner thread; recursive pumping and pumping from a different thread throw InvalidOperationException. ContactGloveBehaviour already pumps for its client.
Snapshots own private arrays exposed through read-only FixedValues<T>. You may retain an old snapshot across later updates or copy values with CopyTo. Parsing allocates objects and arrays.
On queue overflow, the SDK discards all queued notifications, reports notification loss and clears cached controller state. If a discarded notification was a profile response, that request receives neither a response nor a timeout. Release latched application input and resynchronize from new snapshots. A new baseline must not manufacture an input edge. Callback exceptions are isolated and counted. Disposing a subscription or client inside a callback is supported.
Connection lifecycle
Connectcreates and binds one receive socket and one retained send socket, then submits registration. Initial success means local startup/submission, not server acknowledgement.- The retained sender handles registration, the 30-second monotonic heartbeat, profile requests, vibration and disconnect. Timing is independent of Unity
timeScale. - Matching
ConnectorResumeretries registration without replacing sockets, cached snapshots, subscriptions or pending profile requests. Registration is limited to at least one second between sends; a rate-limited retry can return success without sending a packet. - A matching retry requires the same host ID, parsed IP, server port and configured local ports. A configured
0must remain0. Conflicting settings returnInvalidArgumentand leave the existing session intact. Other option values remain those copied at initial creation. - Missing/stale data and profile timeouts do not automatically destroy the session. Registration retry does not prove approval or repair a failed receive worker.
Disconnect,Disposeor owner Disable ends the session, invalidates its subscriptions/data/queued notifications and releases its sockets. A newly created session requires fresh subscriptions. With port0, the new source port can differ.
DivingStation associates approval with the active source endpoint and receive port. Explicit disconnection or 90 seconds without a Connect expires the server session; fixed ports do not preserve approval for a later new session. A lost Disconnect may leave the server session until expiry.
ContactGloveBehaviour stops on Disable, Destroy, application quit, Editor assembly reload and Play exit. Runtime hooks also handle entering Play with domain and scene reload disabled. Its StartClient is idempotent; calling it while a client exists does not issue another Connect. Use Client.Resume for an existing session's registration retry.
Results and diagnostics
OperationResult contains Code, Detail and Succeeded. Handle failures explicitly:
ResultCode | Meaning |
|---|---|
Success | Local operation succeeded |
InvalidArgument | Invalid values or conflicting existing-session settings |
UnknownHost | No active session for that host ID |
UnknownDevice | Required device metadata is absent |
DataUnavailable | Requested stream has not arrived |
NotConnected | Device is not connected |
Unsupported | Device does not support the requested function |
TransportUnavailable | Local transport could not perform the operation, or the pending profile-request limit was reached |
Disposed | Client has already been disposed |
SessionDiagnostics.State is one of Stopped, Starting, WaitingForData, Receiving, Stale and Faulted. WaitingForData does not distinguish approval waiting, server absence or routing failure. After Disconnect, GetDiagnostics returns null for that host rather than Stopped.
Diagnostics include actual ports, queue depth, received datagrams, accepted records, parse errors, cache-limit drops, notification drops, callback exceptions, unexpected sources, unmatched profile responses, registration sends, last receive time, last error and input-resynchronization status. Use these with Troubleshooting.