跳至主要内容
版本:3.5.x

模拟通道

概述

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

默认网址: ws://localhost:10001

可以在配置中更改端口。

核心合同(重要):

  • 当客户端连接时,服务端会发送一条初始设备清单消息,其中包含完整的设备列表。
  • 之后,服务器会针对从客户端接收到的每条消息,发送且仅发送一条 状态更新消息。
  • 每个状态更新都包含所有设备的状态(及状态信息)。
模块

本页面记录了 Simulation Channel的核心协议和命令。

其他模块/功能可以注册并扩展系统,添加其自身的命令和/或状态字段。相关内容在“模块”章节中有单独说明。

议事规则

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

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

这意味着:

  • 如果您需要获取设备状态的最新快照,必须发送一条指令(会话命令、探测命令或设备命令)。
  • 该通道的行为类似于一个由客户端消息驱动的“tick”循环。

不施加力的轮询(探测命令)

如果您想在不施加力或更改模拟参数的情况下观察状态变化,请使用探测命令:

  • probe_position 用于 inverse3
  • probe_orientation 适用于Verse握把

探测命令不包含命令数据,仅强制执行设备信息查询,以便在下一次状态更新时,位置/姿态数据保持最新。

您可能不需要探针命令

已经开始发送控制命令的模拟会话(set_cursor_force, set_cursor_position(等等)做 需要发送探测命令——响应帧中始终包含设备状态。探测命令旨在用于 仅监控 需要轮询设备状态但无需控制设备的会话(例如Haply )。

配置与状态

  • "(《世界人权宣言》) 初始库存 消息包含设备 config, statestatus.
  • 常规 各州最新动态 消息包含设备 statestatus.

如果您需要再次创建包含配置的快照,请使用 session.force_render_full_state.

configure vs commands

设备消息支持两个独立的映射: configurecommands. 它们各自有不同的用途:

地图目的频率
configure一次性持久化配置:预设、基底、挂载、过滤器、模块设置仅发送一次或发生变化时发送
commands每帧更新,非持久化:力、位置、力矩必须在每个时钟周期发送

条目在 commands (set_cursor_force, set_cursor_position, set_angular_torques, set_angular_position) 被应用 每帧一次,且不存储 -- 一旦停止发送信号,效果会立即消失,设备将恢复待机状态。使用 configure 用于任何需要在时钟周期之间保持持久化且无需重新发送的数据。

set_transform 是一个特例

set_transform 居住在…… commands持久的 — 该服务会保留 您发送的最后一次转换,直到您发送新的转换。您无需 在每个时间步长内发送 该数据。话虽如此,由于其用途是用于相机/场景导航, 以时间步长频率进行流式传输(例如,当用户平移场景时,每帧发送一个变换) 是连续运动导航的预期使用模式。

坐标系

Haply 默认Haply Z轴向上(Z-Up)的右手坐标系。

有两个配置项会影响坐标的解析和返回方式:

  • session.configure.basis : 更改Haply 与应用程序坐标系之间的映射关系(参见 基础 (如下)。
  • inverse3[*].configure.preset : 选择一个命名的工厂配置,该配置将工作区原点(例如 arm_front_centered puts (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 领域

每个命令或配置项都支持一个可选的 "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 用于识别模拟环境并保存各应用的设备调整设置。

{
"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-up 示例(虚幻引擎, X-YZ):

{
"session": {
"configure": {
"basis": { "permutation": "X-YZ" }
}
}
}
从……迁移 session.set_basis

在旧版与新版之间,坐标轴的符号约定发生了变化 session.set_basissession.configure.basis. 在旧命令下有效的置换 在新命令下可能会产生反向变换——请在必要时取轴 符号的反(例如: X-ZY 变成 XZ-Y).

所有设备命令

检测(所有设备)

使用探测命令来获取最新的位置/姿态信息,而无需施加力或其他模拟更改。

  • inverse3: probe_position
  • Verse 握把 (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持久的 — 该 服务会保留您发送的最后一个值,直到您发送新的值为止。您无需 在每个时间步长都发送该值,但由于其用途是用于摄像机/场景导航, 因此以时间步长频率进行流式传输是实现连续运动的 预期使用模式(例如,当用户平移场景时,每帧发送一个变换)。

{
"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 握把 / 无线 Verse 握把

对于 Verse 握把和无线 Verse 握把设备,仅限 rotation 变换的 该分量会产生影响——手柄仅报告方向,因此 没有 positionscale 缩放或转换。该 positionscale 这些字段仅出于与Inverse3 保持模式一致性 而被接受(并在快照中回显);将其保留为默认值 (position = {0,0,0}, scale = {1,1,1}) 是标准做法。

Inverse3 配置

配置条目通过 configure map。它们是持久化的——只需发送一次或在发生变化时发送。

预设

{
"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" }
}
}
]
}

安装

设置设备的挂载变换。请注意 transform 内部包装 mount.

{
"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 }
}
}
}
}
]
}
安装模式不对称

"(《世界人权宣言》) 命令 该侧(客户端到服务器)包裹了 transform: "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 设备,包含一个匹配的条目 device_id 在……之下 inverse3 键。命令按每帧执行,必须在每帧重新发送才能保持有效。

控制一台设备:

{
"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 设备均支持此功能 configure 键作为Inverse3 preset, basismount.

扩展诗句手柄

"(《世界人权宣言》) set_extension_data 该命令是 Verse 握把扩展协议的一部分,适用于实现了板扩展通信协议的握把版本。

设置握把伸展数据

支持的数据长度:

  • 上行数据量最多为 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
}
}
]
}