移除MQTT,更新launch文档,提供注册表示例文件,更新到0.10.5

This commit is contained in:
Xuwznln
2025-09-15 02:39:43 +08:00
parent 94cdcbf24e
commit 2ca0311de6
24 changed files with 1902 additions and 715 deletions

View File

@@ -1,6 +1,6 @@
package:
name: unilabos
version: 0.10.3
version: 0.10.5
source:
path: ../unilabos
@@ -10,7 +10,6 @@ build:
python:
entry_points:
- unilab = unilabos.app.main:main
- unilab-register = unilabos.app.register:main
script:
- set PIP_NO_INDEX=
- if: win

View File

@@ -1,26 +1,64 @@
## 简单单变量动作函数
### `SendCmd`
```{literalinclude} ../../unilabos_msgs/action/SendCmd.action
:language: yaml
```
----
---
### `StrSingleInput`
```{literalinclude} ../../unilabos_msgs/action/StrSingleInput.action
:language: yaml
```
---
### `IntSingleInput`
```{literalinclude} ../../unilabos_msgs/action/IntSingleInput.action
:language: yaml
```
---
### `FloatSingleInput`
```{literalinclude} ../../unilabos_msgs/action/FloatSingleInput.action
:language: yaml
```
---
### `Point3DSeparateInput`
```{literalinclude} ../../unilabos_msgs/action/Point3DSeparateInput.action
:language: yaml
```
---
### `Wait`
```{literalinclude} ../../unilabos_msgs/action/Wait.action
:language: yaml
```
---
## 常量有机化学操作
Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab.io/chemputer/xdl/standard/full_steps_specification.html#),包含有机合成实验中常见的操作,如加热、搅拌、冷却等。
### `Clean`
```{literalinclude} ../../unilabos_msgs/action/Clean.action
:language: yaml
```
----
---
### `EvacuateAndRefill`
@@ -28,7 +66,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `Evaporate`
@@ -36,7 +74,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `HeatChill`
@@ -44,7 +82,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `HeatChillStart`
@@ -52,7 +90,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `HeatChillStop`
@@ -60,7 +98,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `PumpTransfer`
@@ -68,7 +106,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `Separate`
@@ -76,7 +114,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `Stir`
@@ -84,20 +122,179 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `Add`
```{literalinclude} ../../unilabos_msgs/action/Add.action
:language: yaml
```
---
### `AddSolid`
```{literalinclude} ../../unilabos_msgs/action/AddSolid.action
:language: yaml
```
---
### `AdjustPH`
```{literalinclude} ../../unilabos_msgs/action/AdjustPH.action
:language: yaml
```
---
### `Centrifuge`
```{literalinclude} ../../unilabos_msgs/action/Centrifuge.action
:language: yaml
```
---
### `CleanVessel`
```{literalinclude} ../../unilabos_msgs/action/CleanVessel.action
:language: yaml
```
---
### `Crystallize`
```{literalinclude} ../../unilabos_msgs/action/Crystallize.action
:language: yaml
```
---
### `Dissolve`
```{literalinclude} ../../unilabos_msgs/action/Dissolve.action
:language: yaml
```
---
### `Dry`
```{literalinclude} ../../unilabos_msgs/action/Dry.action
:language: yaml
```
---
### `Filter`
```{literalinclude} ../../unilabos_msgs/action/Filter.action
:language: yaml
```
---
### `FilterThrough`
```{literalinclude} ../../unilabos_msgs/action/FilterThrough.action
:language: yaml
```
---
### `Hydrogenate`
```{literalinclude} ../../unilabos_msgs/action/Hydrogenate.action
:language: yaml
```
---
### `Purge`
```{literalinclude} ../../unilabos_msgs/action/Purge.action
:language: yaml
```
---
### `Recrystallize`
```{literalinclude} ../../unilabos_msgs/action/Recrystallize.action
:language: yaml
```
---
### `RunColumn`
```{literalinclude} ../../unilabos_msgs/action/RunColumn.action
:language: yaml
```
---
### `StartPurge`
```{literalinclude} ../../unilabos_msgs/action/StartPurge.action
:language: yaml
```
---
### `StartStir`
```{literalinclude} ../../unilabos_msgs/action/StartStir.action
:language: yaml
```
---
### `StopPurge`
```{literalinclude} ../../unilabos_msgs/action/StopPurge.action
:language: yaml
```
---
### `StopStir`
```{literalinclude} ../../unilabos_msgs/action/StopStir.action
:language: yaml
```
---
### `Transfer`
```{literalinclude} ../../unilabos_msgs/action/Transfer.action
:language: yaml
```
---
### `WashSolid`
```{literalinclude} ../../unilabos_msgs/action/WashSolid.action
:language: yaml
```
---
## 移液工作站及相关生物自动化设备操作
Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.org/user_guide/index.html),包含生物实验中常见的操作,如移液、混匀、离心等。
### `LiquidHandlerAspirate`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAspirate.action
:language: yaml
```
----
---
### `LiquidHandlerDiscardTips`
@@ -105,7 +302,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerDispense`
@@ -113,7 +310,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerDropTips`
@@ -121,7 +318,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerDropTips96`
@@ -129,7 +326,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerMoveLid`
@@ -137,7 +334,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerMovePlate`
@@ -145,7 +342,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerMoveResource`
@@ -153,7 +350,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerPickUpTips`
@@ -161,7 +358,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerPickUpTips96`
@@ -169,7 +366,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerReturnTips`
@@ -177,7 +374,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerReturnTips96`
@@ -185,7 +382,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerStamp`
@@ -193,7 +390,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerTransfer`
@@ -201,9 +398,113 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
## 多工作站及小车运行、物料转移
---
### `LiquidHandlerAdd`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAdd.action
:language: yaml
```
---
### `LiquidHandlerIncubateBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerIncubateBiomek.action
:language: yaml
```
---
### `LiquidHandlerMix`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMix.action
:language: yaml
```
---
### `LiquidHandlerMoveBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveBiomek.action
:language: yaml
```
---
### `LiquidHandlerMoveTo`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveTo.action
:language: yaml
```
---
### `LiquidHandlerOscillateBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerOscillateBiomek.action
:language: yaml
```
---
### `LiquidHandlerProtocolCreation`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerProtocolCreation.action
:language: yaml
```
---
### `LiquidHandlerRemove`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerRemove.action
:language: yaml
```
---
### `LiquidHandlerSetGroup`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetGroup.action
:language: yaml
```
---
### `LiquidHandlerSetLiquid`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetLiquid.action
:language: yaml
```
---
### `LiquidHandlerSetTipRack`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetTipRack.action
:language: yaml
```
---
### `LiquidHandlerTransferBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferBiomek.action
:language: yaml
```
---
### `LiquidHandlerTransferGroup`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferGroup.action
:language: yaml
```
---
## 多工作站及小车运行、物料转移
### `AGVTransfer`
@@ -211,7 +512,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `WorkStationRun`
@@ -219,12 +520,64 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `ResetHandling`
```{literalinclude} ../../unilabos_msgs/action/ResetHandling.action
:language: yaml
```
---
### `ResourceCreateFromOuter`
```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuter.action
:language: yaml
```
---
### `ResourceCreateFromOuterEasy`
```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuterEasy.action
:language: yaml
```
---
### `SetPumpPosition`
```{literalinclude} ../../unilabos_msgs/action/SetPumpPosition.action
:language: yaml
```
---
## 固体分配与处理设备操作
### `SolidDispenseAddPowderTube`
```{literalinclude} ../../unilabos_msgs/action/SolidDispenseAddPowderTube.action
:language: yaml
```
---
## 其他设备操作
### `EmptyIn`
```{literalinclude} ../../unilabos_msgs/action/EmptyIn.action
:language: yaml
```
---
## 机械臂、夹爪等机器人设备
Uni-Lab 机械臂、机器人、夹爪和导航指令集沿用 ROS2 的 `control_msgs` 和 `nav2_msgs`
### `FollowJointTrajectory`
```yaml
@@ -292,7 +645,8 @@ trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_error
```
----
---
### `GripperCommand`
```yaml
@@ -310,17 +664,19 @@ bool reached_goal # True iff the gripper position has reached the commanded setp
```
----
---
### `JointTrajectory`
```yaml
trajectory_msgs/JointTrajectory trajectory
---
---
---
```
----
---
### `PointHead`
```yaml
@@ -330,12 +686,13 @@ string pointing_frame
builtin_interfaces/Duration min_duration
float64 max_velocity
---
---
float64 pointing_angle_error
```
----
---
### `SingleJointPosition`
```yaml
@@ -343,15 +700,16 @@ float64 position
builtin_interfaces/Duration min_duration
float64 max_velocity
---
---
std_msgs/Header header
float64 position
float64 velocity
float64 error
```
----
---
### `AssistedTeleop`
```yaml
@@ -363,10 +721,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback
builtin_interfaces/Duration current_teleop_duration
```
----
---
### `BackUp`
```yaml
@@ -380,10 +738,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 distance_traveled
```
----
---
### `ComputePathThroughPoses`
```yaml
@@ -398,10 +756,10 @@ nav_msgs/Path path
builtin_interfaces/Duration planning_time
---
#feedback definition
```
----
---
### `ComputePathToPose`
```yaml
@@ -416,10 +774,10 @@ nav_msgs/Path path
builtin_interfaces/Duration planning_time
---
#feedback definition
```
----
---
### `DriveOnHeading`
```yaml
@@ -433,10 +791,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 distance_traveled
```
----
---
### `DummyBehavior`
```yaml
@@ -447,10 +805,10 @@ std_msgs/String command
builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
```
----
---
### `FollowPath`
```yaml
@@ -465,10 +823,10 @@ std_msgs/Empty result
#feedback definition
float32 distance_to_goal
float32 speed
```
----
---
### `FollowWaypoints`
```yaml
@@ -480,10 +838,10 @@ int32[] missed_waypoints
---
#feedback definition
uint32 current_waypoint
```
----
---
### `NavigateThroughPoses`
```yaml
@@ -501,10 +859,10 @@ builtin_interfaces/Duration estimated_time_remaining
int16 number_of_recoveries
float32 distance_remaining
int16 number_of_poses_remaining
```
----
---
### `NavigateToPose`
```yaml
@@ -521,10 +879,10 @@ builtin_interfaces/Duration navigation_time
builtin_interfaces/Duration estimated_time_remaining
int16 number_of_recoveries
float32 distance_remaining
```
----
---
### `SmoothPath`
```yaml
@@ -540,10 +898,10 @@ builtin_interfaces/Duration smoothing_duration
bool was_completed
---
#feedback definition
```
----
---
### `Spin`
```yaml
@@ -556,10 +914,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 angular_distance_traveled
```
----
---
### `Wait`
```yaml
@@ -571,7 +929,6 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
builtin_interfaces/Duration time_left
```
----
---

View File

@@ -1,37 +1,142 @@
# 添加新动作指令Action
1.`unilabos_msgs/action` 中新建实验操作名和参数列表,如 `MyDeviceCmd.action`。一个 Action 定义由三个部分组成分别是目标Goal、结果Result和反馈Feedback之间使用 `---` 分隔:
本指南将引导你完成添加新动作指令的整个流程,包括编写、在线构建和测试。
## 1. 编写新的 Action
### 1.1 创建 Action 文件
`unilabos_msgs/action` 目录中新建实验操作文件,如 `MyDeviceCmd.action`。一个 Action 定义由三个部分组成分别是目标Goal、结果Result和反馈Feedback之间使用 `---` 分隔:
```action
# 目标Goal
# 目标Goal- 定义动作执行所需的参数
string command
float64 timeout
---
# 结果Result
bool success
# 结果Result- 定义动作完成后返回的结果
bool success # 要求必须包含success以便回传执行结果
string return_info # 要求必须包含return_info以便回传执行结果
... # 其他
---
# 反馈Feedback
# 反馈Feedback- 定义动作执行过程中的反馈信息
float64 progress
string status
```
2.`unilabos_msgs/CMakeLists.txt` 中添加新定义的 action
### 1.2 更新 CMakeLists.txt
`unilabos_msgs/CMakeLists.txt` 中的 `add_action_files()` 部分添加新定义的 action
```cmake
add_action_files(
FILES
MyDeviceCmd.action
# 其他已有的 action 文件...
)
```
3. 因为在指令集中新建了指令,因此调试时需要编译,并在终端环境中加载临时路径:
## 2. 在线构建和测试
为了简化开发流程并确保构建环境的一致性,我们使用 GitHub Actions 进行在线构建。
### 2.1 Fork 仓库并创建分支
1. **Fork 仓库**:在 GitHub 上 fork `Uni-Lab-OS` 仓库到你的个人账户
2. **Clone 你的 fork**
```bash
git clone https://github.com/YOUR_USERNAME/Uni-Lab-OS.git
cd Uni-Lab-OS
```
3. **创建功能分支**
```bash
git checkout -b add-my-device-action
```
4. **提交你的更改**
```bash
git add unilabos_msgs/action/MyDeviceCmd.action
git add unilabos_msgs/CMakeLists.txt
git commit -m "Add MyDeviceCmd action for device control"
git push origin add-my-device-action
```
### 2.2 触发在线构建
1. **访问你的 fork 仓库**:在浏览器中打开你的 fork 仓库页面
2. **手动触发构建**
- 点击 "Actions" 标签
- 选择 "Multi-Platform Conda Build" 工作流
- 点击 "Run workflow" 按钮
3. **监控构建状态**
- 构建过程大约需要 5-10 分钟
- 在 Actions 页面可以实时查看构建日志
- 构建完成后,可以下载生成的 conda 包进行测试
### 2.3 下载和测试构建包
1. **下载构建产物**
- 在构建完成的 Action 页面,找到 "Artifacts" 部分
- 下载对应平台的 `conda-package-*` 文件
2. **本地测试安装**
```bash
# 解压下载的构建产物
unzip conda-package-linux-64.zip # 或其他平台
# 安装测试包
mamba install ./ros-humble-unilabos-msgs-*.conda
```
3. **验证 Action 是否正确添加**
```bash
# 检查 action 是否可用
ros2 interface show unilabos_msgs/action/MyDeviceCmd
```
## 3. 提交 Pull Request
测试成功后,向主仓库提交 Pull Request
1. **创建 Pull Request**
- 在你的 fork 仓库页面,点击 "New Pull Request"
- 选择你的功能分支作为源分支
- 填写详细的 PR 描述,包括:
- 添加的 Action 功能说明
- 测试结果
- 相关的设备或用例
2. **等待审核和合并**
- 维护者会审核你的代码
- CI/CD 系统会自动运行完整的测试套件
- 合并后,新的指令集会自动发布到官方 conda 仓库
## 4. 使用新的 Action
如果采用自己构建的action包可以通过以下命令更新安装
```bash
cd unilabos_msgs
colcon build
source ./install/local_setup.sh
cd ..
mamba remove --force ros-humble-unilabos-msgs
mamba config set safety_checks disabled # 如果没有提升版本号会触发md5与网络上md5不一致是正常现象因此通过本指令关闭md5检查
mamba install xxx.conda2 --offline
```
调试成功后,发起 pull requestUni-Lab 的 CI/CD 系统会自动将新的指令集编译打包mamba执行升级即可永久生效
## 常见问题
```bash
mamba update ros-humble-unilabos-msgs -c http://quetz.dp.tech:8088/get/unilab -c robostack-humble -c robostack-staging
```
**Q: 构建失败怎么办?**
A: 检查 Actions 日志中的错误信息,通常是语法错误或依赖问题。修复后重新推送代码即可自动触发新的构建。
**Q: 如何测试特定平台?**
A: 在手动触发构建时,在平台选择中只填写你需要的平台,如 `linux-64` 或 `win-64`。
**Q: 构建包在哪里下载?**
A: 在 Actions 页面的构建结果中,查找 "Artifacts" 部分,每个平台都有对应的构建包可供下载。

View File

@@ -1,95 +1,610 @@
# yaml注册表编写指南
# yaml 注册表编写指南
`注册表的结构`
## 快速开始:使用注册表编辑器
1. 顶层名称:每个设备的注册表以设备名称开头,例如 new_device
2. class 字段:定义设备的模块路径和类型。
3. schema 字段:定义设备的属性模式,包括属性类型、描述和必需字段。
4. action_value_mappings 字段:定义设备支持的动作及其目标、反馈和结果。
推荐使用 UniLabOS 自带的可视化编辑器,它能帮你自动生成大部分配置,省去手写的麻烦
`创建新的注册表教程`
1. 创建文件
在 devices 文件夹中创建一个新的 YAML 文件,例如 new_device.yaml。
### 怎么用编辑器
2. 定义设备名称
在文件中定义设备的顶层名称例如new_device
1. 启动 UniLabOS
2. 在浏览器中打开"注册表编辑器"页面
3. 选择你的 Python 设备驱动文件
4. 点击"分析文件",让系统读取你的类信息
5. 填写一些基本信息(设备描述、图标啥的)
6. 点击"生成注册表",复制生成的内容
7. 把内容保存到 `devices/` 目录下
3. 定义设备的类信息
添加设备的模块路径和类型:
我们为你准备了一个测试驱动用于在界面上尝试注册表生成参见目录test\registry\example_devices.py
```python
new_device: # 定义一个名为 linear_motion.grbl 的设备
---
## 手动编写指南
class: # 定义设备的类信息
module: unilabos.devices_names.new_device:NewDeviceClass # 指定模块路径和类名
type: python # 指定类型为 Python 类
status_types:
```
4. 定义设备支持的动作
添加设备支持的动作及其目标、反馈和结果:
```python
action_value_mappings:
set_speed:
type: SendCmd
goal:
command: speed
feedback: {}
result:
success: success
```
`如何编写action_valve_mappings`
1. 在 devices 文件夹中的 YAML 文件中action_value_mappings 是用来定义设备支持的动作actions及其目标值goal、反馈值feedback和结果值result的映射规则。以下是规则和编写方法
```python
action_value_mappings:
<action_name>: # <action_name>:动作的名称
# start启动设备或某个功能。
# stop停止设备或某个功能。
# set_speed设置设备的速度。
# set_temperature设置设备的温度。
# move_to_position移动设备到指定位置。
# stir执行搅拌操作。
# heatchill执行加热或冷却操作。
# send_nav_task发送导航任务例如机器人导航
# set_timer设置设备的计时器。
# valve_open_cmd打开阀门。
# valve_close_cmd关闭阀门。
# execute_command_from_outer执行外部命令。
# push_to控制设备推送到某个位置例如机械爪
# move_through_points导航设备通过多个点。
如果你想自己写 yaml 文件,或者想深入了解结构,查阅下方说明。
type: <ActionType> # 动作的类型,表示动作的功能
# 根据动作的功能选择合适的类型:
# SendCmd发送简单命令。
# NavigateThroughPoses导航动作。
# SingleJointPosition设置单一关节的位置。
# Stir搅拌动作。
# HeatChill加热或冷却动作。
## 注册表的基本结构
goal: # 定义动作的目标值映射,表示需要传递给设备的参数。
<goal_key>: <mapped_value> #确定设备需要的输入参数,并将其映射到设备的字段。
yaml 注册表就是设备的配置文件,里面定义了设备怎么用、有什么功能。好消息是系统会自动帮你填大部分内容,你只需要写两个必需的东西:设备名和 class 信息。
feedback: # 定义动作的反馈值映射,表示设备执行动作时返回的实时状态。
<feedback_key>: <mapped_value>
result: # 定义动作的结果值映射,表示动作完成后返回的最终结果。
<result_key>: <mapped_value>
### 各字段用途
| 字段名 | 类型 | 需要手写 | 说明 |
| ----------------- | ------ | -------- | ----------------------------------- |
| 设备标识符 | string | 是 | 设备的唯一名字,比如 `mock_chiller` |
| class | object | 部分 | 设备的核心信息,必须写 |
| description | string | 否 | 设备描述,系统默认给空字符串 |
| handles | array | 否 | 连接关系,默认是空的 |
| icon | string | 否 | 图标路径,默认为空 |
| init_param_schema | object | 否 | 初始化参数,系统自动分析生成 |
| version | string | 否 | 版本号,默认 "1.0.0" |
| category | array | 否 | 设备分类,默认用文件名 |
| config_info | array | 否 | 嵌套配置,默认为空 |
| file_path | string | 否 | 文件路径,系统自动设置 |
| registry_type | string | 否 | 注册表类型,自动设为 "device" |
### class 字段里有啥
class 是核心部分,包含这些内容:
| 字段名 | 类型 | 需要手写 | 说明 |
| --------------------- | ------ | -------- | ---------------------------------- |
| module | string | 是 | Python 类的路径,必须写 |
| type | string | 是 | 驱动类型,一般写 "python" |
| status_types | object | 否 | 状态类型,系统自动分析生成 |
| action_value_mappings | object | 部分 | 动作配置,系统会自动生成一些基础的 |
## 怎么创建新的注册表
### 创建文件
在 devices 文件夹里新建一个 yaml 文件,比如 `new_device.yaml`
### 完整结构是什么样的
```yaml
new_device: # 设备名,要唯一
class: # 核心配置
action_value_mappings: # 动作配置(后面会详细说)
action_name:
# 具体的动作设置
module: unilabos.devices.your_module.new_device:NewDeviceClass # 你的 Python 类
status_types: # 状态类型(系统会自动生成)
status: str
temperature: float
# 其他状态
type: python # 驱动类型,一般就是 python
description: New Device Description # 设备描述
handles: [] # 连接关系,通常是空的
icon: '' # 图标路径
init_param_schema: # 初始化参数(系统会自动生成)
config: # 初始化时需要的参数
properties:
port:
default: DEFAULT_PORT
type: string
required: []
type: object
data: # 前端显示用的数据类型
properties:
status:
type: string
temperature:
type: number
required:
- status
type: object
version: 0.0.1 # 版本号
category:
- device_category # 设备类别
config_info: [] # 嵌套配置,通常为空
```
6. 定义设备的属性模式
添加设备的属性模式,包括属性类型和描述:
```python
schema:
type: object
## action_value_mappings 怎么写
这个部分定义设备能做哪些动作。好消息是系统会自动生成大部分动作,你通常只需要添加一些特殊的自定义动作。
### 系统自动生成哪些动作
系统会帮你生成这些:
1.`auto-` 开头的动作:从你 Python 类的方法自动生成
2. 通用的驱动动作:
- `_execute_driver_command`:同步执行驱动命令
- `_execute_driver_command_async`:异步执行驱动命令
### 如果要手动定义动作
如果你需要自定义一些特殊动作,需要这些字段:
| 字段名 | 需要手写 | 说明 |
| ---------------- | -------- | -------------------------------- |
| type | 是 | 动作类型,必须指定 |
| goal | 是 | 输入参数怎么映射 |
| feedback | 否 | 实时反馈,通常为空 |
| result | 是 | 结果怎么返回 |
| goal_default | 部分 | 参数默认值ROS 动作会自动生成 |
| schema | 部分 | 前端表单配置ROS 动作会自动生成 |
| handles | 否 | 连接关系,默认为空 |
| placeholder_keys | 否 | 特殊输入字段配置 |
### 动作类型有哪些
| 类型 | 什么时候用 | 系统会自动生成什么 |
| ---------------------- | -------------------- | ---------------------- |
| UniLabJsonCommand | 自定义同步 JSON 命令 | 啥都不生成 |
| UniLabJsonCommandAsync | 自定义异步 JSON 命令 | 啥都不生成 |
| ROS 动作类型 | 标准 ROS 动作 | goal_default 和 schema |
常用的 ROS 动作类型:
- `SendCmd`:发送简单命令
- `NavigateThroughPoses`:导航动作
- `SingleJointPosition`:单关节位置控制
- `Stir`:搅拌动作
- `HeatChill``HeatChillStart`:加热冷却动作
### 复杂一点的例子
```yaml
heat_chill_start:
type: HeatChillStart
goal:
purpose: purpose
temp: temp
goal_default: # ROS动作会自动生成你也可以手动覆盖
purpose: ''
temp: 0.0
handles:
output:
- handler_key: labware
label: Labware
data_type: resource
data_source: handle
data_key: liquid
placeholder_keys:
purpose: unilabos_resources
result:
status: status
success: success
# schema 系统会自动生成,不用写
```
### 动作名字怎么起
根据设备用途来起名字:
- 启动停止类:`start``stop``pause``resume`
- 设置参数类:`set_speed``set_temperature``set_timer`
- 移动控制类:`move_to_position``move_through_points`
- 功能操作类:`stir``heat_chill_start``heat_chill_stop`
- 开关控制类:`valve_open_cmd``valve_close_cmd``push_to`
- 命令执行类:`send_nav_task``execute_command_from_outer`
### 常用的动作类型
- `UniLabJsonCommand`:自定义 JSON 命令(不走 ROS
- `UniLabJsonCommandAsync`:异步 JSON 命令(不走 ROS
- `SendCmd`:发送简单命令
- `NavigateThroughPoses`:导航相关
- `SingleJointPosition`:单关节控制
- `Stir`:搅拌
- `HeatChill``HeatChillStart`:加热冷却
- 其他的 ROS 动作类型:看具体的 ROS 服务
### 示例:完整的动作配置
```yaml
heat_chill_start:
type: HeatChillStart
goal:
purpose: purpose
temp: temp
goal_default:
purpose: ''
temp: 0.0
handles:
output:
- handler_key: labware
label: Labware
data_type: resource
data_source: handle
data_key: liquid
placeholder_keys:
purpose: unilabos_resources
result:
status: status
success: success
schema:
description: '启动加热冷却功能'
properties:
goal:
properties:
purpose:
type: string
description: '用途说明'
temp:
type: number
description: '目标温度'
required:
- purpose
- temp
title: HeatChillStart_Goal
type: object
required:
- goal
title: HeatChillStart
type: object
feedback: {}
```
## 系统自动生成的字段
### status_types
系统会扫描你的 Python 类,从状态方法自动生成这部分:
```yaml
status_types:
current_temperature: float # 从 get_current_temperature() 方法来的
is_heating: bool # 从 get_is_heating() 方法来的
status: str # 从 get_status() 方法来的
```
注意几点:
- 系统会找所有 `get_` 开头的方法
- 类型会自动转成 ROS 类型(比如 `str` 变成 `String`
- 如果类型是 `Any``None` 或者不知道的,就默认用 `String`
### init_param_schema
这个完全是系统自动生成的,你不用管:
```yaml
init_param_schema:
config: # 从你类的 __init__ 方法分析出来的
properties:
port:
type: string
default: '/dev/ttyUSB0'
baudrate:
type: integer
default: 9600
required: []
type: object
data: # 根据 status_types 生成的前端用的类型
properties:
current_temperature:
type: number
is_heating:
type: boolean
status:
type: string
description: The status of the device
speed:
type: number
description: The speed of the device
required:
- status
- speed
additionalProperties: false
type: object
```
# 写完yaml注册表后需要添加到哪些其他文件
生成规则很简单:
- `config` 部分:看你类的 `__init__` 方法有什么参数,类型和默认值是啥
- `data` 部分:根据 `status_types` 生成前端显示用的类型定义
### 其他自动填充的字段
```yaml
version: '1.0.0' # 默认版本
category: ['文件名'] # 用你的 yaml 文件名当类别
description: '' # 默认为空,你可以手动改
icon: '' # 默认为空,你可以加图标
handles: [] # 默认空数组
config_info: [] # 默认空数组
file_path: '/path/to/file' # 系统自动填文件路径
registry_type: 'device' # 自动设为设备类型
```
### handles 字段
这个是定义设备连接关系的,类似动作里的 handles 一样:
```yaml
handles: # 大多数时候都是空的,除非设备本身需要连接啥
- handler_key: device_output
label: Device Output
data_type: resource
data_source: value
data_key: default_value
```
### 其他可以配置的字段
```yaml
description: '设备的详细描述' # 写清楚设备是干啥的
icon: 'device_icon.webp' # 设备图标文件名会上传到OSS
version: '0.0.1' # 版本号
category: # 设备分类,前端会用这个分组
- 'heating'
- 'cooling'
- 'temperature_control'
config_info: # 嵌套配置,如果设备包含子设备
- children:
- opentrons_24_tuberack_nest_1point5ml_snapcap_A1
- other_nested_component
```
## 完整的例子
这里是一个比较完整的设备配置示例:
```yaml
my_temperature_controller:
class:
action_value_mappings:
heat_start:
type: HeatChillStart
goal:
target_temp: temp
vessel: vessel
goal_default:
target_temp: 25.0
vessel: ''
handles:
output:
- handler_key: heated_sample
label: Heated Sample
data_type: resource
data_source: handle
data_key: sample
placeholder_keys:
vessel: unilabos_resources
result:
status: status
success: success
schema:
description: '启动加热功能'
properties:
goal:
properties:
target_temp:
type: number
description: '目标温度'
vessel:
type: string
description: '容器标识'
required:
- target_temp
- vessel
title: HeatStart_Goal
type: object
required:
- goal
title: HeatStart
type: object
feedback: {}
stop:
type: UniLabJsonCommand
goal: {}
goal_default: {}
handles: {}
result:
status: status
schema:
description: '停止设备'
properties:
goal:
type: object
title: Stop_Goal
title: Stop
type: object
feedback: {}
module: unilabos.devices.temperature.my_controller:MyTemperatureController
status_types:
current_temperature: float
target_temperature: float
is_heating: bool
is_cooling: bool
status: str
vessel: str
type: python
description: '我的温度控制器设备'
handles: []
icon: 'temperature_controller.webp'
init_param_schema:
config:
properties:
port:
default: '/dev/ttyUSB0'
type: string
baudrate:
default: 9600
type: number
required: []
type: object
data:
properties:
current_temperature:
type: number
target_temperature:
type: number
is_heating:
type: boolean
is_cooling:
type: boolean
status:
type: string
vessel:
type: string
required:
- current_temperature
- target_temperature
- status
type: object
version: '1.0.0'
category:
- 'temperature_control'
- 'heating'
config_info: []
```
## 怎么部署和使用
### 方法一:用编辑器(推荐)
1. 先写好你的 Python 驱动类
2. 用注册表编辑器自动生成 yaml 配置
3. 把生成的文件保存到 `devices/` 目录
4. 重启 UniLabOS 就能用了
### 方法二:手动写(简化版)
1. 创建最简配置:
```yaml
# devices/my_device.yaml
my_device:
class:
module: unilabos.devices.my_module.my_device:MyDevice
type: python
```
2. 启动系统时用 `complete_registry=True` 参数,让系统自动补全
3. 检查一下生成的配置是不是你想要的
### Python 驱动类要怎么写
你的设备类要符合这些要求:
```python
from unilabos.common.device_base import DeviceBase
class MyDevice(DeviceBase):
def __init__(self, config):
"""初始化,参数会自动分析到 init_param_schema.config"""
super().__init__(config)
self.port = config.get('port', '/dev/ttyUSB0')
# 状态方法(会自动生成到 status_types
def get_status(self):
"""返回设备状态"""
return "idle"
def get_temperature(self):
"""返回当前温度"""
return 25.0
# 动作方法(会自动生成 auto- 开头的动作)
async def start_heating(self, temperature: float):
"""开始加热到指定温度"""
pass
def stop(self):
"""停止操作"""
pass
```
### 系统集成
1. 把 yaml 文件放到 `devices/` 目录下
2. 系统启动时会自动扫描并加载设备
3. 系统会自动补全所有缺失的字段
4. 设备马上就能在前端界面中使用
### 高级配置
如果需要特殊设置,可以手动加:
```yaml
my_device:
class:
module: unilabos.devices.my_module.my_device:MyDevice
type: python
action_value_mappings:
# 自定义动作
special_command:
type: UniLabJsonCommand
goal: {}
result: {}
# 可选的自定义配置
description: '我的特殊设备'
icon: 'my_device.webp'
category: ['temperature', 'heating']
```
## 常见问题怎么排查
### 设备加载不了
1. 检查模块路径:确认 `class.module` 路径写对了
2. 确认类能导入:看看你的 Python 驱动类能不能正常导入
3. 检查语法:用 yaml 验证器看看文件格式对不对
4. 查看日志:看 UniLabOS 启动时有没有报错信息
### 自动生成失败了
1. 类分析出问题:确认你的类继承了正确的基类
2. 方法类型不明确:确保状态方法的返回类型写清楚了
3. 导入有问题:检查类能不能被动态导入
4. 没开完整注册:确认启用了 `complete_registry=True`
### 前端显示有问题
1. 重新生成:删掉旧的 yaml 文件,用编辑器重新生成
2. 清除缓存:清除浏览器缓存,重新加载页面
3. 检查字段:确认必需的字段(比如 `schema`)都有
4. 验证数据:检查 `goal_default``schema` 的数据类型是不是一致
### 动作执行出错
1. 方法名不对:确认动作方法名符合规范(比如 `execute_<action_name>`
2. 参数映射错误:检查 `goal` 字段的参数映射是否正确
3. 返回格式不对:确认方法返回值格式符合 `result` 映射
4. 没异常处理:在驱动类里加上异常处理
## 最佳实践
### 开发流程
1. **优先使用编辑器**:除非有特殊需求,否则优先使用注册表编辑器
2. **最小化配置**:手动配置时只定义必要字段,让系统自动生成其他内容
3. **增量开发**:先创建基本配置,后续根据需要添加特殊动作
### 代码规范
1. **方法命名**:状态方法使用 `get_` 前缀,动作方法使用动词开头
2. **类型注解**:为方法参数和返回值添加类型注解
3. **文档字符串**:为类和方法添加详细的文档字符串
4. **异常处理**:实现完善的错误处理和日志记录
### 配置管理
1. **版本控制**:所有 yaml 文件纳入版本控制
2. **命名一致性**:设备 ID、文件名、类名保持一致的命名风格
3. **定期更新**:定期运行完整注册以更新自动生成的字段
4. **备份配置**:在修改前备份重要的手动配置
### 测试验证
1. **本地测试**:在本地环境充分测试后再部署
2. **渐进部署**:先部署到测试环境,验证无误后再上生产环境
3. **监控日志**:密切监控设备加载和运行日志
4. **回滚准备**:准备快速回滚机制,以应对紧急情况
### 性能优化
1. **按需加载**:只加载实际使用的设备类型
2. **缓存利用**:充分利用系统的注册表缓存机制
3. **资源管理**:合理管理设备连接和资源占用
4. **监控指标**:设置关键性能指标的监控和告警

View File

@@ -1,82 +1,73 @@
# Uni-Lab 配置指南
Uni-Lab支持通过Python配置文件进行灵活的系统配置。本指南将帮助您理解配置选项并设置您的Uni-Lab环境。
Uni-Lab 支持通过 Python 配置文件进行灵活的系统配置。本指南将帮助您理解配置选项并设置您的 Uni-Lab 环境。
## 配置文件格式
Uni-Lab支持Python格式的配置文件它比YAMLJSON提供更多的灵活性包括支持注释、条件逻辑和复杂数据结构。
Uni-Lab 支持 Python 格式的配置文件,它比 YAMLJSON 提供更多的灵活性,包括支持注释、条件逻辑和复杂数据结构。
### 基本配置示例
### 默认配置示例
一个典型的配置文件包含以下部分
首次使用时,系统会自动创建一个基础配置文件 `local_config.py`
```python
# unilabos的配置文件
class BasicConfig:
ak = "" # 实验室网页给您提供的ak代码您可以在配置文件中指定也可以通过运行unilabos时以 --ak 传入,优先按照传入参数解析
sk = "" # 实验室网页给您提供的sk代码您可以在配置文件中指定也可以通过运行unilabos时以 --sk 传入,优先按照传入参数解析
# WebSocket配置一般无需调整
class WSConfig:
reconnect_interval = 5 # 重连间隔(秒)
max_reconnect_attempts = 999 # 最大重连次数
ping_interval = 30 # ping间隔
```
### 完整配置示例
您可以根据需要添加更多配置选项:
```python
#!/usr/bin/env python
# coding=utf-8
"""Uni-Lab 配置文件"""
from dataclasses import dataclass
# 基础配置
class BasicConfig:
ak = "your_access_key" # 实验室访问密钥
sk = "your_secret_key" # 实验室私钥
working_dir = "" # 工作目录(通常自动设置)
config_path = "" # 配置文件路径(自动设置)
is_host_mode = True # 是否为主站模式
slave_no_host = False # 从站模式下是否跳过等待主机服务
upload_registry = False # 是否上传注册表
machine_name = "undefined" # 机器名称(自动获取)
vis_2d_enable = False # 是否启用2D可视化
enable_resource_load = True # 是否启用资源加载
communication_protocol = "websocket" # 通信协议
# 配置类定义
# WebSocket配置
class WSConfig:
reconnect_interval = 5 # 重连间隔(秒)
max_reconnect_attempts = 999 # 最大重连次数
ping_interval = 30 # ping间隔
class MQConfig:
"""MQTT 配置类"""
lab_id: str = "YOUR_LAB_ID"
# 更多配置...
# OSS上传配置
class OSSUploadConfig:
api_host = "" # API主机地址
authorization = "" # 授权信息
init_endpoint = "" # 初始化端点
complete_endpoint = "" # 完成端点
max_retries = 3 # 最大重试次数
# 其他配置类...
```
## 配置选项说明
### MQTT配置 (MQConfig)
MQTT配置用于连接消息队列服务是Uni-Lab与云端通信的主要方式。
```python
class MQConfig:
"""MQTT 配置类"""
lab_id: str = "7AAEDBEA" # 实验室唯一标识
instance_id: str = "mqtt-cn-instance"
access_key: str = "your-access-key"
secret_key: str = "your-secret-key"
group_id: str = "GID_labs"
broker_url: str = "mqtt-cn-instance.mqtt.aliyuncs.com"
port: int = 8883
# 可以直接提供证书文件路径
ca_file: str = "/path/to/ca.pem" # 相对config.py所在目录的路径
cert_file: str = "/path/to/cert.pem" # 相对config.py所在目录的路径
key_file: str = "/path/to/key.pem" # 相对config.py所在目录的路径
# 或者直接提供证书内容
ca_content: str = ""
cert_content: str = ""
key_content: str = ""
```
#### 证书配置
MQTT连接支持两种方式配置证书
1. **文件路径方式**(推荐):指定证书文件的路径,系统会自动读取文件内容
2. **直接内容方式**:直接在配置中提供证书内容
推荐使用文件路径方式,便于证书的更新和管理。
### HTTP客户端配置 (HTTPConfig)
即将开放 Uni-Lab 云端实验室。
### ROS模块配置 (ROSConfig)
配置ROS消息转换器需要加载的模块
```python
# HTTP配置
class HTTPConfig:
remote_addr = "http://127.0.0.1:48197/api/v1" # 远程地址
# ROS配置
class ROSConfig:
"""ROS模块配置"""
modules = [
"std_msgs.msg",
"geometry_msgs.msg",
@@ -85,25 +76,365 @@ class ROSConfig:
"nav2_msgs.action",
"unilabos_msgs.msg",
"unilabos_msgs.action",
] # 需要加载的ROS模块
```
## 命令行参数覆盖配置
Uni-Lab 允许通过命令行参数覆盖配置文件中的设置,提供更灵活的配置方式。命令行参数的优先级高于配置文件。
### 支持命令行覆盖的配置项
以下配置项可以通过命令行参数进行覆盖:
| 配置类 | 配置字段 | 命令行参数 | 说明 |
| ------------- | ----------------- | ------------------- | -------------------------------- |
| `BasicConfig` | `ak` | `--ak` | 实验室访问密钥 |
| `BasicConfig` | `sk` | `--sk` | 实验室私钥 |
| `BasicConfig` | `working_dir` | `--working_dir` | 工作目录路径 |
| `BasicConfig` | `is_host_mode` | `--is_slave` | 主站模式(参数为从站模式,取反) |
| `BasicConfig` | `slave_no_host` | `--slave_no_host` | 从站模式下跳过等待主机服务 |
| `BasicConfig` | `upload_registry` | `--upload_registry` | 启动时上传注册表信息 |
| `BasicConfig` | `vis_2d_enable` | `--2d_vis` | 启用 2D 可视化 |
| `HTTPConfig` | `remote_addr` | `--addr` | 远程服务地址 |
### 特殊命令行参数
除了直接覆盖配置项的参数外,还有一些特殊的命令行参数:
| 参数 | 说明 |
| ------------------- | ------------------------------------ |
| `--config` | 指定配置文件路径 |
| `--port` | Web 服务端口(不影响配置文件) |
| `--disable_browser` | 禁用自动打开浏览器(不影响配置文件) |
| `--visual` | 可视化工具选择(不影响配置文件) |
| `--skip_env_check` | 跳过环境检查(不影响配置文件) |
### 配置优先级
配置项的生效优先级从高到低为:
1. **命令行参数**:最高优先级
2. **环境变量**:中等优先级
3. **配置文件**:基础优先级
### 使用示例
```bash
# 通过命令行覆盖认证信息
unilab --ak "new_access_key" --sk "new_secret_key"
# 覆盖服务器地址
unilab --addr "https://custom.server.com/api/v1"
# 启用从站模式并跳过等待主机
unilab --is_slave --slave_no_host
# 启用上传注册表和2D可视化
unilab --upload_registry --2d_vis
# 组合使用多个覆盖参数
unilab --ak "key" --sk "secret" --addr "test" --upload_registry --2d_vis
```
### 预设环境地址
`--addr` 参数支持以下预设值,会自动转换为对应的完整 URL
- `test``https://uni-lab.test.bohrium.com/api/v1`
- `uat``https://uni-lab.uat.bohrium.com/api/v1`
- `local``http://127.0.0.1:48197/api/v1`
- 其他值 → 直接使用作为完整 URL
## 配置选项详解
### 基础配置 (BasicConfig)
基础配置包含了系统运行的核心参数:
| 参数 | 类型 | 默认值 | 说明 |
| ------------------------ | ---- | ------------- | ------------------------------------------ |
| `ak` | str | `""` | 实验室访问密钥(必需) |
| `sk` | str | `""` | 实验室私钥(必需) |
| `working_dir` | str | `""` | 工作目录,通常自动设置 |
| `is_host_mode` | bool | `True` | 是否为主站模式 |
| `slave_no_host` | bool | `False` | 从站模式下是否跳过等待主机服务 |
| `upload_registry` | bool | `False` | 启动时是否上传注册表信息 |
| `machine_name` | str | `"undefined"` | 机器名称,自动从 hostname 获取(不可配置) |
| `vis_2d_enable` | bool | `False` | 是否启用 2D 可视化 |
| `communication_protocol` | str | `"websocket"` | 通信协议,固定为 websocket |
#### 认证配置
`ak``sk` 是必需的认证参数:
1. **获取方式**:在 [Uni-Lab 官网](https://uni-lab.bohrium.com) 注册实验室后获得
2. **配置方式**
- **命令行参数**`--ak "your_key" --sk "your_secret"`(最高优先级)
- **配置文件**:在 `BasicConfig` 类中设置
- **环境变量**`UNILABOS_BASICCONFIG_AK``UNILABOS_BASICCONFIG_SK`
3. **优先级顺序**:命令行参数 > 环境变量 > 配置文件
4. **安全注意**:请妥善保管您的密钥信息
**推荐做法**
- 开发环境:使用配置文件
- 生产环境:使用环境变量或命令行参数
- 临时测试:使用命令行参数
### WebSocket 配置 (WSConfig)
WebSocket 是 Uni-Lab 的主要通信方式:
| 参数 | 类型 | 默认值 | 说明 |
| ------------------------ | ---- | ------ | ------------------ |
| `reconnect_interval` | int | `5` | 断线重连间隔(秒) |
| `max_reconnect_attempts` | int | `999` | 最大重连次数 |
| `ping_interval` | int | `30` | 心跳检测间隔(秒) |
### HTTP 配置 (HTTPConfig)
HTTP 客户端配置用于与云端服务通信:
| 参数 | 类型 | 默认值 | 说明 |
| ------------- | ---- | --------------------------------- | ------------ |
| `remote_addr` | str | `"http://127.0.0.1:48197/api/v1"` | 远程服务地址 |
**预设环境地址**
- 生产环境:`https://uni-lab.bohrium.com/api/v1`
- 测试环境:`https://uni-lab.test.bohrium.com/api/v1`
- UAT 环境:`https://uni-lab.uat.bohrium.com/api/v1`
- 本地环境:`http://127.0.0.1:48197/api/v1`
### ROS 配置 (ROSConfig)
配置 ROS 消息转换器需要加载的模块:
```python
class ROSConfig:
modules = [
"std_msgs.msg", # 标准消息类型
"geometry_msgs.msg", # 几何消息类型
"control_msgs.msg", # 控制消息类型
"control_msgs.action", # 控制动作类型
"nav2_msgs.action", # 导航动作类型
"unilabos_msgs.msg", # UniLab 自定义消息类型
"unilabos_msgs.action", # UniLab 自定义动作类型
]
```
您可以根据需要添加其他ROS模块。
您可以根据实际使用的设备和功能添加其他 ROS 模块。
### 其他配置选项
### OSS 上传配置 (OSSUploadConfig)
- **OSSUploadConfig**: 对象存储上传配置
对象存储服务配置,用于文件上传功能:
## 如何使用配置文件
| 参数 | 类型 | 默认值 | 说明 |
| ------------------- | ---- | ------ | -------------------- |
| `api_host` | str | `""` | OSS API 主机地址 |
| `authorization` | str | `""` | 授权认证信息 |
| `init_endpoint` | str | `""` | 上传初始化端点 |
| `complete_endpoint` | str | `""` | 上传完成端点 |
| `max_retries` | int | `3` | 上传失败最大重试次数 |
启动Uni-Lab时通过`--config`参数指定配置文件路径:
## 环境变量支持
Uni-Lab 支持通过环境变量覆盖配置文件中的设置。环境变量格式为:
```
UNILABOS_{配置类名}_{字段名}
```
### 环境变量示例
```bash
unilab --config path/to/your/config.py
# 设置基础配置
export UNILABOS_BASICCONFIG_AK="your_access_key"
export UNILABOS_BASICCONFIG_SK="your_secret_key"
export UNILABOS_BASICCONFIG_IS_HOST_MODE="true"
# 设置WebSocket配置
export UNILABOS_WSCONFIG_RECONNECT_INTERVAL="10"
export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS="500"
# 设置HTTP配置
export UNILABOS_HTTPCONFIG_REMOTE_ADDR="https://uni-lab.bohrium.com/api/v1"
```
如果您不涉及多环境开发可以在unilabos的安装路径中手动添加local_config.py的文件
### 环境变量类型转换
# 启动Uni-Lab
python -m unilabos.app.main --config path/to/your/config.py
- **布尔值**`"true"`, `"1"`, `"yes"``True`;其他 → `False`
- **整数**:自动转换为 `int` 类型
- **浮点数**:自动转换为 `float` 类型
- **字符串**:保持原值
## 配置文件使用方法
### 1. 指定配置文件启动
```bash
# 使用指定配置文件启动
unilab --config /path/to/your/config.py
```
### 2. 使用默认配置文件
如果不指定配置文件,系统会按以下顺序查找:
1. 环境变量 `UNILABOS_BASICCONFIG_CONFIG_PATH` 指定的路径
2. 工作目录下的 `local_config.py`
3. 首次使用时会引导创建配置文件
### 3. 配置文件验证
系统启动时会自动验证配置文件:
- **语法检查**:确保 Python 语法正确
- **类型检查**:验证配置项类型是否匹配
- **必需项检查**:确保 `ak``sk` 已配置
## 最佳实践
### 1. 安全配置
- 不要将包含密钥的配置文件提交到版本控制系统
- 使用环境变量或命令行参数在生产环境中配置敏感信息
- 定期更换访问密钥
- **推荐配置方式**
```bash
# 生产环境 - 使用环境变量
export UNILABOS_BASICCONFIG_AK="your_access_key"
export UNILABOS_BASICCONFIG_SK="your_secret_key"
unilab
# 或使用命令行参数
unilab --ak "your_access_key" --sk "your_secret_key"
```
### 2. 多环境配置
为不同环境创建不同的配置文件并结合命令行参数:
```
configs/
├── local_config.py # 本地开发
├── test_config.py # 测试环境
├── prod_config.py # 生产环境
└── example_config.py # 示例配置
```
**环境切换示例**
```bash
# 本地开发环境
unilab --config configs/local_config.py --addr local
# 测试环境
unilab --config configs/test_config.py --addr test --upload_registry
# 生产环境
unilab --config configs/prod_config.py --ak "$PROD_AK" --sk "$PROD_SK"
```
### 3. 配置管理
- 保持配置文件简洁,只包含需要修改的配置项
- 为配置项添加注释说明其作用
- 定期检查和更新配置文件
- **命令行参数优先使用场景**
- 临时测试不同配置
- CI/CD 流水线中的动态配置
- 不同环境间快速切换
- 敏感信息的安全传递
### 4. 灵活配置策略
**基础配置文件 + 命令行覆盖**的推荐方式:
```python
# base_config.py - 基础配置
class BasicConfig:
# 非敏感配置写在文件中
is_host_mode = True
upload_registry = False
vis_2d_enable = False
class WSConfig:
reconnect_interval = 5
max_reconnect_attempts = 999
ping_interval = 30
```
```bash
# 启动时通过命令行覆盖关键参数
unilab --config base_config.py \
--ak "$AK" \
--sk "$SK" \
--addr "test" \
--upload_registry \
--2d_vis
```
## 故障排除
### 1. 配置文件加载失败
**错误信息**`[ENV] 配置文件 xxx 不存在`
**解决方法**
- 确认配置文件路径正确
- 检查文件权限是否可读
- 确保配置文件是 `.py` 格式
### 2. 语法错误
**错误信息**`[ENV] 加载配置文件 xxx 失败`
**解决方法**
- 检查 Python 语法是否正确
- 确认类名和字段名拼写正确
- 验证缩进是否正确(使用空格而非制表符)
### 3. 认证失败
**错误信息**`后续运行必须拥有一个实验室`
**解决方法**
- 确认 `ak` 和 `sk` 已正确配置
- 检查密钥是否有效
- 确认网络连接正常
### 4. 环境变量不生效
**解决方法**
- 确认环境变量名格式正确(`UNILABOS_CLASS_FIELD`
- 检查环境变量是否已正确设置
- 重启系统或重新加载环境变量
### 5. 命令行参数不生效
**错误现象**:设置了命令行参数但配置没有生效
**解决方法**
- 确认参数名拼写正确(如 `--ak` 而不是 `--access_key`
- 检查参数格式是否正确(布尔参数如 `--is_slave` 不需要值)
- 确认参数位置正确(所有参数都应在 `unilab` 之后)
- 查看启动日志确认参数是否被正确解析
### 6. 配置优先级混淆
**错误现象**:不确定哪个配置生效
**解决方法**
- 记住优先级:命令行参数 > 环境变量 > 配置文件
- 使用 `--ak` 和 `--sk` 参数时会看到提示信息
- 检查启动日志中的配置加载信息
- 临时移除低优先级配置来测试高优先级配置是否生效

View File

@@ -1,4 +1,4 @@
# Uni-Lab 启动
# Uni-Lab 启动指南
安装完毕后,可以通过 `unilab` 命令行启动:
@@ -8,70 +8,240 @@ Start Uni-Lab Edge server.
options:
-h, --help show this help message and exit
-g GRAPH, --graph GRAPH
Physical setup graph.
-d DEVICES, --devices DEVICES
Devices config file.
-r RESOURCES, --resources RESOURCES
Resources config file.
Physical setup graph file path.
-c CONTROLLERS, --controllers CONTROLLERS
Controllers config file.
Controllers config file path.
--registry_path REGISTRY_PATH
Path to the registry
Path to the registry directory
--working_dir WORKING_DIR
Path to the working directory
--backend {ros,simple,automancer}
Choose the backend to run with: 'ros', 'simple', or 'automancer'.
--app_bridges APP_BRIDGES [APP_BRIDGES ...]
Bridges to connect to. Now support 'mqtt' and 'fastapi'.
--without_host Run the backend as slave (without host).
--config CONFIG Configuration file path for system settings
Bridges to connect to. Now support 'websocket' and 'fastapi'.
--is_slave Run the backend as slave node (without host privileges).
--slave_no_host Skip waiting for host service in slave mode
--upload_registry Upload registry information when starting unilab
--use_remote_resource Use remote resources when starting unilab
--config CONFIG Configuration file path, supports .py format Python config files
--port PORT Port for web service information page
--disable_browser Disable opening information page on startup
--2d_vis Enable 2D visualization when starting pylabrobot instance
--visual {rviz,web,disable}
Choose visualization tool: rviz, web, or disable
--ak AK Access key for laboratory requests
--sk SK Secret key for laboratory requests
--addr ADDR Laboratory backend address
--skip_env_check Skip environment dependency check on startup
--complete_registry Complete registry information
```
## 启动流程详解
Uni-Lab 的启动过程分为以下几个阶段:
### 1. 参数解析阶段
- 解析命令行参数
- 处理参数格式转换(支持 dash 和 underscore 格式)
### 2. 环境检查阶段 (可选)
- 默认进行环境依赖检查并自动安装必需包
- 使用 `--skip_env_check` 可跳过此步骤
### 3. 配置文件处理阶段
您可以直接跟随 unilabos 的提示进行,无需查阅本节
- **工作目录设置**
- 如果当前目录以 `unilabos_data` 结尾,则使用当前目录
- 否则使用 `当前目录/unilabos_data` 作为工作目录
- 可通过 `--working_dir` 指定自定义工作目录
- **配置文件查找顺序**
1. 使用 `--config` 参数指定的配置文件
2. 在工作目录中查找 `local_config.py`
3. 首次使用时会引导创建配置文件
### 4. 服务器地址配置
支持多种后端环境:
- `--addr test`:测试环境 (`https://uni-lab.test.bohrium.com/api/v1`)
- `--addr uat`UAT 环境 (`https://uni-lab.uat.bohrium.com/api/v1`)
- `--addr local`:本地环境 (`http://127.0.0.1:48197/api/v1`)
- 自定义地址:直接指定完整 URL
### 5. 认证配置
- **必需参数**`--ak``--sk` 必须同时提供
- 命令行参数优先于配置文件中的设置
- 未提供认证信息会导致启动失败并提示注册实验室
### 6. 设备图谱加载
支持两种方式:
- **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式)
- **远程资源**:使用 `--use_remote_resource` 从云端获取
### 7. 注册表构建
- 构建设备和资源注册表
- 支持自定义注册表路径 (`--registry_path`)
- 可选择补全注册表信息 (`--complete_registry`)
### 8. 设备验证和注册
- 验证设备连接和端点配置
- 自动注册设备到云端服务
### 9. 通信桥接配置
- **WebSocket**:实时通信和任务下发
- **FastAPI**HTTP API 服务和物料更新
### 10. 可视化和服务启动
- 可选启动可视化工具 (`--visual`)
- 启动 Web 信息服务 (默认端口 8002)
- 启动后端通信服务
## 使用配置文件
Uni-Lab支持使用Python格式的配置文件进行系统设置。通过 `--config` 参数指定配置文件路径:
Uni-Lab 支持使用 Python 格式的配置文件进行系统设置。通过 `--config` 参数指定配置文件路径:
```bash
# 使用配置文件启动
unilab --config path/to/your/config.py
```
配置文件包含MQTT、HTTP、ROS等系统设置。有关配置文件的详细信息,请参阅[配置指南](configuration.md)。
配置文件包含实验室和 WebSocket 连接等设置。有关配置文件的详细信息,请参阅[配置指南](configuration.md)。
## 初始化信息来源
启动 Uni-Lab 时,可以选用两种方式之一配置实验室设备、耗材、通信、控制逻辑
启动 Uni-Lab 时,可以选用两种方式之一配置实验室设备:
### 1. 组态&拓扑图
使用 `-g` 时,组态&拓扑图应包含实验室所有信息,详见{ref}`graph`。目前支持 graphml 和 node-link json 两种格式。格式可参照 `tests/experiments` 下的启动文件。
使用 `-g` 时,组态&拓扑图应包含实验室所有信息,详见{ref}`graph`。目前支持 GraphML 和 node-link JSON 两种格式。格式可参照 `tests/experiments` 下的启动文件。
### 2. 分别指定设备、耗材、控制逻辑
### 2. 分别指定控制逻辑
分别使用 `-d, -r, -c` 依次传入设备组态配置、耗材列表、控制逻辑。
使用 `-c` 传入控制逻辑配置
可参照 `devices.json``resources.json`
不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--registry` 加入 `<your-registry-path>/{devices,device_comms,resources}`
不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--registry_path` 加入 `<your-registry-path>/{devices,device_comms,resources}`
## 通信中间件 `--backend`
目前 Uni-Lab 支持 ros2 作为通信中间件
目前 Uni-Lab 支持以下通信中间件
- **ros** (默认):基于 ROS2 的通信
- **simple**:简化通信模式
- **automancer**Automancer 兼容模式
## 端云桥接 `--app_bridges`
目前 Uni-Lab 提供 FastAPI (http), MQTT 两种端云通信方式。其中默认 MQTT 负责端对云状态同步和云对端任务下发FastAPI 负责端对云物料更新。
目前 Uni-Lab 提供 WebSocket、FastAPI (http) 两种端云通信方式
- **WebSocket**:负责实时通信和任务下发
- **FastAPI**:负责端对云物料更新和 HTTP API
## 分布式组网
启动 Uni-Lab 时,加入 `--without_host` 将作为从站,不加将作为主站,主站 (host) 持有物料修改权以及对云端的通信。局域网内分别启动的 Uni-Lab 主站/从站将自动组网,互相能访问所有设备状态、传感器信息并发送指令。
启动 Uni-Lab 时,加入 `--is_slave` 将作为从站,不加将作为主站
- **主站 (host)**:持有物料修改权以及对云端的通信
- **从站 (slave)**:无主机权限,可选择跳过等待主机服务 (`--slave_no_host`)
局域网内分别启动的 Uni-Lab 主站/从站将自动组网,互相能访问所有设备状态、传感器信息并发送指令。
## 可视化选项
### 2D 可视化
使用 `--2d_vis` 在 PyLabRobot 实例启动时同时启动 2D 可视化。
### 3D 可视化
通过 `--visual` 参数选择:
- **rviz**:使用 RViz 进行 3D 可视化
- **web**:使用 Web 界面进行可视化
- **disable** (默认):禁用可视化
## 实验室管理
### 首次使用
如果是首次使用,系统会:
1. 提示前往 https://uni-lab.bohrium.com 注册实验室
2. 引导创建配置文件
3. 设置工作目录
### 认证设置
- `--ak`:实验室访问密钥
- `--sk`:实验室私钥
- 两者必须同时提供才能正常启动
## 完整启动示例
以下是一些常用的启动命令示例:
```bash
# 使用配置文件和组态图启动
unilab -g path/to/graph.json
# 使用组态图启动,上传注册表
unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry
# 使用配置文件和分离的设备/资源文件启动
unilab -d devices.json -r resources.json
# 使用远程资源启动
unilab --ak your_ak --sk your_sk --use_remote_resource
# 更新注册表
unilab --ak your_ak --sk your_sk --complete_registry
# 启动从站模式
unilab --ak your_ak --sk your_sk --is_slave
# 启用可视化
unilab --ak your_ak --sk your_sk --visual web --2d_vis
# 指定本地信息网页服务端口和禁用自动跳出浏览器
unilab --ak your_ak --sk your_sk --port 8080 --disable_browser
```
## 常见问题
### 1. 认证失败
如果提示 "后续运行必须拥有一个实验室",请确保:
- 已在 https://uni-lab.bohrium.com 注册实验室
- 正确设置了 `--ak``--sk` 参数
- 配置文件中包含正确的认证信息
### 2. 配置文件问题
如果配置文件加载失败:
- 确保配置文件是 `.py` 格式
- 检查配置文件语法是否正确
- 首次使用可让系统自动创建示例配置文件
### 3. 网络连接问题
如果无法连接到服务器:
- 检查网络连接
- 确认服务器地址是否正确
- 尝试使用不同的环境地址test、uat、local
### 4. 设备图谱问题
如果设备加载失败:
- 检查图谱文件格式是否正确
- 验证设备连接和端点配置
- 确保注册表路径正确

View File

@@ -1,6 +1,6 @@
package:
name: ros-humble-unilabos-msgs
version: 0.10.3
version: 0.10.5
source:
path: ../../unilabos_msgs
target_directory: src

View File

@@ -1,6 +1,6 @@
package:
name: unilabos
version: "0.10.3"
version: "0.10.5"
source:
path: ../..

View File

@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup(
name=package_name,
version='0.10.3',
version='0.10.5',
packages=find_packages(),
include_package_data=True,
install_requires=['setuptools'],
@@ -16,8 +16,7 @@ setup(
tests_require=['pytest'],
entry_points={
'console_scripts': [
"unilab = unilabos.app.main:main",
"unilab-register = unilabos.app.register:main"
"unilab = unilabos.app.main:main"
],
},
)

View File

@@ -15,24 +15,33 @@ def start_backend(
without_host: bool = False,
visual: str = "None",
resources_mesh_config: dict = {},
**kwargs
**kwargs,
):
if backend == "ros":
# 假设 ros_main, simple_main, automancer_main 是不同 backend 的启动函数
from unilabos.ros.main_slave_run import main, slave # 如果选择 'ros' 作为 backend
elif backend == 'simple':
elif backend == "simple":
# 这里假设 simple_backend 和 automancer_backend 是你定义的其他两个后端
# from simple_backend import main as simple_main
pass
elif backend == 'automancer':
elif backend == "automancer":
# from automancer_backend import main as automancer_main
pass
else:
raise ValueError(f"Unsupported backend: {backend}")
backend_thread = threading.Thread(
target=main if not without_host else slave,
args=(devices_config, resources_config, resources_edge_config, graph, controllers_config, bridges, visual, resources_mesh_config),
args=(
devices_config,
resources_config,
resources_edge_config,
graph,
controllers_config,
bridges,
visual,
resources_mesh_config,
),
name="backend_thread",
daemon=True,
)

View File

@@ -3,7 +3,7 @@
"""
通信模块
提供MQTT和WebSocket的统一接口支持通过配置选择通信协议。
提供WebSocket的统一接口支持通过配置选择通信协议。
包含通信抽象层基类和通信客户端工厂。
"""
@@ -17,7 +17,7 @@ class BaseCommunicationClient(ABC):
"""
通信客户端抽象基类
定义了所有通信客户端(MQTT、WebSocket等需要实现的接口。
定义了所有通信客户端WebSocket等需要实现的接口。
"""
def __init__(self):
@@ -121,14 +121,12 @@ class CommunicationClientFactory:
protocol = protocol.lower()
if protocol == "mqtt":
return cls._create_mqtt_client()
elif protocol == "websocket":
if protocol == "websocket":
return cls._create_websocket_client()
else:
logger.error(f"[CommunicationFactory] Unsupported protocol: {protocol}")
logger.warning(f"[CommunicationFactory] Falling back to MQTT")
return cls._create_mqtt_client()
logger.warning(f"[CommunicationFactory] Falling back to WebSocket")
return cls._create_websocket_client()
@classmethod
def get_client(cls, protocol: Optional[str] = None) -> BaseCommunicationClient:
@@ -147,26 +145,16 @@ class CommunicationClientFactory:
return cls._client_cache
@classmethod
def _create_mqtt_client(cls) -> BaseCommunicationClient:
"""创建MQTT客户端"""
try:
from unilabos.app.mq import mqtt_client
return mqtt_client
except Exception as e:
logger.error(f"[CommunicationFactory] Failed to create MQTT client: {str(e)}")
raise
@classmethod
def _create_websocket_client(cls) -> BaseCommunicationClient:
"""创建WebSocket客户端"""
try:
from unilabos.app.ws_client import WebSocketClient
return WebSocketClient()
except Exception as e:
logger.error(f"[CommunicationFactory] Failed to create WebSocket client: {str(e)}")
logger.warning(f"[CommunicationFactory] Falling back to MQTT")
return cls._create_mqtt_client()
raise
@classmethod
def reset_client(cls):
@@ -188,7 +176,7 @@ class CommunicationClientFactory:
Returns:
支持的协议列表
"""
return ["mqtt", "websocket"]
return ["websocket"]
def get_communication_client(protocol: Optional[str] = None) -> BaseCommunicationClient:

View File

@@ -51,14 +51,14 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
parser.add_argument("-g", "--graph", help="Physical setup graph.")
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file.")
parser.add_argument("-g", "--graph", help="Physical setup graph file path.")
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file path.")
parser.add_argument(
"--registry_path",
type=str,
default=None,
action="append",
help="Path to the registry",
help="Path to the registry directory",
)
parser.add_argument(
"--working_dir",
@@ -75,84 +75,85 @@ def parse_args():
parser.add_argument(
"--app_bridges",
nargs="+",
default=["mqtt", "fastapi"],
help="Bridges to connect to. Now support 'mqtt' and 'fastapi'.",
default=["websocket", "fastapi"],
help="Bridges to connect to. Now support 'websocket' and 'fastapi'.",
)
parser.add_argument(
"--without_host",
"--is_slave",
action="store_true",
help="Run the backend as slave (without host).",
help="Run the backend as slave node (without host privileges).",
)
parser.add_argument(
"--slave_no_host",
action="store_true",
help="Slave模式下跳过等待host服务",
help="Skip waiting for host service in slave mode",
)
parser.add_argument(
"--upload_registry",
action="store_true",
help="启动unilab时同时报送注册表信息",
help="Upload registry information when starting unilab",
)
parser.add_argument(
"--use_remote_resource",
action="store_true",
help="启动unilab时使用远程资源启动",
help="Use remote resources when starting unilab",
)
parser.add_argument(
"--config",
type=str,
default=None,
help="配置文件路径,支持.py格式的Python配置文件",
help="Configuration file path, supports .py format Python config files",
)
parser.add_argument(
"--port",
type=int,
default=8002,
help="信息页web服务的启动端口",
help="Port for web service information page",
)
parser.add_argument(
"--disable_browser",
action="store_true",
help="是否在启动时关闭信息页",
help="Disable opening information page on startup",
)
parser.add_argument(
"--2d_vis",
action="store_true",
help="是否在pylabrobot实例启动时同时启动可视化",
help="Enable 2D visualization when starting pylabrobot instance",
)
parser.add_argument(
"--visual",
choices=["rviz", "web", "disable"],
default="disable",
help="选择可视化工具: rviz, web",
help="Choose visualization tool: rviz, web, or disable",
)
parser.add_argument(
"--ak",
type=str,
default="",
help="实验室请求的ak",
help="Access key for laboratory requests",
)
parser.add_argument(
"--sk",
type=str,
default="",
help="实验室请求的sk",
help="Secret key for laboratory requests",
)
parser.add_argument(
"--addr",
type=str,
default="https://uni-lab.bohrium.com/api/v1",
help="实验室后端地址",
)
parser.add_argument(
"--websocket",
action="store_true",
help="使用websocket而非mqtt作为通信协议",
help="Laboratory backend address",
)
parser.add_argument(
"--skip_env_check",
action="store_true",
help="跳过启动时的环境依赖检查",
help="Skip environment dependency check on startup",
)
parser.add_argument(
"--complete_registry",
action="store_true",
default=False,
help="Complete registry information",
)
return parser
@@ -237,13 +238,17 @@ def main():
print_status("远程资源不存在,本地将进行首次上报!", "info")
# 设置BasicConfig参数
BasicConfig.ak = args_dict.get("ak", "")
BasicConfig.sk = args_dict.get("sk", "")
if args_dict.get("ak", ""):
BasicConfig.ak = args_dict.get("ak", "")
print_status("传入了ak参数优先采用传入参数", "info")
if args_dict.get("sk", ""):
BasicConfig.sk = args_dict.get("sk", "")
print_status("传入了sk参数优先采用传入参数", "info")
BasicConfig.working_dir = working_dir
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
BasicConfig.is_host_mode = not args_dict.get("is_slave", False)
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
BasicConfig.communication_protocol = "websocket" if args_dict.get("websocket", False) else "mqtt"
BasicConfig.communication_protocol = "websocket"
machine_name = os.popen("hostname").read().strip()
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
BasicConfig.machine_name = machine_name
@@ -261,12 +266,19 @@ def main():
from unilabos.app.backend import start_backend
from unilabos.app.web import http_client
from unilabos.app.web import start_server
from unilabos.app.register import register_devices_and_resources
# 显示启动横幅
print_unilab_banner(args_dict)
# 注册表
lab_registry = build_registry(args_dict["registry_path"], False, args_dict["upload_registry"])
lab_registry = build_registry(
args_dict["registry_path"], args_dict.get("complete_registry", False), args_dict["upload_registry"]
)
if not BasicConfig.ak or not BasicConfig.sk:
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
os._exit(1)
if args_dict["graph"] is None:
request_startup_json = http_client.request_startup_json()
if not request_startup_json:
@@ -297,14 +309,24 @@ def main():
target_node = nodes[i["target"]]
source_handle = i["sourceHandle"]
target_handle = i["targetHandle"]
source_handler_keys = [h["handler_key"] for h in materials[source_node["class"]]["handles"] if h["io_type"] == 'source']
target_handler_keys = [h["handler_key"] for h in materials[target_node["class"]]["handles"] if h["io_type"] == 'target']
if not source_handle in source_handler_keys:
print_status(f"节点 {source_node['id']} 的source端点 {source_handle} 不存在,请检查,支持的端点 {source_handler_keys}", "error")
source_handler_keys = [
h["handler_key"] for h in materials[source_node["class"]]["handles"] if h["io_type"] == "source"
]
target_handler_keys = [
h["handler_key"] for h in materials[target_node["class"]]["handles"] if h["io_type"] == "target"
]
if source_handle not in source_handler_keys:
print_status(
f"节点 {source_node['id']} 的source端点 {source_handle} 不存在,请检查,支持的端点 {source_handler_keys}",
"error",
)
resource_edge_info.pop(edge_info - ind - 1)
continue
if not target_handle in target_handler_keys:
print_status(f"节点 {target_node['id']} 的target端点 {target_handle} 不存在,请检查,支持的端点 {target_handler_keys}", "error")
if target_handle not in target_handler_keys:
print_status(
f"节点 {target_node['id']} 的target端点 {target_handle} 不存在,请检查,支持的端点 {target_handler_keys}",
"error",
)
resource_edge_info.pop(edge_info - ind - 1)
continue
@@ -318,6 +340,19 @@ def main():
for i in args_dict["resources_config"]:
print_status(f"DeviceId: {i['id']}, Class: {i['class']}", "info")
# 设备注册到服务端 - 需要 ak 和 sk
if args_dict.get("ak") and args_dict.get("sk"):
print_status("检测到 ak 和 sk开始注册设备到服务端...", "info")
try:
register_devices_and_resources(lab_registry)
print_status("设备注册完成", "info")
except Exception as e:
print_status(f"设备注册失败: {e}", "error")
elif args_dict.get("ak") or args_dict.get("sk"):
print_status("警告ak 和 sk 必须同时提供才能注册设备到服务端", "warning")
else:
print_status("未提供 ak 和 sk跳过设备注册", "info")
if args_dict["controllers"] is not None:
args_dict["controllers_config"] = yaml.safe_load(open(args_dict["controllers"], encoding="utf-8"))
else:
@@ -325,14 +360,14 @@ def main():
args_dict["bridges"] = []
# 获取通信客户端(根据配置选择MQTT或WebSocket
# 获取通信客户端(仅支持WebSocket
comm_client = get_communication_client()
if "mqtt" in args_dict["app_bridges"]:
if "websocket" in args_dict["app_bridges"]:
args_dict["bridges"].append(comm_client)
if "fastapi" in args_dict["app_bridges"]:
args_dict["bridges"].append(http_client)
if "mqtt" in args_dict["app_bridges"]:
if "websocket" in args_dict["app_bridges"]:
def _exit(signum, frame):
comm_client.stop()

View File

@@ -1,225 +0,0 @@
import json
import time
import traceback
from typing import Optional
import uuid
import paho.mqtt.client as mqtt
import ssl
import base64
import hmac
from hashlib import sha1
import tempfile
import os
from unilabos.config.config import MQConfig
from unilabos.app.controler import job_add
from unilabos.app.model import JobAddReq
from unilabos.app.communication import BaseCommunicationClient
from unilabos.utils import logger
from unilabos.utils.type_check import TypeEncoder
from paho.mqtt.enums import CallbackAPIVersion
class MQTTClient(BaseCommunicationClient):
mqtt_disable = True
def __init__(self):
super().__init__()
self.mqtt_disable = not MQConfig.lab_id
self.is_disabled = self.mqtt_disable # 更新父类属性
self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}"
logger.info("[MQTT] Client_id: " + self.client_id)
self.client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
self._setup_callbacks()
def _setup_callbacks(self):
self.client.on_log = self._on_log
self.client.on_connect = self._on_connect
self.client.on_message = self._on_message
self.client.on_disconnect = self._on_disconnect
def _on_log(self, client, userdata, level, buf):
# logger.info(f"[MQTT] log: {buf}")
pass
def _on_connect(self, client, userdata, flags, rc, properties=None):
logger.info("[MQTT] Connected with result code " + str(rc))
client.subscribe(f"labs/{MQConfig.lab_id}/job/start/", 0)
client.subscribe(f"labs/{MQConfig.lab_id}/pong/", 0)
def _on_message(self, client, userdata, msg) -> None:
# logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
try:
payload_str = msg.payload.decode("utf-8")
payload_json = json.loads(payload_str)
if msg.topic == f"labs/{MQConfig.lab_id}/job/start/":
if "data" not in payload_json:
payload_json["data"] = {}
if "action" in payload_json:
payload_json["data"]["action"] = payload_json.pop("action")
if "action_type" in payload_json:
payload_json["data"]["action_type"] = payload_json.pop("action_type")
if "action_args" in payload_json:
payload_json["data"]["action_args"] = payload_json.pop("action_args")
if "action_kwargs" in payload_json:
payload_json["data"]["action_kwargs"] = payload_json.pop("action_kwargs")
job_req = JobAddReq.model_validate(payload_json)
data = job_add(job_req)
return
elif msg.topic == f"labs/{MQConfig.lab_id}/pong/":
# 处理pong响应通知HostNode
from unilabos.ros.nodes.presets.host_node import HostNode
host_instance = HostNode.get_instance(0)
if host_instance:
host_instance.handle_pong_response(payload_json)
return
except json.JSONDecodeError as e:
logger.error(f"[MQTT] JSON 解析错误: {e}")
logger.error(f"[MQTT] Raw message: {msg.payload}")
logger.error(traceback.format_exc())
except Exception as e:
logger.error(f"[MQTT] 处理消息时出错: {e}")
logger.error(traceback.format_exc())
def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None):
if rc != 0:
logger.error(f"[MQTT] Unexpected disconnection {rc}")
def _setup_ssl_context(self):
temp_files = []
try:
with tempfile.NamedTemporaryFile(mode="w", delete=False) as ca_temp:
ca_temp.write(MQConfig.ca_content)
temp_files.append(ca_temp.name)
with tempfile.NamedTemporaryFile(mode="w", delete=False) as cert_temp:
cert_temp.write(MQConfig.cert_content)
temp_files.append(cert_temp.name)
with tempfile.NamedTemporaryFile(mode="w", delete=False) as key_temp:
key_temp.write(MQConfig.key_content)
temp_files.append(key_temp.name)
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
context.load_verify_locations(cafile=temp_files[0])
context.load_cert_chain(certfile=temp_files[1], keyfile=temp_files[2])
self.client.tls_set_context(context)
finally:
for temp_file in temp_files:
try:
os.unlink(temp_file)
except Exception as e:
pass
def start(self):
if self.mqtt_disable:
logger.warning("MQTT is disabled, skipping connection.")
return
userName = f"Signature|{MQConfig.access_key}|{MQConfig.instance_id}"
password = base64.b64encode(
hmac.new(MQConfig.secret_key.encode(), self.client_id.encode(), sha1).digest()
).decode()
self.client.username_pw_set(userName, password)
self._setup_ssl_context()
# 创建连接线程
def connect_thread_func():
try:
self.client.connect(MQConfig.broker_url, MQConfig.port, 60)
self.client.loop_start()
# 添加连接超时检测
max_attempts = 5
attempt = 0
while not self.client.is_connected() and attempt < max_attempts:
logger.info(
f"[MQTT] 正在连接到 {MQConfig.broker_url}:{MQConfig.port},尝试 {attempt+1}/{max_attempts}"
)
time.sleep(3)
attempt += 1
if self.client.is_connected():
logger.info(f"[MQTT] 已成功连接到 {MQConfig.broker_url}:{MQConfig.port}")
else:
logger.error(f"[MQTT] 连接超时,可能是账号密码错误或网络问题")
self.client.loop_stop()
except Exception as e:
logger.error(f"[MQTT] 连接失败: {str(e)}")
connect_thread_func()
# connect_thread = threading.Thread(target=connect_thread_func)
# connect_thread.daemon = True
# connect_thread.start()
def stop(self):
if self.mqtt_disable:
return
self.client.disconnect()
self.client.loop_stop()
def publish_device_status(self, device_status: dict, device_id, property_name):
# status = device_status.get(device_id, {})
if self.mqtt_disable:
return
status = {"data": device_status.get(device_id, {}), "device_id": device_id, "timestamp": time.time()}
address = f"labs/{MQConfig.lab_id}/devices/"
self.client.publish(address, json.dumps(status), qos=2)
# logger.info(f"Device {device_id} status published: address: {address}, {status}")
def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[dict] = None):
if self.mqtt_disable:
return
if return_info is None:
return_info = {}
jobdata = {"job_id": job_id, "data": feedback_data, "status": status, "return_info": return_info}
self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2)
def publish_registry(self, device_id: str, device_info: dict, print_debug: bool = True):
if self.mqtt_disable:
return
address = f"labs/{MQConfig.lab_id}/registry/"
registry_data = json.dumps({device_id: device_info}, ensure_ascii=False, cls=TypeEncoder)
self.client.publish(address, registry_data, qos=2)
if print_debug:
logger.debug(f"Registry data published: address: {address}, {registry_data}")
def publish_actions(self, action_id: str, action_info: dict):
if self.mqtt_disable:
return
address = f"labs/{MQConfig.lab_id}/actions/"
self.client.publish(address, json.dumps(action_info), qos=2)
logger.debug(f"Action data published: address: {address}, {action_id}, {action_info}")
def send_ping(self, ping_id: str, timestamp: float):
"""发送ping消息到服务端"""
if self.mqtt_disable:
return
address = f"labs/{MQConfig.lab_id}/ping/"
ping_data = {"ping_id": ping_id, "client_timestamp": timestamp, "type": "ping"}
self.client.publish(address, json.dumps(ping_data), qos=2)
def setup_pong_subscription(self):
"""设置pong消息订阅"""
if self.mqtt_disable:
return
pong_topic = f"labs/{MQConfig.lab_id}/pong/"
self.client.subscribe(pong_topic, 0)
logger.debug(f"Subscribed to pong topic: {pong_topic}")
@property
def is_connected(self) -> bool:
"""检查MQTT是否已连接"""
if self.is_disabled:
return False
return hasattr(self.client, "is_connected") and self.client.is_connected()
mqtt_client = MQTTClient()
if __name__ == "__main__":
mqtt_client.start()

View File

@@ -10,117 +10,53 @@ from unilabos.utils.log import logger
from unilabos.utils.type_check import TypeEncoder
def register_devices_and_resources(comm_client, lab_registry):
def register_devices_and_resources(lab_registry):
"""
注册设备和资源到通信服务器(MQTT/WebSocket
注册设备和资源到服务器(仅支持HTTP
"""
# 注册资源信息 - 使用HTTP方式
from unilabos.app.web.client import http_client
logger.info("[UniLab Register] 开始注册设备和资源...")
if BasicConfig.auth_secret():
# 注册设备信息
devices_to_register = {}
for device_info in lab_registry.obtain_registry_device_info():
devices_to_register[device_info["id"]] = json.loads(
json.dumps(device_info, ensure_ascii=False, cls=TypeEncoder)
)
logger.debug(f"[UniLab Register] 收集设备: {device_info['id']}")
resources_to_register = {}
for resource_info in lab_registry.obtain_registry_resource_info():
resources_to_register[resource_info["id"]] = resource_info
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
print(
"[UniLab Register] 设备注册",
http_client.resource_registry({"resources": list(devices_to_register.values())}).text,
# 注册设备信息
devices_to_register = {}
for device_info in lab_registry.obtain_registry_device_info():
devices_to_register[device_info["id"]] = json.loads(
json.dumps(device_info, ensure_ascii=False, cls=TypeEncoder)
)
print(
"[UniLab Register] 资源注册",
http_client.resource_registry({"resources": list(resources_to_register.values())}).text,
)
else:
# 注册设备信息
for device_info in lab_registry.obtain_registry_device_info():
comm_client.publish_registry(device_info["id"], device_info, False)
logger.debug(f"[UniLab Register] 注册设备: {device_info['id']}")
logger.debug(f"[UniLab Register] 收集设备: {device_info['id']}")
# # 注册资源信息
# for resource_info in lab_registry.obtain_registry_resource_info():
# comm_client.publish_registry(resource_info["id"], resource_info, False)
# logger.debug(f"[UniLab Register] 注册资源: {resource_info['id']}")
resources_to_register = {}
for resource_info in lab_registry.obtain_registry_resource_info():
resources_to_register[resource_info["id"]] = resource_info
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
resources_to_register = {}
for resource_info in lab_registry.obtain_registry_resource_info():
resources_to_register[resource_info["id"]] = resource_info
logger.debug(f"[UniLab Register] 准备注册资源: {resource_info['id']}")
if resources_to_register:
# 注册设备
if devices_to_register:
try:
start_time = time.time()
response = http_client.resource_registry(resources_to_register)
response = http_client.resource_registry({"resources": list(devices_to_register.values())})
cost_time = time.time() - start_time
if response.status_code in [200, 201]:
logger.info(f"[UniLab Register] 成功通过HTTP注册 {len(resources_to_register)}资源 {cost_time}ms")
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)}设备 {cost_time}ms")
else:
logger.error(
f"[UniLab Register] HTTP注册资源失败: {response.status_code}, {response.text} {cost_time}ms"
)
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}ms")
except Exception as e:
logger.error(f"[UniLab Register] 设备注册异常: {e}")
# 注册资源
if resources_to_register:
try:
start_time = time.time()
response = http_client.resource_registry({"resources": list(resources_to_register.values())})
cost_time = time.time() - start_time
if response.status_code in [200, 201]:
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}ms")
else:
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}ms")
except Exception as e:
logger.error(f"[UniLab Register] 资源注册异常: {e}")
logger.info("[UniLab Register] 设备和资源注册完成.")
def main():
"""
命令行入口函数
"""
parser = argparse.ArgumentParser(description="注册设备和资源到 MQTT")
parser.add_argument(
"--registry",
type=str,
default=None,
action="append",
help="注册表路径",
)
parser.add_argument(
"--config",
type=str,
default=None,
help="配置文件路径,支持.py格式的Python配置文件",
)
parser.add_argument(
"--ak",
type=str,
default="",
help="实验室请求的ak",
)
parser.add_argument(
"--sk",
type=str,
default="",
help="实验室请求的sk",
)
parser.add_argument(
"--complete_registry",
action="store_true",
default=False,
help="是否补全注册表",
)
args = parser.parse_args()
load_config_from_file(args.config)
BasicConfig.ak = args.ak
BasicConfig.sk = args.sk
# 构建注册表
build_registry(args.registry, args.complete_registry, True)
from unilabos.app.communication import get_communication_client
# 获取通信客户端并启动连接
comm_client = get_communication_client()
comm_client.start()
from unilabos.registry.registry import lab_registry
# 注册设备和资源
register_devices_and_resources(comm_client, lab_registry)
if __name__ == "__main__":
main()

View File

@@ -9,16 +9,13 @@ import asyncio
import yaml
from unilabos.app.controler import devices, job_add, job_info
from unilabos.app.web.controler import devices, job_add, job_info
from unilabos.app.model import (
Resp,
RespCode,
JobStatusResp,
JobAddResp,
JobAddReq,
JobStepFinishReq,
JobPreintakeFinishReq,
JobFinishReq,
)
from unilabos.app.web.utils.host_utils import get_host_node_info
from unilabos.registry.registry import lab_registry

View File

@@ -3,6 +3,7 @@ HTTP客户端模块
提供与远程服务器通信的客户端功能只有host需要用
"""
import json
import os
from typing import List, Dict, Any, Optional
@@ -15,7 +16,6 @@ from unilabos.utils import logger
class HTTPClient:
"""HTTP客户端用于与远程服务器通信"""
backend_go = False # 是否使用Go后端
def __init__(self, remote_addr: Optional[str] = None, auth: Optional[str] = None) -> None:
"""
@@ -32,7 +32,6 @@ class HTTPClient:
auth_secret = BasicConfig.auth_secret()
if auth_secret:
self.auth = auth_secret
self.backend_go = True
info(f"正在使用ak sk作为授权信息 {auth_secret}")
else:
self.auth = MQConfig.lab_id
@@ -48,17 +47,15 @@ class HTTPClient:
Returns:
Response: API响应对象
"""
database_param = 1 if database_process_later else 0
response = requests.post(
f"{self.remote_addr}/lab/resource/edge/batch_create/?database_process_later={database_param}"
if not self.backend_go else f"{self.remote_addr}/lab/material/edge",
f"{self.remote_addr}/lab/material/edge",
json={
"edges": resources,
} if self.backend_go else resources,
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
if self.backend_go and response.status_code == 200:
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"添加物料关系失败: {response.text}")
@@ -77,12 +74,12 @@ class HTTPClient:
Response: API响应对象
"""
response = requests.post(
f"{self.remote_addr}/lab/resource/?database_process_later={1 if database_process_later else 0}" if not self.backend_go else f"{self.remote_addr}/lab/material",
json=resources if not self.backend_go else {"nodes": resources},
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
f"{self.remote_addr}/lab/material",
json={"nodes": resources},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
if self.backend_go and response.status_code == 200:
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"添加物料失败: {response.text}")
@@ -102,9 +99,9 @@ class HTTPClient:
Dict: 返回的资源数据
"""
response = requests.get(
f"{self.remote_addr}/lab/resource/?edge_format=1" if not self.backend_go else f"{self.remote_addr}/lab/material",
f"{self.remote_addr}/lab/material",
params={"id": id, "with_children": with_children},
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
headers={"Authorization": f"Lab {self.auth}"},
timeout=20,
)
return response.json()
@@ -122,7 +119,7 @@ class HTTPClient:
response = requests.delete(
f"{self.remote_addr}/lab/resource/batch_delete/",
params={"id": id},
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
headers={"Authorization": f"Lab {self.auth}"},
timeout=20,
)
return response
@@ -140,7 +137,7 @@ class HTTPClient:
response = requests.patch(
f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1",
json=resources,
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
return response
@@ -164,7 +161,7 @@ class HTTPClient:
response = requests.post(
f"{self.remote_addr}/api/account/file_upload/{scene}",
files=files,
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
headers={"Authorization": f"Lab {self.auth}"},
timeout=30, # 上传文件可能需要更长的超时时间
)
return response
@@ -180,9 +177,9 @@ class HTTPClient:
Response: API响应对象
"""
response = requests.post(
f"{self.remote_addr}/lab/registry/" if not self.backend_go else f"{self.remote_addr}/lab/resource",
f"{self.remote_addr}/lab/resource",
json=registry_data,
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
headers={"Authorization": f"Lab {self.auth}"},
timeout=30,
)
if response.status_code not in [200, 201]:
@@ -201,7 +198,7 @@ class HTTPClient:
"""
response = requests.get(
f"{self.remote_addr}/lab/resource/graph_info/",
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
headers={"Authorization": f"Lab {self.auth}"},
timeout=(3, 30),
)
if response.status_code != 200:

View File

@@ -1,6 +1,3 @@
#!/usr/bin/env python
# coding=utf-8
# 定义配置变量和加载函数
import base64
import traceback
import os
@@ -10,7 +7,6 @@ from unilabos.utils import logger
class BasicConfig:
ENV = "pro" # 'test'
ak = ""
sk = ""
working_dir = ""
@@ -21,12 +17,10 @@ class BasicConfig:
machine_name = "undefined"
vis_2d_enable = False
enable_resource_load = True
# 通信协议配置
communication_protocol = "mqtt" # 支持: "mqtt", "websocket"
communication_protocol = "websocket"
@classmethod
def auth_secret(cls):
# base64编码
if not cls.ak or not cls.sk:
return ""
target = f"{cls.ak}:{cls.sk}"
@@ -34,25 +28,6 @@ class BasicConfig:
return base64_target
# MQTT配置
class MQConfig:
lab_id = ""
instance_id = ""
access_key = ""
secret_key = ""
group_id = ""
broker_url = ""
port = 1883
ca_content = ""
cert_content = ""
key_content = ""
# 指定
ca_file = "" # 相对config.py所在目录的路径
cert_file = "" # 相对config.py所在目录的路径
key_file = "" # 相对config.py所在目录的路径
# WebSocket配置
class WSConfig:
reconnect_interval = 5 # 重连间隔(秒)
@@ -94,9 +69,6 @@ def _update_config_from_module(module):
for attr in dir(getattr(module, name)):
if not attr.startswith("_"):
setattr(obj, attr, getattr(getattr(module, name), attr))
# 更新OSS认证
if len(OSSUploadConfig.authorization) == 0:
OSSUploadConfig.authorization = f"Lab {MQConfig.lab_id}"
def _update_config_from_env():
prefix = "UNILABOS_"

View File

@@ -1 +1,12 @@
# 暂无配置
# unilabos的配置文件
class BasicConfig:
ak = "" # 实验室网页给您提供的ak代码您可以在配置文件中指定也可以通过运行unilabos时以 --ak 传入,优先按照传入参数解析
sk = "" # 实验室网页给您提供的sk代码您可以在配置文件中指定也可以通过运行unilabos时以 --sk 传入,优先按照传入参数解析
# WebSocket配置一般无需调整
class WSConfig:
reconnect_interval = 5 # 重连间隔(秒)
max_reconnect_attempts = 999 # 最大重连次数
ping_interval = 30 # ping间隔

View File

@@ -1 +0,0 @@
from .eis_model import EISModelBasedController

View File

@@ -1,5 +0,0 @@
import numpy as np
def EISModelBasedController(eis: np.array) -> float:
return 0.0

View File

@@ -164,13 +164,10 @@ class HostNode(BaseROS2DeviceNode):
self.device_status = {} # 用来存储设备状态
self.device_status_timestamps = {} # 用来存储设备状态最后更新时间
if BasicConfig.upload_registry:
from unilabos.app.communication import get_communication_client
comm_client = get_communication_client()
register_devices_and_resources(comm_client, lab_registry)
register_devices_and_resources(lab_registry)
else:
self.lab_logger().warning(
"本次启动注册表不报送云端,如果您需要联网调试,请使用unilab-register命令进行单独报送或者在启动命令增加--upload_registry"
"本次启动注册表不报送云端,如果您需要联网调试,请在启动命令增加--upload_registry"
)
time.sleep(1) # 等待通信连接稳定
# 首次发现网络中的设备

View File

@@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>unilabos_msgs</name>
<version>0.10.3</version>
<version>0.10.5</version>
<description>ROS2 Messages package for unilabos devices</description>
<maintainer email="changjh@pku.edu.cn">Junhan Chang</maintainer>
<license>MIT</license>