diff --git a/.conda/recipe.yaml b/.conda/recipe.yaml index 8043f67a..a2aa54d8 100644 --- a/.conda/recipe.yaml +++ b/.conda/recipe.yaml @@ -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 diff --git a/docs/developer_guide/action_includes.md b/docs/developer_guide/action_includes.md index 4111fb98..44403eb5 100644 --- a/docs/developer_guide/action_includes.md +++ b/docs/developer_guide/action_includes.md @@ -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 - ``` ----- +--- diff --git a/docs/developer_guide/add_action.md b/docs/developer_guide/add_action.md index 227c2797..0e39e119 100644 --- a/docs/developer_guide/add_action.md +++ b/docs/developer_guide/add_action.md @@ -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 request,Uni-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" 部分,每个平台都有对应的构建包可供下载。 diff --git a/docs/developer_guide/add_yaml.md b/docs/developer_guide/add_yaml.md index aef71226..73e5e101 100644 --- a/docs/developer_guide/add_yaml.md +++ b/docs/developer_guide/add_yaml.md @@ -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: - : # :动作的名称 - # 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: # 动作的类型,表示动作的功能 - # 根据动作的功能选择合适的类型: - # SendCmd:发送简单命令。 - # NavigateThroughPoses:导航动作。 - # SingleJointPosition:设置单一关节的位置。 - # Stir:搅拌动作。 - # HeatChill:加热或冷却动作。 +## 注册表的基本结构 - goal: # 定义动作的目标值映射,表示需要传递给设备的参数。 - : #确定设备需要的输入参数,并将其映射到设备的字段。 +yaml 注册表就是设备的配置文件,里面定义了设备怎么用、有什么功能。好消息是系统会自动帮你填大部分内容,你只需要写两个必需的东西:设备名和 class 信息。 - feedback: # 定义动作的反馈值映射,表示设备执行动作时返回的实时状态。 - : - result: # 定义动作的结果值映射,表示动作完成后返回的最终结果。 - : +### 各字段用途 + +| 字段名 | 类型 | 需要手写 | 说明 | +| ----------------- | ------ | -------- | ----------------------------------- | +| 设备标识符 | 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_`) +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. **监控指标**:设置关键性能指标的监控和告警 diff --git a/docs/user_guide/configuration.md b/docs/user_guide/configuration.md index 049f59ac..cd1a6e9f 100644 --- a/docs/user_guide/configuration.md +++ b/docs/user_guide/configuration.md @@ -1,82 +1,73 @@ # Uni-Lab 配置指南 -Uni-Lab支持通过Python配置文件进行灵活的系统配置。本指南将帮助您理解配置选项并设置您的Uni-Lab环境。 +Uni-Lab 支持通过 Python 配置文件进行灵活的系统配置。本指南将帮助您理解配置选项并设置您的 Uni-Lab 环境。 ## 配置文件格式 -Uni-Lab支持Python格式的配置文件,它比YAML或JSON提供更多的灵活性,包括支持注释、条件逻辑和复杂数据结构。 +Uni-Lab 支持 Python 格式的配置文件,它比 YAML 或 JSON 提供更多的灵活性,包括支持注释、条件逻辑和复杂数据结构。 -### 基本配置示例 +### 默认配置示例 -一个典型的配置文件包含以下部分: +首次使用时,系统会自动创建一个基础配置文件 `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` 参数时会看到提示信息 +- 检查启动日志中的配置加载信息 +- 临时移除低优先级配置来测试高优先级配置是否生效 diff --git a/docs/user_guide/launch.md b/docs/user_guide/launch.md index b973975e..f65a212a 100644 --- a/docs/user_guide/launch.md +++ b/docs/user_guide/launch.md @@ -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` 加入 `/{devices,device_comms,resources}`。 +不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--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. 设备图谱问题 + +如果设备加载失败: + +- 检查图谱文件格式是否正确 +- 验证设备连接和端点配置 +- 确保注册表路径正确 diff --git a/recipes/msgs/recipe.yaml b/recipes/msgs/recipe.yaml index 85f648cd..48b4651f 100644 --- a/recipes/msgs/recipe.yaml +++ b/recipes/msgs/recipe.yaml @@ -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 diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index 33448231..71643b4e 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.10.3" + version: "0.10.5" source: path: ../.. diff --git a/setup.py b/setup.py index d3282041..e7201ddf 100644 --- a/setup.py +++ b/setup.py @@ -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" ], }, ) diff --git a/example_devices.py b/test/registry/example_devices.py similarity index 100% rename from example_devices.py rename to test/registry/example_devices.py diff --git a/unilabos/app/backend.py b/unilabos/app/backend.py index 9f7d4279..3e7e35f5 100644 --- a/unilabos/app/backend.py +++ b/unilabos/app/backend.py @@ -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, ) diff --git a/unilabos/app/communication.py b/unilabos/app/communication.py index e1ad228f..436fa98a 100644 --- a/unilabos/app/communication.py +++ b/unilabos/app/communication.py @@ -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: diff --git a/unilabos/app/main.py b/unilabos/app/main.py index cde79ef0..06e4095a 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -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() diff --git a/unilabos/app/mq.py b/unilabos/app/mq.py deleted file mode 100644 index adbee0ea..00000000 --- a/unilabos/app/mq.py +++ /dev/null @@ -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() diff --git a/unilabos/app/register.py b/unilabos/app/register.py index a96ff16c..204e8175 100644 --- a/unilabos/app/register.py +++ b/unilabos/app/register.py @@ -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() diff --git a/unilabos/app/web/api.py b/unilabos/app/web/api.py index 95276ac8..868de44a 100644 --- a/unilabos/app/web/api.py +++ b/unilabos/app/web/api.py @@ -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 diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index c8bab670..0a9714ac 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -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: diff --git a/unilabos/app/controler.py b/unilabos/app/web/controler.py similarity index 100% rename from unilabos/app/controler.py rename to unilabos/app/web/controler.py diff --git a/unilabos/config/config.py b/unilabos/config/config.py index 6fe972f0..e0664449 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -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_" diff --git a/unilabos/config/example_config.py b/unilabos/config/example_config.py index 649a0606..b096e410 100644 --- a/unilabos/config/example_config.py +++ b/unilabos/config/example_config.py @@ -1 +1,12 @@ -# 暂无配置 \ No newline at end of file +# 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间隔(秒) \ No newline at end of file diff --git a/unilabos/controllers/__init__.py b/unilabos/controllers/__init__.py deleted file mode 100644 index db4ee185..00000000 --- a/unilabos/controllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .eis_model import EISModelBasedController \ No newline at end of file diff --git a/unilabos/controllers/eis_model.py b/unilabos/controllers/eis_model.py deleted file mode 100644 index 0f3381f2..00000000 --- a/unilabos/controllers/eis_model.py +++ /dev/null @@ -1,5 +0,0 @@ -import numpy as np - - -def EISModelBasedController(eis: np.array) -> float: - return 0.0 diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 94b9f62d..9dbdcc1f 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -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) # 等待通信连接稳定 # 首次发现网络中的设备 diff --git a/unilabos_msgs/package.xml b/unilabos_msgs/package.xml index 4cea3066..898dc6f4 100644 --- a/unilabos_msgs/package.xml +++ b/unilabos_msgs/package.xml @@ -2,7 +2,7 @@ unilabos_msgs - 0.10.3 + 0.10.5 ROS2 Messages package for unilabos devices Junhan Chang MIT