仿真通道
概述
模拟通道是一种高频双向 WebSocket 通道,用于交换设备状态以及传输会话或 设备命令。
默认网址: ws://localhost:10001
端口可在“设置”中进行更改。
核心协议(重要):
- 当客户端连接时,服务会发送一条包含完整设备列表的初始设备清单消息。
- 此后,服务器会针对从客户端接收到的每条消息,准确地发送一条 状态更新消息。
- 每次状态更新都包含所有设备的状态(及状态信息)。
本页面记录了仿真通道的核心协议和命令。
附加模块/功能可通过注册自身的命令和/或状态字段来扩展系统。这些内容已在“模块”部分中单独进行了说明。
协议规则
每个客户端消息对应一个响应
该服务会针对从客户端接收到的每条消息,发送一条包含所有设备状态的状态更新消息。
这意味着:
- 如果需要设备状态的最新快照,则必须发送某项操作(会话命令、探针命令或设备命令)。
- 该通道的工作原理类似于由客户端消息驱动的“滴答”循环。
不施加力进行探针检测(探针命令)
若要在不施加力或不更改仿真参数的情况下观察状态变化,请使用 probe 命令:
inverse3在……的情况下probe_position- 关于Verse grip的情况
probe_orientation
探针命令不包含命令数据,而是强制执行设备信息查询,以确保在下一次状态更新时,位置/方向数据保持最新。
已有的控制命令(set_cursor_force, set_cursor_position 正在传输(等)的仿真会话将发送探针命令 不需要 -- 设备状态总是包含在应答帧中。探针在无需驱动设备的情况下,应轮询设备状态。 仅用于监控 适用于会话(例如:Haply Hub)。
构成与状态
- 初始库存 该消息中提到了设备的
config,state,status包括…… - 一般 状态更新 该消息中提到了设备的
state哇status包括……
如果需要包含配置的快照, session.force_render_full_state请使用。
configure哇 commands
设备消息是 configure哇 commands支持两张独立的地图。这两张地图的用途各不相同:
| 地图 | 用途 | 传输频率 |
|---|---|---|
configure | 一次性永久设置:预设、基准、安装、滤镜、模块设置 | 发送一次或在更改时发送 |
commands | 每帧应用的非持久化项:力、位置、转矩 | 每次交易都必须进行转账 |
commands该项目(set_cursor_force, set_cursor_position, set_angular_torques, set_angular_position)是 每次点击仅应用一次,且不会保存 -- 一旦停止传输,效果将立即消失,设备将恢复到空闲状态。对于无需再次传输、但需在每个时间步长内保持的项目,包括 configure请使用。
set_transform这属于例外情况set_transform银 commands虽然属于……,但 永久的——该服务会保留上次发送的变换,直到发送新的变换为止。
并非必须在每个时间步长都发送变换。不过,由于其目的是用于摄像机/场景导航,因此在连续动作导航中,按时间步长进行流式传输(例如:当用户移动场景时,每帧发送一个变换)是预期中的使用模式。
坐标系
Haply 的默认坐标系是Z 轴向上方向的右手坐标系。
有两个配置选项会影响坐标的解析和返回方式:
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 字段
所有 command 或 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-up 示例(虚幻引擎, 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)都发送数据,但鉴于其用途是相机/场景导航,因此在连续运动过程中,以时间步长为间隔进行数据流传输是常见的使用模式(例如:当用户移动场景时,每帧发送一个变换)。
{
"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 和 Wireless 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
设置设备安装转换。 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": {...} }.
这是设计使然。因为 command 使用集成封装器,而 snapshot 则直接序列化转换结果。请注意,不要将 snapshot 格式复制到 command 有效载荷中。
阻尼
控制均匀阻尼和/或定向阻尼。必须至少包含一个字段。
{
"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 和 Wireless Verse Grip 设备是 preset, basis, mount关于 Inverse3 中使用的相同 configure 支持该键。
扩展版 Verse Grip
set_extension_data 该命令是 Verse grip 扩展协议的一部分,用于实现板级扩展通信协议的 grip 版本中。
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
}
}
]
}
示例:状态更新有效载荷
该服务会针对收到的每条消息,分别发送一条包含所有设备状态的状态更新消息。
如果想了解设备的状态,必须提前发送消息(例如:即使数值为0,也需发送探测命令或力值)。当将设备用作输入源(例如:位置跟踪)且不施加外力时,这一点尤为重要。
状态更新消息如下: 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
}
}
]
}