跳过至主要内容

仿真通道

概述​

模拟通道是一种高频双向的 WebSocket 通道,用于交换设备状态以及发送会话或设备命令。

默认 URL: ws://localhost:10001

端口可在设置中进行更改。

核心约定(重要):

  • 当客户端连接时,服务会发送一条包含所有设备完整列表的初始设备清单消息。
  • 随后,服务器会针对从客户端接收到的每条消息,准确地发送一条 State Update消息。
  • “各状态更新”包含所有设备的状态(及状态信息)。
模块

本页面将介绍仿真通道的核心协议和命令。

通过注册自定义命令和状态字段,可以利用附加模块或功能来扩展系统。关于这些内容,将在“模块”一节中分别进行说明。

协议规则​

每个客户端消息对应一个响应​

该服务会针对从客户端接收到的每条消息, 发送一条包含所有设备状态的 State Update 消息。

这是以下意思:

  • 如果需要获取设备状态的最新快照,则必须发送某条消息(会话命令、探测命令或设备命令)。
  • 该通道的工作方式类似于由客户端消息驱动的“滴答”循环。

在不施加电力的情况下进行采样(探针命令)​

如果希望在不施加力或更改仿真参数的情况下监控状态变化,请使用探针命令:

  • 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只需设置一次的持久配置:预设、基准(basis)、安装(mount)、滤波器、模块设置第1次或修改时发送
commands每帧的非持久性指令:力、位置、扭矩需要每 tick 发送一次

commands 内的条目(set_cursor_force、set_cursor_position、set_angular_torques、set_angular_position)是,每次点击应用一次,不会保存。停止发送后,效果将立即消失,设备将恢复到空闲状态。对于那些无需重新发送、但应跨时钟周期保持的状态,configure 请使用。

注释
set_transform 这是一个特例

set_transform 是 commands 虽然会放在……下面,但,持久的。该服务会保留上次发送的变换,直到发送新的变换为止。每帧发送一次没有必要。话虽如此,由于其目的是用于相机/场景导航,因此以帧频进行流式传输(例如:当用户平移场景时,每帧发送一个变换)是连续运动导航中预期的工作模式。

坐标系​

Haply 默认情况下,使用Z-Up 的右手系坐标系。

有两个 configure 条目会影响坐标的解释和返回方式:

  • session.configure.basis: 更改Haply 的坐标与应用程序坐标系之间的对应关系(如下所示的 基础 (参见)。
  • inverse3[*].configure.preset: 选择一个带有名称的出厂配置来设置工作区的初始位置(例如: arm_front_centered 是 (0, 0, 0) 将其放置在工作区中央)。请参阅各设备的“配置”部分。

消息格式​

本节将介绍消息的高级封套和消息类型。完整的示例见本文档后半部分。

设备组​

消息在最高级别按设备类型进行分组(例如: inverse3、verse_grip、wireless_verse_grip)。各设备类型的键对应于每个设备的对象数组。

初始库存(服务器 → 客户端)​

在与 WebSocket 建立连接后立即发送一次。

每个条目包含以下内容:

  • device_id
  • config
  • state
  • status

参考:例如:初始库存的有效载荷

状态更新(服务器 → 客户端)​

每条客户端消息都会发送一次。

每个条目包含以下内容:

  • device_id
  • state
  • status

参考:例如:状态更新的有效载荷

会话命令的封装(客户端 → 服务器)​

会话命令是针对当前连接/会话执行的操作,与特定设备无关。

{
"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-ZY
  • YZX 是应用程序的 Y 位于Haply 的右侧,Z 但Haply 的前方,X 这意味着它位于Haply 的上方。

左手系·Z轴向上示例(Unreal、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 / 无线 Verse Grip

在 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 内的 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 模式的非对称性

命令在客户端到服务器的方向上,转换会被封装在包装器中:"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 } }

ForceGate​

设置ForceGate的衰减增益(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 扩展协议的一部分,用于实现了板卡扩展通信协议的握把版本。

握把扩展数据的设置​

支持的数据长度:

  • 上游(客户端 → 设备):最多 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
}
}
]
}