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 fieldDefaultConstraint or meaning
HostIdlocalhostNonempty, non-whitespace application-local session key
DisplayNameDivingStationHost label
IpAddress127.0.0.1Numeric IP accepted by IPAddress.TryParse; DNS names are not resolved
ServerPort257911–65535
ReceivePort257880–65535; 0 allocates a local port
SendPort00–65535; 0 allocates a local port
NotificationCapacity2568–65536 queued notifications per host
StaleAfterSeconds0Finite nonnegative seconds; 0 disables session-level stale diagnosis
ProfileTimeoutSeconds5Finite 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 methodPurpose
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.

GetterSnapshotContents
TryGetDevice(key, out value)DeviceSnapshotDevice metadata
TryGetController(key, out value)ControllerSnapshotButton and analog input
TryGetErgonomics(key, out value)ErgonomicsSnapshotTwenty angles and tracking state
TryGetSkeleton(key, out value)SkeletonSnapshotRetargeted hand skeleton and profile UUID
TryGetRawSkeleton(key, out value)RawSkeletonSnapshotSource hand skeleton before retargeting
TryGetRawSensorTransforms(key, out value)RawSensorSnapshotFive 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

PropertiesValues
A, B, System, TriggerButton, JoystickButton, TrackpadButtonButtonState.Unpressed=0, Touching=1, Pressed=2
Trigger, GripValue, GripForce0–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.

IndicesFingerOrder
0–3ThumbCMC, MCP, IP, tilt
4–7IndexMCP, PIP, DIP, tilt
8–11MiddleMCP, PIP, DIP, tilt
12–15RingMCP, PIP, DIP, tilt
16–19LittleMCP, 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:

IndicesNodes in order
0wrist
1–4thumb CMC, MCP, IP, tip
5–9index CMC, MCP, PIP, DIP, tip
10–14middle CMC, MCP, PIP, DIP, tip
15–19ring CMC, MCP, PIP, DIP, tip
20–24little 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 stateAllowed use
Tracked=0Current IK result; check freshness before control
IkFailed=1Display/diagnosis only
NotAttempted=2Display/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 axisDirection
+XFrom the fingertips toward the wrist; a straight middle finger extends approximately along -X
+YFrom the palm toward the back of the hand
ZAcross 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.

ProfileStatusMeaning
Success=0Profile exists; display name and description are available
NotFound=1Well-formed UUID is absent
InvalidRequest=2Server 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

  1. Connect creates and binds one receive socket and one retained send socket, then submits registration. Initial success means local startup/submission, not server acknowledgement.
  2. The retained sender handles registration, the 30-second monotonic heartbeat, profile requests, vibration and disconnect. Timing is independent of Unity timeScale.
  3. Matching Connect or Resume retries 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.
  4. A matching retry requires the same host ID, parsed IP, server port and configured local ports. A configured 0 must remain 0. Conflicting settings return InvalidArgument and leave the existing session intact. Other option values remain those copied at initial creation.
  5. Missing/stale data and profile timeouts do not automatically destroy the session. Registration retry does not prove approval or repair a failed receive worker.
  6. Disconnect, Dispose or owner Disable ends the session, invalidates its subscriptions/data/queued notifications and releases its sockets. A newly created session requires fresh subscriptions. With port 0, 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:

ResultCodeMeaning
SuccessLocal operation succeeded
InvalidArgumentInvalid values or conflicting existing-session settings
UnknownHostNo active session for that host ID
UnknownDeviceRequired device metadata is absent
DataUnavailableRequested stream has not arrived
NotConnectedDevice is not connected
UnsupportedDevice does not support the requested function
TransportUnavailableLocal transport could not perform the operation, or the pending profile-request limit was reached
DisposedClient 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.