模拟通道
概述
模拟通道是一个高频双向 WebSocket 通道,用于交换设备状态以及发送会话或设备命令。
默认网址: ws://localhost:10001
该端口可在配置中进行修改。
基本合同(重要):
- 当客户端连接时,该服务会发送一条包含完整设备列表的初始库存消息。
- 随后,服务器针对从客户端收到的每条消息, 都会发送一条 状态更新消息。
- 每次状态更新都包含所有设备的状态(及状态标记)。
本页面记录了模拟通道的协议和基本命令。
附加模块或功能可通过其自身的命令和/或状态字段来记录并扩展该系统。相关内容在“模块”一节中另有说明。
礼仪规则
客户通过消息回复
该服务会针对从客户端接收到的每条消息,发送一条包含所有设备状态的状态更新消息。
这意味着:
- 如果您需要获取设备状态的最新快照,则必须发送某项指令(会话命令、轮询命令或设备命令)。
- 该通道的工作原理是根据客户端的消息进行定时循环(“tick”)。
非强制性讯问(探询指令)
如果您想在不施加外力或更改模拟参数的情况下观察状态变化,请使用探测命令:
probe_position倾倒inverse3probe_orientation适用于Verse握把
探测命令不包含任何控制数据;它们只是强制查询设备信息,以便在下一次状态更新时,位置和姿态数据能够保持最新。
那些已经发送控制命令的模拟会话(set_cursor_force, set_cursor_position, 等)没有 pas简体中文(大陆) 需要发送探测命令——设备状态始终包含在响应帧中。探测命令适用于会话 仅限监控 (例如:Haply Hub)这些设备需要查询设备的状态,但不能对其进行控制。
配置与状态
- 来自初始盘点 包括字段
config,stateetstatus设备的。 - 来自 状态更新 常规字段包括以下字段
stateetstatus设备的。
如果您需要再次生成包含配置的快照,请使用 session.force_render_full_state.
configure et commands
设备消息支持两个独立的词典: configure et 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 这是一个词典,其中可以包含同一台设备的多个命令,但每种命令类型仅包含一个。
Le champ 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 Hub 会使用该名称来识别模拟环境,并保留各应用程序特有的设备调整设置。
{
"session": {
"configure": {
"profile": {
"name": "my_profile"
}
}
}
}
基础(课程级别)
定义整个会话的基础坐标系。该坐标系描述了Haply 的坐标如何转换为您的应用程序的坐标系; 一旦定义,所有设备的状态都将以此为基础返回,您发送的所有值也将在此基础上进行解释。
置换链以右手坐标系(Z轴向上)表示,其Haply :一个置换为 X, Y, Z,前面可能带有 + ou de -. 示例:
XYZ,ZYX,+Y-Z+X,X-ZYYZX这意味着您的Y对应于Haply 的右侧,您的Z位于Haply 前方,以及您的XHaply 顶部。
Z轴向上左旋坐标系示例(Unreal, X-YZ) :
{
"session": {
"configure": {
"basis": { "permutation": "X-YZ" }
}
}
}
session.set_basis在旧命令中,坐标轴的符号约定发生了变化 session.set_basis et session.configure.basis. 一种在旧命令中有效的置换,在新命令中可能会产生反向变换——此时请根据需要反转坐标轴的正负号(例如: X-ZY devient 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 est 持久的 -- 该服务会保留上次发送的值,直到您发送新值为止。无需在每个周期都发送该值;但由于其功能是控制摄像机或场景的导航,因此以周期为间隔进行更新是实现连续运动时预期的工作方式(例如,在用户移动场景时,每帧进行一次变换)。
{
"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 ou scale. 字段 position et 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" }
}
}
]
}
蒙太奇
定义设备的装配变换。请注意包络线 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 }
}
}
}
}
]
}
Le côté commande (客户端到服务器)封装了该转换: "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, basis et 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
}
}
]
}