This reference describes the topics, message types, node parameters, and coordinate-system conventions provided by the contact_glove and contact_glove_msgs packages.
The installed ROS 2 message definitions are the authoritative source for fields, constants, types, and comments. Inspect them with:
ros2 interface show contact_glove_msgs/msg/GloveDataros2 interface show contact_glove_msgs/msg/Ergonomicsros2 interface show contact_glove_msgs/msg/Skeletonros2 interface show contact_glove_msgs/msg/RawSkeletonros2 interface show contact_glove_msgs/msg/RawSensorTransformsros2 interface show contact_glove_msgs/msg/Controllerros2 interface show contact_glove_msgs/msg/HapticsBodyros2 interface show contact_glove_msgs/srv/GetRetargetProfile
This document explains interface-wide conventions that are not evident from an individual msg file, including topic mappings, QoS, coordinate systems, and multiple-glove selection rules.
Every topic identifies a glove through the message id field and its hand through is_left. All streaming, control, and visualization topics also have a <hand> subtopic (left or right), allowing users to naturally subscribe to or remap one hand. Topic names are simple node-relative names (left/... and right/...) without the package name.
contact_glove_node starts in the contact_glove namespace by default, so its effective names are /contact_glove/left/... and /contact_glove/right/.... Override this with namespace:=.... To operate multiple DivingStation instances or glove pairs on one computer, give each node a distinct namespace or use <group><push-ros-namespace> in a launch file. Per-topic remapping is unnecessary.
A distinct namespace alone is insufficient. receive_port is the OSC receive port used by that node on the local machine (25788 by default). If two nodes share it, the second and later nodes fail at ConnectToHost. Assign each node a unique receive_port.
Ergonomics data converted for URDF integration; see JointState + URDF below
glove_data contains state that changes infrequently, such as battery percentage and connection state, so it uses transient_local (latched) durability. A topic for one side can multiplex multiple gloves through id. Latching with depth 1 would retain only the glove most recently published and a late subscriber would miss other gloves.
To avoid that, contact_glove_node retains the latest state for every known glove. Whenever any glove updates, the node republishes all known current glove states to left/glove_data or right/glove_data according to is_left. The transient-local cache then contains one current sample per glove. Its depth is therefore the maximum number of gloves the node can track simultaneously (16 by default). Increase the depth if more gloves must run at once.
Important: A subscriber must also request transient_local durability to receive the glove_data latched cache. A volatile subscriber can connect to a transient_local publisher but receives no cached samples at connection time—only messages published afterward. Thus a default create_subscription(topic, 10, callback), or ros2 topic echo without a durability option, receives no current glove state until a glove sends new glove_data.
In C++, use the same QoS as the publisher:
auto sub = create_subscription<contact_glove_msgs::msg::GloveData>( "left/glove_data", rclcpp::QoS(16).transient_local(), callback);
ergonomics, skeleton, raw_skeleton, raw_sensor_transforms, and controller are high-rate streams. They use rclcpp::SensorDataQoS() (best effort, volatile, equivalent to depth 5), which prioritizes low latency and non-blocking publication over preventing dropped samples. Subscribers must use that QoS or compatible best effort. A reliable subscriber cannot receive this data because the QoS is incompatible.
Vibration command; publishing it triggers vibration
haptics_body keeps the default reliable/volatile QoS to avoid losing vibration commands. Both topics use msg.id to choose the target glove. When multiple gloves are assigned to one side, set id explicitly.
Queries display metadata for a profile UUID from the currently connected DivingStation
The service is node-relative. Its fully qualified default name is /contact_glove/get_retarget_profile. Pass only profile_uuid in the request. The ROS 2 SDK assigns a positive request ID internally, sends it to the C++ SDK, and waits only for its matching asynchronous response, so callers do not manage request IDs. It does not query or cache profile UUIDs automatically while Skeleton messages arrive.
Value
Constant
Description
0
STATUS_FOUND
The profile exists. profile_uuid, name, and description are valid.
1
STATUS_NOT_FOUND
The UUID format is valid but DivingStation has no matching profile. name and description are empty.
2
STATUS_INVALID_QUERY
DivingStation rejected the query as invalid. The profile UUID and similar values are empty.
3
STATUS_TIMEOUT
No DivingStation response was received within retarget_profile_timeout_ms (ROS 2 bridge-specific).
4
STATUS_SDK_REQUEST_REJECTED
The C++ SDK did not send the request, for example because the UUID format was invalid (ROS 2 bridge-specific).
Late responses that arrive after STATUS_TIMEOUT are discarded. Use name and description only for STATUS_FOUND.
Joint spheres (SPHERE_LIST) and bones (LINE_LIST), in the message header.frame_id frame
/tf
TF
-
With publish_placeholder_wrist_tf:=true, broadcasts placeholder TF from parent_frame_id to header.frame_id. With publish_joint_tf:=true, broadcasts TF for the 24 joints other than the wrist. It is always the absolute /tf path regardless of namespace.
contact_glove_rviz_node subscribes to left/skeleton and right/skeleton with rclcpp::SensorDataQoS(), then publishes the corresponding MarkerArrays. Its subscriber QoS must match contact_glove_node's publisher QoS.
ds_port (25791 by default) is the OSC receive port on the DivingStation side to which the SDK sends data. Check or change it from DivingStation settings:
On the DivingStation home screen, click the gear icon in the upper right and open Settings.
Select DivingStationProtocol from the left menu.
The number for DivingStationProtocol v2 port is the SDK connection destination port.
If DivingStation uses a v2 port other than 25791, set contact_glove_node ds_port to the same value. Otherwise OSC packets do not reach DivingStation and data cannot be exchanged.
receive_port is the local UDP port for callbacks from DivingStation. Give every SDK node on one PC a distinct value. When connecting to DivingStation on another PC, permit the actual receive_port through the local PC firewall.
send_port is the source port of UDP packets the SDK sends to DivingStation. With send_port:=0, the OS selects an available port. Specify it only where a fixed source port is required.
DivingStation identifies an active session by the source UDP endpoint and receive_port. Explicit Disconnect, or 90 seconds after the final Connect, expires a session. Fixed ports therefore do not preserve approval for a later connection.
# To fix both the source port and callback receive portros2 launch contact_glove contact_glove.launch.xml \ receive_port:=25788 \ send_port:=25789
send_port and receive_port may differ. Multiple SDK nodes on one PC need distinct receive_port values.
Skeleton.msg is a ROS 2 representation of SDK SkeletonData::transforms: 25 elements in SkeletonDataType order.
geometry_msgs/Transform[25] joints contains position and rotation (translation and rotation) for all 25 joints. Units, coordinate system, and rotation rules are described below.
NODE_WRIST through NODE_LITTLE_TIP (uint8, 0 through 24) are index constants for joints. Their values and order exactly match SDK SkeletonDataType.
Every finger's CMC joint has NODE_WRIST as its parent. Every other joint is parented to the preceding joint in that finger. The kBonePairs table in contact_glove_rviz_node.cpp uses the same constants to render bones.
RawSkeleton.msg exposes SDK RawSkeletonData::transforms with the same 25-joint order and NODE_* constants as Skeleton.msg. It has no profile_uuid.
Positions are in the pre-retarget source skeleton model-rooted space. header.frame_id is ds2/<namespace>/<glove_id>/source_wrist.
rotation is local relative to the parent joint, as in Skeleton, but only 19 joint rotations arrive over the wire. The C++ SDK initializes the six wrist and fingertip rotations to identity quaternions; do not interpret them as measured or estimated values.
RawSkeleton.tracking_state and Ergonomics.tracking_state report the IK that obtains the source skeleton. They can differ from post-retarget Skeleton.tracking_state at the same time. For example, when only retargeting fails, RawSkeleton and Ergonomics can be TRACKED while Skeleton is IK_FAILED.
contact_glove_rviz_node visualizes and publishes TF only for post-retarget Skeleton. It does not publish a MarkerArray or TF for RawSkeleton; consumers that visualize source coordinates must handle it explicitly.
Skeleton, RawSkeleton, and RawSensorTransforms coordinates use the SDK's model-rooted space: right-handed and measured in meters. Skeleton uses the retarget profile hand-model coordinate system; RawSkeleton uses the source-skeleton hand-model coordinate system. Their header.frame_id values therefore differ.
translation is an absolute position in model-rooted space relative to each message's wrist. It is not relative to a preceding joint or sensor. rotation conventions differ by message.
Axis
Direction
+X
Fingertip toward wrist
+Y
Palm toward back of hand
+Z
Little-finger side toward thumb side on the right hand; thumb side toward little-finger side on the left hand
+Y (back of hand) ^ | wrist | o-----+-----> +X (toward wrist) / / v +Z (right: little to thumb / left: thumb to little)
Reversing +Z between hands keeps each local hand coordinate system right-handed (+X x +Y = +Z). Coordinate-transform libraries such as tf2 can therefore assume a right-handed system.
Units: translation (x, y, and z of geometry_msgs/Vector3) is in meters.
rotation, common: A normalized quaternion in standard geometry_msgs/Quaternion field order: x, y, z, w.
rotation in Skeleton: A local quaternion relative to the parent joint, not an absolute orientation in model-rooted space. See Skeleton.msg NODE_* comments for the parent-child topology. The wire format sends 19 local rotations, including every CMC joint. The SDK fills NODE_WRIST and the five *_TIP rotations with identity quaternions; those six values are not measured rotations.
rotation in RawSkeleton: A local quaternion relative to the parent joint, like Skeleton. Only 19 joint rotations are transported; the identity quaternions for the wrist and every tip are not valid rotation measurements.
rotation in RawSensorTransforms: An absolute orientation in model-rooted space, like translation. It expresses the orientation from each sensor's local coordinate system to model-rooted space. This differs from Skeleton rotation, which is local relative to the parent joint.
Raw and calibrated data: Skeleton is post-retarget data from DivingStation Core IK processing that uses the profile identified by profile_uuid. The SDK decodes those values; it does not run IK. RawSkeleton is pre-retarget data produced by applying source IK and forward kinematics to calibrated EMF tracking. RawSensorTransforms contains raw transforms for the individual sensors without retargeting. tracking_state has TRACKED, IK_FAILED, and NOT_ATTEMPTED values. IK_FAILED and NOT_ATTEMPTED samples are useful for display and diagnostics, but must not drive control, velocity estimation, or interpolation. RawSkeleton and Skeleton tracking states can differ. NOT_ATTEMPTED means the required IK was not performed. It includes earlier processing failures or missing input and cases where pose calculation was deemed unnecessary, such as update-interval control reusing an existing pose. This state does not itself indicate an IK failure. Only TRACKED values are valid for control, velocity estimation, or interpolation.
The corresponding units, ranges, and coordinate rules are also comments in every msg file. Inspect them with ros2 interface show, for example:
ros2 interface show contact_glove_msgs/msg/Skeleton
Every streaming message (GloveData, Ergonomics, Skeleton, RawSkeleton, RawSensorTransforms, and Controller) begins with std_msgs/Header header. header.stamp is ROS time when the SDK callback was received and converted to a ROS 2 message. header.frame_id is the target glove wrist frame, except RawSkeleton, which uses the source-skeleton wrist.
generated_at_ms is the Unix timestamp in milliseconds when the SDK / DivingStation Protocol v2 generated a frame. It is included by Ergonomics, Skeleton, RawSkeleton, and RawSensorTransforms. GloveData and Controller do not have it because their protocol data has no generation time.
The ROS 2 topic namespace (the namespace:=... launch argument) and TF frame names are different concepts.
Normal topics such as left/skeleton_markers are nested in the node namespace, for example /contact_glove/left/skeleton_markers, and are independent per namespace.
tf2_ros::TransformBroadcaster always publishes to the absolute /tf topic regardless of namespace. Thus /tf is one global topic: TF from every simultaneously running namespace, such as several DivingStation or demo environments, is sent to it.
To avoid TF collisions across multiple namespaces, make frame_id and child_frame_id names unique. Separating topic namespaces is not enough.
Wrist frames normally use:
ds2/<namespace>/<glove_id>/wrist
RawSkeleton uses a distinct name to avoid confusing source-skeleton coordinates with post-retarget Skeleton:
ds2/<namespace>/<glove_id>/source_wrist
The default namespace is contact_glove. The same launch argument is used for both the namespace in header.frame_id and the ROS 2 node namespace.
Physical glove IDs are UUIDs, so collision on glove_id alone is unlikely. Including namespace also makes separate DivingStation and demo environments easier for people to distinguish.
ds2 is a fixed prefix identifying frames from DivingStation Protocol v2 and reducing collision with frames from other ROS 2 packages on /tf.
contact_glove_with_rviz.launch.xml passes the same namespace to the driver and RViz node, so they always refer to the same wrist-frame names.
RViz joint markers and bones are published in the relevant Skeleton.header.frame_id wrist frame.
contact_glove_rviz_node publishes each hand skeleton in that hand's header.frame_id, with the wrist at its origin:
publish_placeholder_wrist_tf:=true (default) publishes a placeholder wrist TF at left_wrist_offset or right_wrist_offset so the hands appear side by side. It is for visualization only, not the actual wrist position.
publish_placeholder_wrist_tf:=false publishes no wrist TF. Another program must publish parent_frame_id to header.frame_id, for example from an external tracker with wrist 6DoF. The skeleton then follows that actual wrist position.
Setting contact_glove_rviz_node publish_joint_tf (default false) to true publishes the 24 Skeleton joints other than the wrist to /tf. It is specifically for direct debugging of skeleton positions and rotations as TF. It is independent of JointState + URDF, which serves robot integration and joint-hierarchy visualization; either feature may be enabled alone.
The wrist remains ds2/<namespace>/<glove_id>/wrist. Its placement follows the publish_placeholder_wrist_tf placeholder or actual wrist TF from an external node. publish_joint_tf does not alter wrist behavior.
Non-wrist joints are direct wrist children named:
ds2/<namespace>/<glove_id>/<finger>/<joint>
Examples are ds2/contact_glove/0123456789ABCDEF/thumb/cmc, ds2/contact_glove/0123456789ABCDEF/index/pip, and ds2/contact_glove/0123456789ABCDEF/little/tip. <finger> is thumb, index, middle, ring, or little. <joint> is the relevant Skeleton.msg NODE_* suffix: cmc, mcp, pip, dip, ip, or tip.
translation directly relays Skeleton.joints[NODE_*].translation, which is already wrist-relative absolute position. Skeleton.joints[NODE_*].rotation is local relative to the parent joint, so the node composes rotations from parent to child before publishing the wrist-relative absolute rotation. This only changes rotation values; it does not reconstruct a serial TF hierarchy. The debug layout stays flat, with every joint a direct wrist child.
TF header.stamp is the corresponding Skeleton.header.stamp.
When publish_joint_tf:=false, none of this behavior occurs and existing MarkerArray or placeholder-wrist TF behavior is unchanged.
contact_glove_node also publishes the data in left/ergonomics and right/ergonomics as sensor_msgs/JointState on left/joint_states and right/joint_states. These topics are for robot_state_publisher and standard ROS 2 robot stacks such as MoveIt. The contact_glove package includes left and right hand-model URDF files: urdf/left_hand.urdf.xacro and urdf/right_hand.urdf.xacro.
25-joint debug TF exposes post-retarget Skeleton positions and rotations for inspection. JointState + URDF instead maps joint angles into named URDF joints for conventional robot visualization and integration.
The 20 Ergonomics.msg fields (thumb.{cmc,mcp,ip,tilt} and {index,middle,ring,little}.{mcp,pip,dip,tilt}) map one-to-one to 20 URDF joints.
Conversion is degrees to radians only. Values are not sign-inverted between left and right hands. URDF geometry and joint axes create the visual mirror image.
JointState.name is <side>_<finger>_<field>_joint, where <side> is left or right. Examples: left_thumb_cmc_joint and right_index_tilt_joint.
JointState.name and JointState.position follow Ergonomics.msg field order: thumb, index, middle, ring, little; mcp, pip, dip, tilt for each non-thumb finger; cmc, mcp, ip, tilt for the thumb.
Topic
Type
QoS
left/joint_states
sensor_msgs/JointState
depth 10, reliable, volatile
right/joint_states
sensor_msgs/JointState
depth 10, reliable, volatile
Unlike high-rate ergonomics and skeleton topics, these publishers are reliable. robot_state_publisher, MoveIt, and other common ROS 2 robot stacks often use reliable QoS, the ROS 2 default. A best-effort publisher would be incompatible; a reliable publisher remains compatible with a subscriber requesting best effort.
The fully qualified default topics are /contact_glove/left/joint_states and /contact_glove/right/joint_states. Override the namespace under the same rules as other streaming topics.
left/joint_states represents one robot model. With multiple left gloves connected at once, it publishes data from only one.
When left_joint_state_glove_id or right_joint_state_glove_id is empty (the default), the first glove detected on that side is selected automatically. It is selected again after reconnecting.
To fix the selected glove, set the parameter to that glove ID. Data from other gloves is ignored and logged as a warning. Normal topics such as left/ergonomics continue to publish all gloves.
To display multiple gloves separately through URDF, start multiple contact_glove_node instances with one namespace per glove. Also set a unique receive_port per node; a duplicate port causes the second and later UDP binds to fail. ds_ip and ds_port may remain shared when they point to the same DivingStation. Explicitly set left_joint_state_glove_id or right_joint_state_glove_id too, otherwise every node can select the same first glove.
Every joint and link name uses a left_ or right_ prefix, so both hand models can be loaded without collision.
Each hand has 20 joints, matching the 20 Ergonomics.msg fields, plus links that connect them. The root link is <side>_wrist.
Joint positions and orientations (origin) use calibrated hand geometry derived from measured reference points. The left hand reverses the right-hand reference-point Z coordinates and derives its own geometry. Its rest pose has exact mirror-image fingertip positions: equal X/Y and reversed Z.
Joint axes are adjusted so applying equal signed Ergonomics values to both hands correctly mirrors finger motion for flex joints (cmc, mcp, pip, dip, ip) and tilt joints (abduction/adduction).
Joint limits use the hand model's ranges in radians. They are not exact physical-device specifications.
Each link visual is a simple cylinder connecting adjacent joints, not a reconstruction of the physical hand.
The URDF is for visualization and joint hierarchy. It has no collision or inertial definitions; physical simulation is outside this package scope.
Passed directly to identically named contact_glove_node parameters
start_driver
true
Manual ownership switch for starting the SDK driver contact_glove_node. It does not automatically prevent duplicate starts.
hand
"both"
both, left, or right: selects which hand to display
parent_frame_id
"world"
Parent frame for left_wrist and right_wrist
enable_rviz2
true
Whether to start RViz2 with config/contact_glove_urdf.rviz
publish_wrist_placement
true
Publishes placeholder static TF from parent_frame_id to left_wrist and right_wrist at left/right_wrist_offset_{x,y,z}. Set false when an external source publishes actual wrist 6DoF.
left_wrist_offset_x/y/z, right_wrist_offset_x/y/z
left (0.0, -0.15, 0.0) / right (0.0, 0.15, 0.0)
Placeholder wrist positions in meters in parent_frame_id coordinates
left_wrist_offset_roll, right_wrist_offset_roll
left π (180°) / right 0.0
Placeholder wrist rotations in radians about X. The left and right URDFs share a local coordinate convention, so rotating only the left hand by 180° places them facing each other.
With hand:=both, the file starts both left and right robot_state_publisher processes. hand:=left or hand:=right starts only the specified side. robot_description and joint_states resolve to /<namespace>/left/... and equivalent names, automatically matching contact_glove_node left/joint_states. No remapping is needed.
This launch file does not start contact_glove_rviz_node for MarkerArrays or 25-joint debug TF. To use both views, do not start it and contact_glove_with_rviz.launch.xml separately. Use contact_glove_with_urdf_and_rviz.launch.xml instead. Both individual launch files start the SDK driver, so separate starts create two contact_glove_node instances whose UDP bind to the same receive_port conflicts.
URDF TF frame names such as left_wrist and left_thumb_cmc_link include namespace as a prefix, preventing TF collisions for multiple DivingStation instances in separate namespaces.
The publish_wrist_placement static TF uses the same names. The bundled config/contact_glove_urdf.rviz and config/contact_glove_combined.rviz set RobotModel TF Prefix to contact_glove for the default namespace. When overriding namespace:=..., set RViz TF Prefix to the same value without a trailing slash.
This launch file displays the post-retarget Skeleton (MarkerArray and 25-joint debug TF) alongside the derived URDF RobotModel. Its primary use is comparing both motions to verify Ergonomics-to-JointState conversion and URDF joint axes.
As above, separately starting contact_glove_with_rviz.launch.xml and contact_glove_with_urdf.launch.xml creates duplicate SDK drivers. Always use this combined launch file when both views are required.
It accepts every argument of contact_glove_with_rviz.launch.xml and contact_glove_with_urdf.launch.xml except start_driver. RViz2 starts with the bundled config/contact_glove_combined.rviz preset, which combines MarkerArray, TF, and RobotModel in one window.