仿真通道
概述
仿真通道是一种高频、双向的 WebSocket通道,用于交换设备状态,并发送会话或设备命令。
默认网址: ws://localhost:10001
端口可在配置中进行修改。
核心约定(重要):
- 当客户端连接时,服务会发送一条“Initial Inventory”消息,其中包含完整的设备列表。
- 此后,对于客户端发来的每条消息,服务器都会恰好发送一条 State Update消息。
- 每条“状态更新”(State Update)都包含所有设备的状态(state 和 status)。
本页介绍了仿真通道的核心协议和命令。
其他模块/功能可以通过各自的命令和/或状态字段来注册并扩展系统。相关内容将在“模块”一章中另行说明。
协议规则
每条客户端消息对应一次响应
对于客户端发来的每条消息,服务都会发送一条“状态更新”消息,其中包含所有设备的状态。
这意味着:
- 如果需要获取设备状态的最新快照,您必须发送某些内容(会话命令、探测命令或设备命令之一)。
- 该通道的工作方式类似于由客户端消息驱动的“tick”循环。
不施加力的轮询(探测命令)
如果希望观察状态变化,但不想施加力或修改仿真参数,请使用探测命令:
probe_position用于inverse3probe_orientation适用于 Verse 握把
探测命令不包含命令数据,仅用于强制查询设备信息,以确保位置/方向数据在下一条状态更新中保持最新。
正在发送控制命令(set_cursor_force、set_cursor_position 等)的仿真会话无需发送探测命令——设备状态始终包含在响应帧中。探测命令旨在用于仅监控的会话(例如Haply Hub),此类会话需要在不驱动设备的情况下轮询设备状态。
配置与状态
- 初始库存 消息包含设备的
config、state和status。 - 常规的 各州最新动态 消息包含设备的
state和status。
如需再次获取包含配置信息的完整快照,请使用 session.force_render_full_state。
configure 与 commands
设备消息支持两种独立的映射:configure 和 commands。它们的用途不同:
| 映射 | 用途 | 发送频率 |
|---|---|---|
configure | 一次性持久配置:预设、基准、安装方式、滤波器、模块设置 | 发送一次,或在发生变化时发送 |
commands | 逐 tick 生效、非持久:力、位置、扭矩 | 必须在每个 tick 时发送 |
commands 中的条目(set_cursor_force、set_cursor_position、set_angular_torques、set_angular_position)每个 tick 应用一次,且不会被存储——如果停止发送,效果将立即消失,设备将恢复到空闲状态。对于需要跨 tick 持续生效且无需重复发送的内容,请使用 configure。
set_transform 属于特例set_transform 位于 commands 下,但它是持久的——该服务将保留
您最后发送的转换,直到您发送新的转换为止。您不必每个 tick 都会发送它。
不过,由于它的用途是相机/场景导航,
因此当用户平移场景时(例如每帧发送一个变换),以 tick 频率持续发送它,
这正是连续运动导航的预期使用模式。
坐标系
Haply 默认使用右手坐标系,Z 轴朝上。
以下两个 configure 条目会影响坐标的解释和返回方式:
session.configure.basis:更改Haply 坐标与应用程序坐标系之间的映射关系(参见下文的 基准)。inverse3[*].configure.preset:选择一个已命名的出厂配置,用于设置工作空间原点(例如arm_front_centered将(0, 0, 0)(置于工作区中心)。请参阅各设备中的“配置”部分。
消息格式
本节介绍高层消息封套结构与消息类型。完整示例请参见本文档后文。
设备分组
消息在顶层按设备类型分组(例如 inverse3、verse_grip、wireless_verse_grip)。每个设备类型键对应一个由各设备对象组成的数组。
初始库存(服务器 → 客户端)
在 WebSocket 连接建立后立即发送一次。
每个条目包含:
device_idconfigstatestatus
参见:示例:初始库存 负载
状态更新(服务器 → 客户端)
每条客户端消息对应发送一次。
每个条目包含:
device_idstatestatus
会话命令信封(客户端 → 服务器)
会话命令是针对当前连接/会话的操作,且不针对特定设备。
{
"session": {
"<command_name>": {
"...": "..."
}
}
}
设备命令封套(客户端 → 服务器)
设备命令以数组形式放在设备类型键下发送,从而使单条消息能够同时向一个或多个设备发送命令。每个设备条目同时支持 configure 映射和 commands 映射。
{
"<device_type>": [
{
"device_id": "<id>",
"configure": {
"<config_key>": { "...": "..." }
},
"commands": {
"<command_name>": { "...": "..." }
}
}
]
}
您可以在一条消息中包含多个条目,以便同时向同一类型的多个设备发送命令。
请注意,commands 这是一个字典,可以为某个设备包含多个命令,但每种命令类型只能有一个。
execute 字段
每个命令或 configure 条目都接受一个可选的 "execute" 布尔值(默认值为 true)。设置 "execute": false 之后,该条目会被解析但不会被应用。对于总是输出所有字段的基于反射的序列化器(例如 Unity JsonUtility)很有用。
"set_cursor_force": { "execute": false, "vector": { "x": 0.0, "y": 0.0, "z": 0.0 } }
命令参考
会话
强制渲染完整状态
请求所有设备的状态和配置快照。
{
"session": {
"force_render_full_state": {}
}
}
会话配置
会话级持久化配置通过 session.configure 发送。这与设备级别的 configure 与映射模式相对应。
配置文件
设置会话配置文件名称,Haply Hub 将据此识别仿真,并保存每个应用的设备微调设置。
{
"session": {
"configure": {
"profile": {
"name": "my_profile"
}
}
}
}
基准映射(会话级)
为整个会话设置基准映射。基准映射描述了Haply 坐标如何转换为应用程序的坐标系;设置完成后,所有设备状态都将以此基准返回,您发送的所有值也将以此基准进行解释。
以Haply 的右手/Z轴向上坐标系为参考系表示字符串的排列,即 X、Y、Z 的一种排列方式,可选择以 + 或 - 用作前缀。示例:
XYZ、ZYX、+Y-Z+X、X-ZYYZX请表达您的Y该轴对应Haply 的右侧,您的Z该轴位于Haply 的前方,您的X该轴位于Haply 的上方。
左手 Z 轴方向示例(虚幻引擎,X-YZ):
{
"session": {
"configure": {
"basis": { "permutation": "X-YZ" }
}
}
}
session.set_basis 迁移旧版 session.set_basis 与 session.configure.basis 各轴的符号约定已发生变化。在旧命令下有效的排列,在新命令下可能会产生反向变换——请在需要时对相应轴的符号进行取反(例如 X-ZY 变为 XZ-Y)。
所有设备命令
检测(所有设备)
使用探测命令可请求最新的位置/姿态信息,而无需施加力或进行其他仿真更改。
- inverse3:
probe_position - Verse grip(
verse_grip、wireless_verse_grip):probe_orientation
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"probe_position": {}
}
}
],
"verse_grip": [
{
"device_id": "049D",
"commands": {
"probe_orientation": {}
}
}
],
"wireless_verse_grip": [
{
"device_id": "049D",
"commands": {
"probe_orientation": {}
}
}
]
}
设置转换(所有设备)
为设备设置工作空间变换(从设备空间到应用空间)。
与其他 commands 条目不同,set_transform 是持久的——该服务会保留您发送的最后一个值,直到您发送新值为止。您无需在每个 tick 时都发送该值;但由于其用途是相机/场景导航,因此在用户平移场景等连续运动场景中,按 tick 频率持续发送(例如每帧发送一次变换)是预期用法。
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_transform": {
"transform": {
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 }
}
}
}
}
]
}
对于 Verse grip 和无线 Verse grip 设备,只有在转换过程中 rotation 质量会产生影响——握把仅报告姿态,因此 position 与 scale 没有可缩放或平移的内容。position 和 scale 这些字段仅为与Inverse3 的结构保持一致而被接受(并在快照中显示);通常的做法是将它们保留为单位值(position = {0,0,0},scale = {1,1,1})。
Inverse3 设备配置
配置条目通过 configure 映射发送。它们是持久的——只需发送一次,或者在发生变化时再发送。
预设
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"preset": { "preset": "arm_front_centered" }
}
}
]
}
可用值:device_defaults、arm_front、arm_front_centered、led_front、led_front_centered、custom。
基准映射(设备级)
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"basis": { "permutation": "ZXY" }
}
}
]
}
安装
设置设备安装转换。注意 mount 内部的 transform 包装层。
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"mount": {
"transform": {
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 }
}
}
}
}
]
}
命令一侧(客户端到服务器端)会进行封装和转换:"mount": { "transform": { ... } }。
快照另一侧(从服务器到客户端)则是扁平化的:"mount": { "position": {...}, "rotation": {...}, "scale": {...} }。
这是有意为之——命令采用统一的封装结构,而快照则直接序列化转换。请注意,不要将快照的结构复制到命令负载中。
阻尼
控制均匀阻尼和/或方向性阻尼。至少须包含一个字段。
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"damping": { "scalar": 0.5 }
}
}
]
}
你还可以设置方向性阻尼,或者同时设置这两者:
"damping": { "scalar": 0.5, "vector": { "x": 0.0, "y": 1.0, "z": 0.0 } }
力门控
设置力门控的衰减增益(0.0 至 1.0)。
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"force_gate": { "gain": 0.3 }
}
}
]
}
Inverse3 命令
要向 inverse3 设备发送命令,请在 inverse3 该键下包含一个 device_id 匹配的条目。该命令在每个 tick 时生效,必须在每个 tick 时重新发送才能保持有效。
向单个设备发送命令:
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
在一条消息中向多个设备发送命令:
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
},
{
"device_id": "049E",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
设置光标位置
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_position": {
"position": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
设置光标力
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
设置角度位置
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_angular_position": {
"angles": {
"a0": 1.0,
"a1": 2.0,
"a2": 3.0
}
}
}
}
]
}
设置角力矩
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_angular_torques": {
"torques": {
"a0": 1.0,
"a1": 2.0,
"a2": 3.0
}
}
}
}
]
}
Verse Grip 配置
Verse Grip 和无线 Verse Grip 设备支持与Inverse3 相同的 configure 键,用于 preset、basis 和 mount。
扩展 Verse Grip
set_extension_data 该命令是 Verse Grip 扩展协议的一部分,适用于实现了板级扩展通信协议的握把版本。
设置握把扩展数据
支持的数据长度:
- 上行(客户端 → 设备)最多 20 字节。
- 下行(设备 → 客户端)最多 12 字节,通过状态更新消息以
state.extension_data返回。
数据规格:
- 数组长度:20 字节
- 取值范围:每个值在0–255 之间
{
"wireless_verse_grip": [
{
"device_id": "049D",
"commands": {
"set_extension_data": {
"extension_data": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
}
}
}
]
}
示例
示例:初始设备清单载荷
当 WebSocket 建立连接时,服务会发送一条包含完整设备列表的消息。初始消息采用以下 JSON 格式:
{
"inverse3": [
{
"device_id": "04BA",
"config": {
"type": "inverse3",
"port": "COM13",
"device_info": {
"major_version": 7,
"minor_version": 1,
"id": "04BA",
"device_type": 4,
"uuid": "2D35F80DD9005F599B68F49944CB04BA"
},
"extended_device_id": "2D35F80DD9005F599B68F49944CB04BA",
"extended_firmware_version": "8C20FDC8010AA1E15AA133CDA2534874",
"gravity_compensation": {
"enabled": true,
"scaling_factor": 1
},
"handedness": "right",
"streaming_mode": "USB",
"torque_scaling": {
"enabled": true
},
"home_return": {
"enabled": false
},
"filters": {
"force_gate": { "gain": 0.3 },
"damping": { "scalar": 0 }
},
"preset": "defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"cursor_position": {
"x": 0.07842738,
"y": -0.14836666,
"z": 0.14297646
},
"cursor_velocity": {
"x": -0.011969013,
"y": 0.0012009288,
"z": -0.043197
},
"angular_position": {
"x": -69.31704,
"y": 137.62952,
"z": 19.832787
},
"angular_velocity": {
"x": 0,
"y": 0,
"z": 0
},
"body_orientation": {
"x": -0.01940918,
"y": 0.7026367,
"z": 0.00048828125,
"w": 0.7113037
},
"current_cursor_force": { "x": 0, "y": 0, "z": 0 },
"current_cursor_position": { "x": 0, "y": 0, "z": 0 },
"current_angular_torques": { "x": 0, "y": 0, "z": 0 },
"current_angular_position": { "x": 0, "y": 0, "z": 0 },
"control_domain": "cartesian",
"control_mode": "force",
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"calibrated": false,
"in_use": false,
"power_supply": true,
"ready": true,
"started": true
}
}
],
"verse_grip": [
{
"device_id": "61548",
"config": {
"type": "verse_grip",
"port": "COM3",
"preset": "device_defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"button": false,
"hall": 0,
"orientation": {
"x": -0.5019531,
"y": 0.8632202,
"z": -0.048095703,
"w": -0.022338867
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"error": 0,
"ready": true
}
}
],
"wireless_verse_grip": [
{
"device_id": "0",
"config": {
"type": "wireless_verse_grip",
"port": "COM6",
"major_version": 1,
"minor_version": 4,
"hardware_version": 1,
"streaming_mode": "Radio",
"preset": "device_defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"connected": true,
"awake": true,
"ready": true
}
}
]
}
示例:状态更新负载
该服务每收到一条消息,就会发送一条状态更新消息,其中包含所有设备的状态。
如果您想了解设备的状态,必须事先向其发送一条消息(例如探测命令或力值,即使数值为零)。当将设备用作输入源(例如跟踪位置)且未施加力时,这一点尤为重要。
状态更新消息采用以下 JSON 格式:
{
"inverse3": [
{
"device_id": "04BA",
"state": {
"cursor_position": {
"x": 0.07842738,
"y": -0.14836666,
"z": 0.14297646
},
"cursor_velocity": {
"x": -0.011969013,
"y": 0.0012009288,
"z": -0.043197
},
"angular_position": {
"x": -69.31704,
"y": 137.62952,
"z": 19.832787
},
"angular_velocity": {
"x": 0,
"y": 0,
"z": 0
},
"body_orientation": {
"x": -0.01940918,
"y": 0.7026367,
"z": 0.00048828125,
"w": 0.7113037
},
"current_cursor_force": { "x": 0, "y": 0, "z": 0 },
"current_cursor_position": { "x": 0, "y": 0, "z": 0 },
"current_angular_torques": { "x": 0, "y": 0, "z": 0 },
"current_angular_position": { "x": 0, "y": 0, "z": 0 },
"control_domain": "cartesian",
"control_mode": "force",
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"calibrated": false,
"in_use": false,
"power_supply": true,
"ready": true,
"started": true
}
}
],
"verse_grip": [
{
"device_id": "61548",
"state": {
"button": false,
"hall": 0,
"orientation": {
"x": -0.5019531,
"y": 0.8632202,
"z": -0.048095703,
"w": -0.022338867
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"error": 0,
"ready": true
}
}
],
"wireless_verse_grip": [
{
"device_id": "0",
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"connected": true,
"awake": true,
"ready": true
}
}
],
"custom_verse_grip": [
{
"device_id": "0",
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"extension_data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"ready": true
}
}
]
}