diff --git a/docs/conf.py b/docs/conf.py index c6b7d50a..f15f0e6f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,6 +24,7 @@ extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings "sphinx_rtd_theme", + "sphinxcontrib.mermaid" ] source_suffix = { @@ -42,6 +43,8 @@ myst_enable_extensions = [ "substitution", ] +myst_fence_as_directive = ["mermaid"] + templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] @@ -203,3 +206,5 @@ def generate_action_includes(app): def setup(app): app.connect("builder-inited", generate_action_includes) + app.add_js_file("https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js") + app.add_js_file(None, body="mermaid.initialize({startOnLoad:true});") diff --git a/docs/developer_guide/action_includes.md b/docs/developer_guide/action_includes.md index 44403eb5..ee145bfb 100644 --- a/docs/developer_guide/action_includes.md +++ b/docs/developer_guide/action_includes.md @@ -1,88 +1,26 @@ ## 简单单变量动作函数 + ### `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` - -```{literalinclude} ../../unilabos_msgs/action/EvacuateAndRefill.action -:language: yaml -``` - ---- - -### `Evaporate` - -```{literalinclude} ../../unilabos_msgs/action/Evaporate.action -:language: yaml -``` - ---- - -### `HeatChill` - -```{literalinclude} ../../unilabos_msgs/action/HeatChill.action -:language: yaml -``` - ---- +---- ### `HeatChillStart` @@ -90,7 +28,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab :language: yaml ``` ---- +---- ### `HeatChillStop` @@ -98,7 +36,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab :language: yaml ``` ---- +---- ### `PumpTransfer` @@ -106,195 +44,12 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab :language: yaml ``` ---- - -### `Separate` - -```{literalinclude} ../../unilabos_msgs/action/Separate.action -:language: yaml -``` - ---- - -### `Stir` - -```{literalinclude} ../../unilabos_msgs/action/Stir.action -: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` @@ -302,15 +57,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ---- - -### `LiquidHandlerDispense` - -```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerDispense.action -:language: yaml -``` - ---- +---- ### `LiquidHandlerDropTips` @@ -318,7 +65,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ---- +---- ### `LiquidHandlerDropTips96` @@ -326,7 +73,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ---- +---- ### `LiquidHandlerMoveLid` @@ -334,7 +81,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ---- +---- ### `LiquidHandlerMovePlate` @@ -342,7 +89,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ---- +---- ### `LiquidHandlerMoveResource` @@ -350,7 +97,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ---- +---- ### `LiquidHandlerPickUpTips` @@ -358,7 +105,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ---- +---- ### `LiquidHandlerPickUpTips96` @@ -366,7 +113,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ---- +---- ### `LiquidHandlerReturnTips` @@ -374,7 +121,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ---- +---- ### `LiquidHandlerReturnTips96` @@ -382,7 +129,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ---- +---- ### `LiquidHandlerStamp` @@ -390,129 +137,17 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ---- - -### `LiquidHandlerTransfer` - -```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransfer.action -: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` ```{literalinclude} ../../unilabos_msgs/action/AGVTransfer.action :language: yaml ``` ---- +---- ### `WorkStationRun` @@ -520,64 +155,12 @@ 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 @@ -645,8 +228,7 @@ trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_error ``` ---- - +---- ### `GripperCommand` ```yaml @@ -664,19 +246,42 @@ bool reached_goal # True iff the gripper position has reached the commanded setp ``` ---- - +---- ### `JointTrajectory` ```yaml trajectory_msgs/JointTrajectory trajectory --- - --- + ``` ---- +---- +### `ParallelGripperCommand` +```yaml +# Parallel grippers refer to an end effector where two opposing fingers grasp an object from opposite sides. +sensor_msgs/JointState command +# name: the name(s) of the joint this command is requesting +# position: desired position of each gripper joint (radians or meters) +# velocity: (optional, not used if empty) max velocity of the joint allowed while moving (radians or meters / second) +# effort: (optional, not used if empty) max effort of the joint allowed while moving (Newtons or Newton-meters) +--- +sensor_msgs/JointState state # The current gripper state. +# position of each joint (radians or meters) +# optional: velocity of each joint (radians or meters / second) +# optional: effort of each joint (Newtons or Newton-meters) +bool stalled # True if the gripper is exerting max effort and not moving +bool reached_goal # True if the gripper position has reached the commanded setpoint +--- +sensor_msgs/JointState state # The current gripper state. +# position of each joint (radians or meters) +# optional: velocity of each joint (radians or meters / second) +# optional: effort of each joint (Newtons or Newton-meters) + +``` + +---- ### `PointHead` ```yaml @@ -686,13 +291,12 @@ string pointing_frame builtin_interfaces/Duration min_duration float64 max_velocity --- - --- float64 pointing_angle_error + ``` ---- - +---- ### `SingleJointPosition` ```yaml @@ -700,16 +304,15 @@ float64 position builtin_interfaces/Duration min_duration float64 max_velocity --- - --- std_msgs/Header header float64 position float64 velocity float64 error + ``` ---- - +---- ### `AssistedTeleop` ```yaml @@ -721,10 +324,10 @@ builtin_interfaces/Duration total_elapsed_time --- #feedback builtin_interfaces/Duration current_teleop_duration + ``` ---- - +---- ### `BackUp` ```yaml @@ -738,10 +341,10 @@ builtin_interfaces/Duration total_elapsed_time --- #feedback definition float32 distance_traveled + ``` ---- - +---- ### `ComputePathThroughPoses` ```yaml @@ -756,10 +359,10 @@ nav_msgs/Path path builtin_interfaces/Duration planning_time --- #feedback definition + ``` ---- - +---- ### `ComputePathToPose` ```yaml @@ -774,10 +377,10 @@ nav_msgs/Path path builtin_interfaces/Duration planning_time --- #feedback definition + ``` ---- - +---- ### `DriveOnHeading` ```yaml @@ -791,10 +394,10 @@ builtin_interfaces/Duration total_elapsed_time --- #feedback definition float32 distance_traveled + ``` ---- - +---- ### `DummyBehavior` ```yaml @@ -805,10 +408,10 @@ std_msgs/String command builtin_interfaces/Duration total_elapsed_time --- #feedback definition + ``` ---- - +---- ### `FollowPath` ```yaml @@ -823,10 +426,10 @@ std_msgs/Empty result #feedback definition float32 distance_to_goal float32 speed + ``` ---- - +---- ### `FollowWaypoints` ```yaml @@ -838,10 +441,10 @@ int32[] missed_waypoints --- #feedback definition uint32 current_waypoint + ``` ---- - +---- ### `NavigateThroughPoses` ```yaml @@ -859,10 +462,10 @@ builtin_interfaces/Duration estimated_time_remaining int16 number_of_recoveries float32 distance_remaining int16 number_of_poses_remaining + ``` ---- - +---- ### `NavigateToPose` ```yaml @@ -879,10 +482,10 @@ builtin_interfaces/Duration navigation_time builtin_interfaces/Duration estimated_time_remaining int16 number_of_recoveries float32 distance_remaining + ``` ---- - +---- ### `SmoothPath` ```yaml @@ -898,10 +501,10 @@ builtin_interfaces/Duration smoothing_duration bool was_completed --- #feedback definition + ``` ---- - +---- ### `Spin` ```yaml @@ -914,10 +517,10 @@ builtin_interfaces/Duration total_elapsed_time --- #feedback definition float32 angular_distance_traveled + ``` ---- - +---- ### `Wait` ```yaml @@ -929,6 +532,7 @@ builtin_interfaces/Duration total_elapsed_time --- #feedback definition builtin_interfaces/Duration time_left + ``` ---- +---- diff --git a/docs/developer_guide/image/workstation_architecture/workstation_by_supplier.png b/docs/developer_guide/image/workstation_architecture/workstation_by_supplier.png new file mode 100644 index 00000000..e5f3f666 Binary files /dev/null and b/docs/developer_guide/image/workstation_architecture/workstation_by_supplier.png differ diff --git a/docs/developer_guide/image/workstation_architecture/workstation_liquid_handler.png b/docs/developer_guide/image/workstation_architecture/workstation_liquid_handler.png new file mode 100644 index 00000000..71b2d9ad Binary files /dev/null and b/docs/developer_guide/image/workstation_architecture/workstation_liquid_handler.png differ diff --git a/docs/developer_guide/image/workstation_architecture/workstation_organic.png b/docs/developer_guide/image/workstation_architecture/workstation_organic.png new file mode 100644 index 00000000..cd159a81 Binary files /dev/null and b/docs/developer_guide/image/workstation_architecture/workstation_organic.png differ diff --git a/docs/developer_guide/image/workstation_architecture/workstation_organic_yed.png b/docs/developer_guide/image/workstation_architecture/workstation_organic_yed.png new file mode 100644 index 00000000..ab1da3fb Binary files /dev/null and b/docs/developer_guide/image/workstation_architecture/workstation_organic_yed.png differ diff --git a/docs/developer_guide/workstation_architecture.md b/docs/developer_guide/workstation_architecture.md index f9d113e2..073d9aea 100644 --- a/docs/developer_guide/workstation_architecture.md +++ b/docs/developer_guide/workstation_architecture.md @@ -1,378 +1,778 @@ -# 工作站基础架构设计文档 +# 工作站模板架构设计与对接指南 + +## 0. 问题简介 + +我们可以从以下几类例子,来理解对接大型工作站需要哪些设计。本文档之后的实战案例也将由这些组成。 + +### 0.1 自研常量有机工站:最重要的是子设备管理和通信转发 + +![workstation_organic_yed](image/workstation_architecture/workstation_organic_yed.png) + +![workstation_organic](image/workstation_architecture/workstation_organic.png) + +这类工站由开发者自研,组合所有子设备和实验耗材、希望让他们在工作站这一级协调配合; + +1. 工作站包含大量已经注册的子设备,可能各自通信组态很不相同;部分设备可能会拥有同一个通信设备作为出口,如2个泵共用1个串口、所有设备共同接入PLC等。 +2. 任务系统是统一实现的 protocols,protocols 中会将高层指令处理成各子设备配合的工作流 json并管理执行、同时更改物料信息 +3. 物料系统较为简单直接,如常量有机化学仅为工作站内固定的瓶子,初始化时就已固定;随后在任务执行过程中,记录试剂量更改信息 + +### 0.2 移液工作站:物料系统和工作流模板管理 + +![workstation_liquid_handler](image/workstation_architecture/workstation_liquid_handler.png) + +1. 绝大多数情况没有子设备,有时候选配恒温震荡等模块时,接口也由工作站提供 +2. 所有任务系统均由工作站本身实现并下发指令,有统一的抽象函数可实现(pick_up_tips, aspirate, dispense, transfer 等)。有时需要将这些指令组合、转化为工作站的脚本语言,再统一下发。因此会形成大量固定的 protocols。 +3. 物料系统为固定的板位系统:台面上有多个可摆放位置,摆放标准孔板。 + +### 0.3 厂家开发的定制大型工站 + +![workstation_by_supplier](image/workstation_architecture/workstation_by_supplier.png) + +由厂家开发,具备完善的物料系统、任务系统甚至调度系统;由 PLC 或 OpenAPI TCP 协议统一通信 + +1. 在监控状态时,希望展现子设备的状态;但子设备仅为逻辑概念,通信由工作站上位机接口提供;部分情况下,子设备状态是被记录在文件中的,需要读取 +2. 工作站有自己的工作流系统甚至调度系统;可以通过脚本/PLC连续读写来配置工作站可用的工作流; +3. 部分拥有完善的物料入库、出库、过程记录,需要与 Uni-Lab-OS 物料系统对接 ## 1. 整体架构图 -```mermaid +### 1.1 工作站核心架构 + +```{mermaid} graph TB - subgraph "工作站基础架构" - WB[WorkstationBase] - WB --> |继承| RPN[ROS2WorkstationNode] - WB --> |组合| WCB[WorkstationCommunicationBase] - WB --> |组合| MMB[MaterialManagementBase] - WB --> |组合| WHS[WorkstationHTTPService] + subgraph "工作站模板组成" + WB[WorkstationBase
工作流状态管理] + RPN[ROS2WorkstationNode
Protocol执行引擎] + WB -.post_init关联.-> RPN end - subgraph "通信层实现" - WCB --> |实现| PLC[PLCCommunication] - WCB --> |实现| SER[SerialCommunication] - WCB --> |实现| ETH[EthernetCommunication] + subgraph "物料管理系统" + DECK[Deck
PLR本地物料系统] + RS[ResourceSynchronizer
外部物料同步器] + WB --> DECK + WB --> RS + RS --> DECK end - subgraph "物料管理实现" - MMB --> |实现| PLR[PyLabRobotMaterialManager] - MMB --> |实现| BIO[BioyondMaterialManager] - MMB --> |实现| SIM[SimpleMaterialManager] + subgraph "通信与子设备管理" + HW[hardware_interface
硬件通信接口] + SUBDEV[子设备集合
pumps/grippers/sensors] + WB --> HW + RPN --> SUBDEV + HW -.代理模式.-> RPN end - subgraph "HTTP服务" - WHS --> |处理| LIMS[LIMS协议报送] - WHS --> |处理| MAT[物料变更报送] - WHS --> |处理| ERR[错误处理报送] + subgraph "工作流任务系统" + PROTO[Protocol定义
LiquidHandling/PlateHandling] + WORKFLOW[Workflow执行器
步骤管理与编排] + RPN --> PROTO + RPN --> WORKFLOW + WORKFLOW --> SUBDEV + end +``` + +### 1.2 外部系统对接关系 + +```{mermaid} +graph LR + subgraph "Uni-Lab-OS工作站" + WS[WorkstationBase + ROS2WorkstationNode] + DECK2[物料系统
Deck] + HW2[通信接口
hardware_interface] + HTTP[HTTP服务
WorkstationHTTPService] end - subgraph "具体工作站实现" - WB --> |继承| WS1[PLCWorkstation] - WB --> |继承| WS2[ReportingWorkstation] - WB --> |继承| WS3[HybridWorkstation] + subgraph "外部物料系统" + BIOYOND[Bioyond物料管理] + LIMS[LIMS系统] + WAREHOUSE[第三方仓储] end - subgraph "外部系统" - EXT1[PLC设备] --> |通信| PLC - EXT2[外部工作站] --> |HTTP报送| WHS - EXT3[LIMS系统] --> |HTTP报送| WHS - EXT4[Bioyond物料系统] --> |查询| BIO + subgraph "外部硬件系统" + PLC[PLC设备] + SERIAL[串口设备] + ROBOT[机械臂/机器人] end + + subgraph "云端系统" + CLOUD[UniLab云端
资源管理] + MONITOR[监控与调度] + end + + BIOYOND <-->|RPC双向同步| DECK2 + LIMS -->|HTTP报送| HTTP + WAREHOUSE <-->|API对接| DECK2 + + PLC <-->|Modbus TCP| HW2 + SERIAL <-->|串口通信| HW2 + ROBOT <-->|SDK/API| HW2 + + WS -->|ROS消息| CLOUD + CLOUD -->|任务下发| WS + MONITOR -->|状态查询| WS +``` + +### 1.3 具体实现示例 + +```{mermaid} +graph TB + subgraph "工作站基类" + BASE[WorkstationBase
抽象基类] + end + + subgraph "Bioyond集成工作站" + BW[BioyondWorkstation] + BW_DECK[Deck + Warehouses] + BW_SYNC[BioyondResourceSynchronizer] + BW_HW[BioyondV1RPC] + BW_HTTP[HTTP报送服务] + + BW --> BW_DECK + BW --> BW_SYNC + BW --> BW_HW + BW --> BW_HTTP + end + + subgraph "纯协议节点" + PN[ProtocolNode] + PN_SUB[子设备集合] + PN_PROTO[Protocol工作流] + + PN --> PN_SUB + PN --> PN_PROTO + end + + subgraph "PLC控制工作站" + PW[PLCWorkstation] + PW_DECK[Deck物料系统] + PW_PLC[Modbus PLC客户端] + PW_WF[工作流定义] + + PW --> PW_DECK + PW --> PW_PLC + PW --> PW_WF + end + + BASE -.继承.-> BW + BASE -.继承.-> PN + BASE -.继承.-> PW ``` ## 2. 类关系图 -```mermaid +```{mermaid} classDiagram class WorkstationBase { <> - +device_id: str - +communication: WorkstationCommunicationBase - +material_management: MaterialManagementBase - +http_service: WorkstationHTTPService - +workflow_status: WorkflowStatus - +supported_workflows: Dict - - +_create_communication_module()* - +_create_material_management_module()* - +_register_supported_workflows()* - - +process_step_finish_report() - +process_sample_finish_report() - +process_order_finish_report() - +process_material_change_report() - +handle_external_error() - - +start_workflow() - +stop_workflow() - +get_workflow_status() + +_ros_node: ROS2WorkstationNode + +deck: Deck + +plr_resources: Dict[str, PLRResource] + +resource_synchronizer: ResourceSynchronizer + +hardware_interface: Union[Any, str] + +current_workflow_status: WorkflowStatus + +supported_workflows: Dict[str, WorkflowInfo] + + +post_init(ros_node)* + +set_hardware_interface(interface) + +call_device_method(method, *args, **kwargs) +get_device_status() - } + +is_device_available() + +get_deck() + +get_all_resources() + +find_resource_by_name(name) + +find_resources_by_type(type) + +sync_with_external_system() + + +execute_workflow(name, params) + +stop_workflow(emergency) + +workflow_status + +is_busy + } + class ROS2WorkstationNode { - +sub_devices: Dict - +protocol_names: List - +execute_single_action() - +create_ros_action_server() - +initialize_device() - } - - class WorkstationCommunicationBase { - <> - +config: CommunicationConfig - +is_connected: bool - +connect() - +disconnect() - +start_workflow()* - +stop_workflow()* - +get_device_status()* - +write_register() - +read_register() - } - - class MaterialManagementBase { - <> +device_id: str - +deck_config: Dict + +children: Dict[str, Any] + +sub_devices: Dict + +protocol_names: List[str] + +_action_clients: Dict + +_action_servers: Dict +resource_tracker: DeviceNodeResourceTracker - +plr_deck: Deck - +find_materials_by_type() - +update_material_location() - +convert_to_unilab_format() - +_create_resource_by_type()* - } + +initialize_device(device_id, config) + +create_ros_action_server(action_name, mapping) + +execute_single_action(device_id, action, kwargs) + +update_resource(resources) + +transfer_resource_to_another(resources, target, sites) + +_setup_hardware_proxy(device, comm_device, read, write) + } + + %% 物料管理相关类 + class Deck { + +name: str + +children: List + +assign_child_resource() + } + + class ResourceSynchronizer { + <> + +workstation: WorkstationBase + +sync_from_external()* + +sync_to_external(plr_resource)* + +handle_external_change(change_info)* + } + + class BioyondResourceSynchronizer { + +bioyond_api_client: BioyondV1RPC + +sync_interval: int + +last_sync_time: float + + +initialize() + +sync_from_external() + +sync_to_external(resource) + +handle_external_change(change_info) + } + + %% 硬件接口相关类 + class HardwareInterface { + <> + } + + class BioyondV1RPC { + +base_url: str + +api_key: str + +stock_material() + +add_material() + +material_inbound() + } + + %% 服务类 class WorkstationHTTPService { - +workstation_instance: WorkstationBase + +workstation: WorkstationBase +host: str +port: int + +server: HTTPServer + +running: bool + +start() +stop() +_handle_step_finish_report() + +_handle_sample_finish_report() + +_handle_order_finish_report() +_handle_material_change_report() + +_handle_error_handling_report() } + + %% 具体实现类 + class BioyondWorkstation { + +bioyond_config: Dict + +workflow_mappings: Dict + +workflow_sequence: List - class PLCWorkstation { - +plc_config: Dict - +modbus_client: ModbusTCPClient - +_create_communication_module() - +_create_material_management_module() - +_register_supported_workflows() + +post_init(ros_node) + +transfer_resource_to_another() + +resource_tree_add(resources) + +append_to_workflow_sequence(name) + +get_all_workflows() + +get_bioyond_status() } - - class ReportingWorkstation { - +report_handlers: Dict - +_create_communication_module() - +_create_material_management_module() - +_register_supported_workflows() + + class ProtocolNode { + +post_init(ros_node) } - - WorkstationBase --|> ROS2WorkstationNode - WorkstationBase *-- WorkstationCommunicationBase - WorkstationBase *-- MaterialManagementBase - WorkstationBase *-- WorkstationHTTPService - - PLCWorkstation --|> WorkstationBase - ReportingWorkstation --|> WorkstationBase - - WorkstationCommunicationBase <|-- PLCCommunication - WorkstationCommunicationBase <|-- DummyCommunication - - MaterialManagementBase <|-- PyLabRobotMaterialManager - MaterialManagementBase <|-- SimpleMaterialManager + + %% 核心关系 + WorkstationBase o-- ROS2WorkstationNode : post_init关联 + WorkstationBase o-- WorkstationHTTPService : 可选服务 + + %% 物料管理侧 + WorkstationBase *-- Deck : deck + WorkstationBase *-- ResourceSynchronizer : 可选组合 + ResourceSynchronizer <|-- BioyondResourceSynchronizer + + %% 硬件接口侧 + WorkstationBase o-- HardwareInterface : hardware_interface + HardwareInterface <|.. BioyondV1RPC : 实现 + BioyondResourceSynchronizer --> BioyondV1RPC : 使用 + + %% 继承关系 + BioyondWorkstation --|> WorkstationBase + ProtocolNode --|> WorkstationBase + ROS2WorkstationNode --|> BaseROS2DeviceNode : 继承 ``` ## 3. 工作站启动时序图 -```mermaid +```{mermaid} sequenceDiagram participant APP as Application participant WS as WorkstationBase - participant COMM as CommunicationModule - participant MAT as MaterialManager - participant HTTP as HTTPService + participant DECK as PLR Deck + participant SYNC as ResourceSynchronizer + participant HW as HardwareInterface participant ROS as ROS2WorkstationNode - - APP->>WS: 创建工作站实例 - WS->>ROS: 初始化ROS2WorkstationNode - ROS->>ROS: 初始化子设备 + participant HTTP as HTTPService + + APP->>WS: 创建工作站实例(__init__) + WS->>DECK: 初始化PLR Deck + DECK->>DECK: 创建Warehouse等子资源 + DECK-->>WS: Deck创建完成 + + WS->>HW: 创建硬件接口(如BioyondV1RPC) + HW->>HW: 建立连接(PLC/RPC/串口等) + HW-->>WS: 硬件接口就绪 + + WS->>SYNC: 创建ResourceSynchronizer(可选) + SYNC->>HW: 使用hardware_interface + SYNC->>SYNC: 初始化同步配置 + SYNC-->>WS: 同步器创建完成 + + WS->>SYNC: sync_from_external() + SYNC->>HW: 查询外部物料系统 + HW-->>SYNC: 返回物料数据 + SYNC->>DECK: 转换并添加到Deck + SYNC-->>WS: 同步完成 + + Note over WS: __init__完成,等待ROS节点 + + APP->>ROS: 初始化ROS2WorkstationNode + ROS->>ROS: 初始化子设备(children) + ROS->>ROS: 创建Action客户端 ROS->>ROS: 设置硬件接口代理 - - WS->>COMM: _create_communication_module() - COMM->>COMM: 初始化通信配置 - COMM->>COMM: 建立PLC/串口连接 - COMM-->>WS: 返回通信模块实例 - - WS->>MAT: _create_material_management_module() - MAT->>MAT: 创建PyLabRobot Deck - MAT->>MAT: 初始化物料资源 - MAT->>MAT: 注册到ResourceTracker - MAT-->>WS: 返回物料管理实例 - - WS->>WS: _register_supported_workflows() - WS->>WS: _create_workstation_services() - WS->>HTTP: _start_http_service() - HTTP->>HTTP: 创建HTTP服务器 - HTTP->>HTTP: 启动监听线程 - HTTP-->>WS: HTTP服务启动完成 - - WS-->>APP: 工作站初始化完成 + ROS-->>APP: ROS节点就绪 + + APP->>WS: post_init(ros_node) + WS->>WS: self._ros_node = ros_node + WS->>ROS: update_resource([deck]) + ROS->>ROS: 上传物料到云端 + ROS-->>WS: 上传完成 + + WS->>HTTP: 创建WorkstationHTTPService(可选) + HTTP->>HTTP: 启动HTTP服务器线程 + HTTP-->>WS: HTTP服务启动 + + WS-->>APP: 工作站完全就绪 ``` -## 4. 工作流执行时序图 +## 4. 工作流执行时序图(Protocol模式) -```mermaid +```{mermaid} sequenceDiagram - participant EXT as ExternalSystem - participant WS as WorkstationBase - participant COMM as CommunicationModule - participant MAT as MaterialManager + participant CLIENT as 客户端 participant ROS as ROS2WorkstationNode - participant DEV as SubDevice + participant WS as WorkstationBase + participant HW as HardwareInterface + participant DECK as PLR Deck + participant CLOUD as 云端资源管理 + participant DEV as 子设备 + + CLIENT->>ROS: 发送Protocol Action请求 + ROS->>ROS: execute_protocol回调 + ROS->>ROS: 从Goal提取参数 + ROS->>ROS: 调用protocol_steps_generator + ROS->>ROS: 生成action步骤列表 + + ROS->>WS: 更新workflow_status = RUNNING + + loop 执行每个步骤 + alt 调用子设备 + ROS->>ROS: execute_single_action(device_id, action, params) + ROS->>DEV: 发送Action Goal(通过Action Client) + DEV->>DEV: 执行设备动作 + DEV-->>ROS: 返回Result + else 调用工作站自身 + ROS->>WS: call_device_method(method, *args) + alt 直接模式 + WS->>HW: 调用hardware_interface方法 + HW->>HW: 执行硬件操作 + HW-->>WS: 返回结果 + else 代理模式 + WS->>ROS: 转发到子设备 + ROS->>DEV: 调用子设备方法 + DEV-->>ROS: 返回结果 + ROS-->>WS: 返回结果 + end + WS-->>ROS: 返回结果 + end - EXT->>WS: start_workflow(type, params) - WS->>WS: 验证工作流类型 - WS->>COMM: start_workflow(type, params) - COMM->>COMM: 发送启动命令到PLC - COMM-->>WS: 启动成功 - - WS->>WS: 更新workflow_status = RUNNING - - loop 工作流步骤执行 - WS->>ROS: execute_single_action(device_id, action, params) - ROS->>DEV: 发送ROS Action请求 - DEV->>DEV: 执行设备动作 - DEV-->>ROS: 返回执行结果 - ROS-->>WS: 返回动作结果 - - WS->>MAT: update_material_location(material_id, location) - MAT->>MAT: 更新PyLabRobot资源状态 - MAT-->>WS: 更新完成 + ROS->>DECK: 更新本地物料状态 + DECK->>DECK: 修改PLR资源属性 end - - WS->>COMM: get_workflow_status() - COMM->>COMM: 查询PLC状态寄存器 - COMM-->>WS: 返回状态信息 - - WS->>WS: 更新workflow_status = COMPLETED - WS-->>EXT: 工作流执行完成 + + ROS->>CLOUD: 同步物料到云端(可选) + CLOUD-->>ROS: 同步完成 + + ROS->>WS: 更新workflow_status = COMPLETED + ROS-->>CLIENT: 返回Protocol Result ``` ## 5. HTTP报送处理时序图 -```mermaid +```{mermaid} sequenceDiagram - participant EXT as ExternalWorkstation + participant EXT as 外部工作站/LIMS participant HTTP as HTTPService participant WS as WorkstationBase - participant MAT as MaterialManager - participant DB as DataStorage - + participant DECK as PLR Deck + participant SYNC as ResourceSynchronizer + participant CLOUD as 云端 + EXT->>HTTP: POST /report/step_finish HTTP->>HTTP: 解析请求数据 HTTP->>HTTP: 验证LIMS协议字段 HTTP->>WS: process_step_finish_report(request) - - WS->>WS: 增加接收计数 + + WS->>WS: 增加接收计数(_reports_received_count++) WS->>WS: 记录步骤完成事件 - WS->>MAT: 更新相关物料状态 - MAT->>MAT: 更新PyLabRobot资源 - MAT-->>WS: 更新完成 - - WS->>DB: 保存报送记录 - DB-->>WS: 保存完成 - + WS->>DECK: 更新相关物料状态(可选) + DECK->>DECK: 修改PLR资源状态 + + WS->>WS: 保存报送记录到内存 + WS-->>HTTP: 返回处理结果 HTTP->>HTTP: 构造HTTP响应 HTTP-->>EXT: 200 OK + acknowledgment_id - - Note over EXT,DB: 类似处理sample_finish, order_finish, material_change等报送 + + Note over EXT,CLOUD: 类似处理sample_finish, order_finish等报送 + + alt 物料变更报送 + EXT->>HTTP: POST /report/material_change + HTTP->>WS: process_material_change_report(data) + WS->>DECK: 查找或创建物料 + WS->>SYNC: sync_to_external(resource) + SYNC->>SYNC: 同步到外部系统(如Bioyond) + SYNC-->>WS: 同步完成 + WS->>CLOUD: update_resource(通过ROS节点) + CLOUD-->>WS: 上传完成 + WS-->>HTTP: 返回结果 + HTTP-->>EXT: 200 OK + end ``` ## 6. 错误处理时序图 -```mermaid +```{mermaid} sequenceDiagram - participant DEV as Device + participant DEV as 子设备/外部系统 + participant ROS as ROS2WorkstationNode participant WS as WorkstationBase - participant COMM as CommunicationModule + participant HW as HardwareInterface participant HTTP as HTTPService - participant EXT as ExternalSystem - - DEV->>WS: 设备错误事件 - WS->>WS: handle_external_error(error_data) - WS->>WS: 记录错误历史 - - alt 关键错误 - WS->>COMM: emergency_stop() - COMM->>COMM: 发送紧急停止命令 - WS->>WS: 更新workflow_status = ERROR - else 普通错误 - WS->>WS: 标记动作失败 - WS->>WS: 触发重试逻辑 + participant LOG as 日志系统 + + alt 设备错误(ROS Action失败) + DEV->>ROS: Action返回失败结果 + ROS->>ROS: 记录错误信息 + ROS->>WS: 更新workflow_status = ERROR + ROS->>LOG: 记录错误日志 + else 外部系统错误报送 + DEV->>HTTP: POST /report/error_handling + HTTP->>WS: handle_external_error(error_data) + WS->>WS: 记录错误历史 + WS->>LOG: 记录错误日志 end - - WS->>HTTP: 记录错误报送 - HTTP->>EXT: 主动通知错误状态 - - WS-->>DEV: 错误处理完成 + + alt 关键错误需要停止 + WS->>ROS: stop_workflow(emergency=True) + ROS->>ROS: 取消所有进行中的Action + ROS->>HW: 调用emergency_stop()(如果支持) + HW->>HW: 执行紧急停止 + WS->>WS: 更新workflow_status = ERROR + else 可恢复错误 + WS->>WS: 标记步骤失败 + WS->>ROS: 触发重试逻辑(可选) + ROS->>DEV: 重新发送Action + end + + WS-->>HTTP: 返回错误处理结果 + HTTP-->>DEV: 200 OK + 处理状态 ``` ## 7. 典型工作站实现示例 -### 7.1 PLC工作站实现 +### 7.1 Bioyond集成工作站实现 + +```python +class BioyondWorkstation(WorkstationBase): + def __init__(self, bioyond_config: Dict, deck: Deck, *args, **kwargs): + # 初始化deck + super().__init__(deck=deck, *args, **kwargs) + + # 设置硬件接口为Bioyond RPC客户端 + self.hardware_interface = BioyondV1RPC(bioyond_config) + + # 创建资源同步器 + self.resource_synchronizer = BioyondResourceSynchronizer(self) + + # 从Bioyond同步物料到本地deck + self.resource_synchronizer.sync_from_external() + + # 配置工作流 + self.workflow_mappings = bioyond_config.get("workflow_mappings", {}) + + def post_init(self, ros_node: ROS2WorkstationNode): + """ROS节点就绪后的初始化""" + self._ros_node = ros_node + + # 上传deck(包括所有物料)到云端 + ROS2DeviceNode.run_async_func( + self._ros_node.update_resource, + True, + resources=[self.deck] + ) + + def resource_tree_add(self, resources: List[ResourcePLR]): + """添加物料并同步到Bioyond""" + for resource in resources: + self.deck.assign_child_resource(resource, location) + self.resource_synchronizer.sync_to_external(resource) +``` + +### 7.2 纯协议节点实现 + +```python +class ProtocolNode(WorkstationBase): + """纯协议节点,不需要物料管理和外部通信""" + + def __init__(self, deck: Optional[Deck] = None, *args, **kwargs): + super().__init__(deck=deck, *args, **kwargs) + # 不设置hardware_interface和resource_synchronizer + # 所有功能通过子设备协同完成 + + def post_init(self, ros_node: ROS2WorkstationNode): + self._ros_node = ros_node + # 不需要上传物料或其他初始化 +``` + +### 7.3 PLC直接控制工作站 ```python class PLCWorkstation(WorkstationBase): - def _create_communication_module(self): - return PLCCommunication(self.communication_config) - - def _create_material_management_module(self): - return PyLabRobotMaterialManager( - self.device_id, - self.deck_config, - self.resource_tracker + def __init__(self, plc_config: Dict, deck: Deck, *args, **kwargs): + super().__init__(deck=deck, *args, **kwargs) + + # 设置硬件接口为Modbus客户端 + from pymodbus.client import ModbusTcpClient + self.hardware_interface = ModbusTcpClient( + host=plc_config["host"], + port=plc_config["port"] ) - - def _register_supported_workflows(self): + self.hardware_interface.connect() + + # 定义支持的工作流 self.supported_workflows = { - "battery_assembly": WorkflowInfo(...), - "quality_check": WorkflowInfo(...) - } -``` - -### 7.2 报送接收工作站实现 - -```python -class ReportingWorkstation(WorkstationBase): - def _create_communication_module(self): - return DummyCommunication(self.communication_config) - - def _create_material_management_module(self): - return SimpleMaterialManager( - self.device_id, - self.deck_config, - self.resource_tracker - ) - - def _register_supported_workflows(self): - self.supported_workflows = { - "data_collection": WorkflowInfo(...), - "report_processing": WorkflowInfo(...) + "battery_assembly": WorkflowInfo( + name="电池组装", + description="自动化电池组装流程", + estimated_duration=300.0, + required_materials=["battery_cell", "connector"], + output_product="battery_pack", + parameters_schema={"quantity": int, "model": str} + ) } + + def execute_workflow(self, workflow_name: str, parameters: Dict): + """通过PLC执行工作流""" + workflow_id = self._get_workflow_id(workflow_name) + + # 写入PLC寄存器启动工作流 + self.hardware_interface.write_register(100, workflow_id) + self.hardware_interface.write_register(101, parameters["quantity"]) + + self.current_workflow_status = WorkflowStatus.RUNNING + return True ``` ## 8. 核心接口说明 -### 8.1 必须实现的抽象方法 -- `_create_communication_module()`: 创建通信模块 -- `_create_material_management_module()`: 创建物料管理模块 -- `_register_supported_workflows()`: 注册支持的工作流 +### 8.1 WorkstationBase核心属性 + +| 属性 | 类型 | 说明 | +| --------------------------- | ----------------------- | ----------------------------- | +| `_ros_node` | ROS2WorkstationNode | ROS节点引用,由post_init设置 | +| `deck` | Deck | PyLabRobot Deck,本地物料系统 | +| `plr_resources` | Dict[str, PLRResource] | 物料资源映射 | +| `resource_synchronizer` | ResourceSynchronizer | 外部物料同步器(可选) | +| `hardware_interface` | Union[Any, str] | 硬件接口或代理字符串 | +| `current_workflow_status` | WorkflowStatus | 当前工作流状态 | +| `supported_workflows` | Dict[str, WorkflowInfo] | 支持的工作流定义 | + +### 8.2 必须实现的方法 + +- `post_init(ros_node)`: ROS节点就绪后的初始化,必须实现 + +### 8.3 硬件接口相关方法 + +- `set_hardware_interface(interface)`: 设置硬件接口 +- `call_device_method(method, *args, **kwargs)`: 统一设备方法调用 + - 支持直接模式: 直接调用hardware_interface的方法 + - 支持代理模式: hardware_interface="proxy:device_id"通过ROS转发 +- `get_device_status()`: 获取设备状态 +- `is_device_available()`: 检查设备可用性 + +### 8.4 物料管理方法 + +- `get_deck()`: 获取PLR Deck +- `get_all_resources()`: 获取所有物料 +- `find_resource_by_name(name)`: 按名称查找物料 +- `find_resources_by_type(type)`: 按类型查找物料 +- `sync_with_external_system()`: 触发外部同步 + +### 8.5 工作流控制方法 + +- `execute_workflow(name, params)`: 执行工作流 +- `stop_workflow(emergency)`: 停止工作流 +- `workflow_status`: 获取工作流状态(属性) +- `is_busy`: 检查是否忙碌(属性) +- `workflow_runtime`: 获取运行时间(属性) + +### 8.6 可选的HTTP报送处理方法 -### 8.2 可重写的报送处理方法 - `process_step_finish_report()`: 步骤完成处理 - `process_sample_finish_report()`: 样本完成处理 - `process_order_finish_report()`: 订单完成处理 - `process_material_change_report()`: 物料变更处理 - `handle_external_error()`: 错误处理 -### 8.3 工作流控制接口 -- `start_workflow()`: 启动工作流 -- `stop_workflow()`: 停止工作流 -- `get_workflow_status()`: 获取状态 +### 8.7 ROS2WorkstationNode核心方法 + +- `initialize_device(device_id, config)`: 初始化子设备 +- `create_ros_action_server(action_name, mapping)`: 创建Action服务器 +- `execute_single_action(device_id, action, kwargs)`: 执行单个动作 +- `update_resource(resources)`: 同步物料到云端 +- `transfer_resource_to_another(...)`: 跨设备物料转移 ## 9. 配置参数说明 +### 9.1 工作站初始化配置 + ```python -workstation_config = { - "communication_config": { - "protocol": "modbus_tcp", - "host": "192.168.1.100", - "port": 502 +# 示例1: Bioyond集成工作站 +bioyond_config = { + "base_url": "http://192.168.1.100:8080", + "api_key": "your_api_key", + "sync_interval": 600, # 同步间隔(秒) + "workflow_mappings": { + "样品制备": "workflow_uuid_1", + "质检流程": "workflow_uuid_2" }, - "deck_config": { - "size_x": 1000.0, - "size_y": 1000.0, - "size_z": 500.0 + "material_type_mappings": { + "plate": "板", + "tube": "试管" }, - "http_service_config": { - "enabled": True, - "host": "127.0.0.1", - "port": 8081 - }, - "communication_interfaces": { - "logical_device_1": CommunicationInterface(...) + "warehouse_mapping": { + "冷藏区": { + "uuid": "warehouse_uuid_1", + "locations": {...} + } + } +} + +# 创建Deck +from pylabrobot.resources import Deck +deck = Deck(name="main_deck", size_x=1000, size_y=800, size_z=200) + +workstation = BioyondWorkstation( + bioyond_config=bioyond_config, + deck=deck +) +``` + +### 9.2 子设备配置(children) + +```python +# 在devices.json中配置 +{ + "bioyond_workstation": { + "type": "protocol", # 表示这是工作站节点 + "protocol_type": ["LiquidHandling", "PlateHandling"], + "children": { + "pump_1": { + "type": "device", + "driver": "TricontInnovaDriver", + "communication": "serial_1", + "config": {...} + }, + "gripper_1": { + "type": "device", + "driver": "RobotiqGripperDriver", + "communication": "io_modbus_1", + "config": {...} + }, + "serial_1": { + "type": "communication", + "protocol": "serial", + "port": "/dev/ttyUSB0", + "baudrate": 9600 + }, + "io_modbus_1": { + "type": "communication", + "protocol": "modbus_tcp", + "host": "192.168.1.101", + "port": 502 + } + } } } ``` -这个架构设计支持: -1. **灵活的通信方式**: 通过CommunicationBase支持PLC、串口、以太网等 -2. **多样的物料管理**: 支持PyLabRobot、Bioyond、简单物料系统 -3. **统一的HTTP报送**: 基于LIMS协议的标准化报送接口 -4. **完整的工作流控制**: 支持动态和静态工作流 -5. **强大的错误处理**: 多层次的错误处理和恢复机制 +### 9.3 HTTP服务配置 + +```python +from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService + +# 创建HTTP服务(可选) +http_service = WorkstationHTTPService( + workstation_instance=workstation, + host="0.0.0.0", # 监听所有网卡 + port=8081 +) +http_service.start() +``` + +## 10. 架构设计特点总结 + +这个简化后的架构设计具有以下特点: + +### 10.1 清晰的职责分离 + +- **WorkstationBase**: 负责物料管理(deck)、硬件接口(hardware_interface)、工作流状态管理 +- **ROS2WorkstationNode**: 负责子设备管理、Protocol执行、云端物料同步 +- **ResourceSynchronizer**: 可选的外部物料系统同步(如Bioyond) +- **WorkstationHTTPService**: 可选的HTTP报送接收服务 + +### 10.2 灵活的硬件接口模式 + +1. **直接模式**: hardware_interface是具体对象(如BioyondV1RPC、ModbusClient) +2. **代理模式**: hardware_interface="proxy:device_id",通过ROS节点转发到子设备 +3. **混合模式**: 工作站有自己的接口,同时管理多个子设备 + +### 10.3 统一的物料系统 + +- 基于PyLabRobot Deck的标准化物料表示 +- 通过ResourceSynchronizer实现与外部系统(如Bioyond、LIMS)的双向同步 +- 通过ROS2WorkstationNode实现与云端的物料状态同步 + +### 10.4 Protocol驱动的工作流 + +- ROS2WorkstationNode负责Protocol的执行和步骤管理 +- 支持子设备协同(通过Action Client调用) +- 支持工作站直接控制(通过hardware_interface) + +### 10.5 可选的HTTP报送服务 + +- 基于LIMS协议规范的统一报送接口 +- 支持步骤完成、样本完成、任务完成、物料变更等多种报送类型 +- 与工作站解耦,可独立启停 + +### 10.6 简化的初始化流程 + +``` +1. __init__: 创建deck、设置hardware_interface、创建resource_synchronizer +2. 从外部系统同步物料(如果有) +3. ROS节点初始化子设备 +4. post_init: 关联ROS节点、上传物料到云端 +5. (可选)启动HTTP服务 +``` + +这种设计既保持了灵活性,又避免了过度抽象,更适合实际的工作站对接场景。 diff --git a/docs/intro.md b/docs/intro.md index 163598b4..3b176daf 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -32,9 +32,8 @@ developer_guide/device_driver developer_guide/add_device developer_guide/add_action developer_guide/actions +developer_guide/workstation_architecture developer_guide/add_protocol -developer_guide/add_batteryPLC -developer_guide/materials_tutorial.md ``` ## 接口文档 diff --git a/docs/requirements.txt b/docs/requirements.txt index 36809637..1cc92477 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,6 +2,7 @@ sphinx>=7.0.0 sphinx-rtd-theme>=2.0.0 myst-parser>=2.0.0 +sphinxcontrib-mermaid # 用于支持Jupyter notebook文档 myst-nb>=1.0.0 diff --git a/unilabos/devices/workstation/workstation_material_management.py b/unilabos/devices/workstation/workstation_material_management.py deleted file mode 100644 index a9229130..00000000 --- a/unilabos/devices/workstation/workstation_material_management.py +++ /dev/null @@ -1,583 +0,0 @@ -""" -工作站物料管理基类 -Workstation Material Management Base Class - -基于PyLabRobot的物料管理系统 -""" -from typing import Dict, Any, List, Optional, Union, Type -from abc import ABC, abstractmethod -import json - -from pylabrobot.resources import ( - Resource as PLRResource, - Container, - Deck, - Coordinate as PLRCoordinate, -) - -from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker -from unilabos.utils.log import logger -from unilabos.resources.graphio import resource_plr_to_ulab, resource_ulab_to_plr - - -class MaterialManagementBase(ABC): - """物料管理基类 - - 定义工作站物料管理的标准接口: - 1. 物料初始化 - 根据配置创建物料资源 - 2. 物料追踪 - 实时跟踪物料位置和状态 - 3. 物料查找 - 按类型、位置、状态查找物料 - 4. 物料转换 - PyLabRobot与UniLab资源格式转换 - """ - - def __init__( - self, - device_id: str, - deck_config: Dict[str, Any], - resource_tracker: DeviceNodeResourceTracker, - children_config: Dict[str, Dict[str, Any]] = None - ): - self.device_id = device_id - self.deck_config = deck_config - self.resource_tracker = resource_tracker - self.children_config = children_config or {} - - # 创建主台面 - self.plr_deck = self._create_deck() - - # 扩展ResourceTracker - self._extend_resource_tracker() - - # 注册deck到resource tracker - self.resource_tracker.add_resource(self.plr_deck) - - # 初始化子资源 - self.plr_resources = {} - self._initialize_materials() - - def _create_deck(self) -> Deck: - """创建主台面""" - return Deck( - name=f"{self.device_id}_deck", - size_x=self.deck_config.get("size_x", 1000.0), - size_y=self.deck_config.get("size_y", 1000.0), - size_z=self.deck_config.get("size_z", 500.0), - origin=PLRCoordinate(0, 0, 0) - ) - - def _extend_resource_tracker(self): - """扩展ResourceTracker以支持PyLabRobot特定功能""" - - def find_by_type(resource_type): - """按类型查找资源""" - return self._find_resources_by_type_recursive(self.plr_deck, resource_type) - - def find_by_category(category: str): - """按类别查找资源""" - found = [] - for resource in self._get_all_resources(): - if hasattr(resource, 'category') and resource.category == category: - found.append(resource) - return found - - def find_by_name_pattern(pattern: str): - """按名称模式查找资源""" - import re - found = [] - for resource in self._get_all_resources(): - if re.search(pattern, resource.name): - found.append(resource) - return found - - # 动态添加方法到resource_tracker - self.resource_tracker.find_by_type = find_by_type - self.resource_tracker.find_by_category = find_by_category - self.resource_tracker.find_by_name_pattern = find_by_name_pattern - - def _find_resources_by_type_recursive(self, resource, target_type): - """递归查找指定类型的资源""" - found = [] - if isinstance(resource, target_type): - found.append(resource) - - # 递归查找子资源 - children = getattr(resource, "children", []) - for child in children: - found.extend(self._find_resources_by_type_recursive(child, target_type)) - - return found - - def _get_all_resources(self) -> List[PLRResource]: - """获取所有资源""" - all_resources = [] - - def collect_resources(resource): - all_resources.append(resource) - children = getattr(resource, "children", []) - for child in children: - collect_resources(child) - - collect_resources(self.plr_deck) - return all_resources - - def _initialize_materials(self): - """初始化物料""" - try: - # 确定创建顺序,确保父资源先于子资源创建 - creation_order = self._determine_creation_order() - - # 按顺序创建资源 - for resource_id in creation_order: - config = self.children_config[resource_id] - self._create_plr_resource(resource_id, config) - - logger.info(f"物料管理系统初始化完成,共创建 {len(self.plr_resources)} 个资源") - - except Exception as e: - logger.error(f"物料初始化失败: {e}") - - def _determine_creation_order(self) -> List[str]: - """确定资源创建顺序""" - order = [] - visited = set() - - def visit(resource_id: str): - if resource_id in visited: - return - visited.add(resource_id) - - config = self.children_config.get(resource_id, {}) - parent_id = config.get("parent") - - # 如果有父资源,先访问父资源 - if parent_id and parent_id in self.children_config: - visit(parent_id) - - order.append(resource_id) - - for resource_id in self.children_config: - visit(resource_id) - - return order - - def _create_plr_resource(self, resource_id: str, config: Dict[str, Any]): - """创建PyLabRobot资源""" - try: - resource_type = config.get("type", "unknown") - data = config.get("data", {}) - location_config = config.get("location", {}) - - # 创建位置坐标 - location = PLRCoordinate( - x=location_config.get("x", 0.0), - y=location_config.get("y", 0.0), - z=location_config.get("z", 0.0) - ) - - # 根据类型创建资源 - resource = self._create_resource_by_type(resource_id, resource_type, config, data, location) - - if resource: - # 设置父子关系 - parent_id = config.get("parent") - if parent_id and parent_id in self.plr_resources: - parent_resource = self.plr_resources[parent_id] - parent_resource.assign_child_resource(resource, location) - else: - # 直接放在deck上 - self.plr_deck.assign_child_resource(resource, location) - - # 保存资源引用 - self.plr_resources[resource_id] = resource - - # 注册到resource tracker - self.resource_tracker.add_resource(resource) - - logger.debug(f"创建资源成功: {resource_id} ({resource_type})") - - except Exception as e: - logger.error(f"创建资源失败 {resource_id}: {e}") - - @abstractmethod - def _create_resource_by_type( - self, - resource_id: str, - resource_type: str, - config: Dict[str, Any], - data: Dict[str, Any], - location: PLRCoordinate - ) -> Optional[PLRResource]: - """根据类型创建资源 - 子类必须实现""" - pass - - # ============ 物料查找接口 ============ - - def find_materials_by_type(self, material_type: str) -> List[PLRResource]: - """按材料类型查找物料""" - return self.resource_tracker.find_by_category(material_type) - - def find_material_by_id(self, resource_id: str) -> Optional[PLRResource]: - """按ID查找物料""" - return self.plr_resources.get(resource_id) - - def find_available_positions(self, position_type: str) -> List[PLRResource]: - """查找可用位置""" - positions = self.resource_tracker.find_by_category(position_type) - available = [] - - for pos in positions: - if hasattr(pos, 'is_available') and pos.is_available(): - available.append(pos) - elif hasattr(pos, 'children') and len(pos.children) == 0: - available.append(pos) - - return available - - def get_material_inventory(self) -> Dict[str, int]: - """获取物料库存统计""" - inventory = {} - - for resource in self._get_all_resources(): - if hasattr(resource, 'category'): - category = resource.category - inventory[category] = inventory.get(category, 0) + 1 - - return inventory - - # ============ 物料状态更新接口 ============ - - def update_material_location(self, material_id: str, new_location: PLRCoordinate) -> bool: - """更新物料位置""" - try: - material = self.find_material_by_id(material_id) - if material: - material.location = new_location - return True - return False - except Exception as e: - logger.error(f"更新物料位置失败: {e}") - return False - - def move_material(self, material_id: str, target_container_id: str) -> bool: - """移动物料到目标容器""" - try: - material = self.find_material_by_id(material_id) - target = self.find_material_by_id(target_container_id) - - if material and target: - # 从原位置移除 - if material.parent: - material.parent.unassign_child_resource(material) - - # 添加到新位置 - target.assign_child_resource(material) - return True - - return False - - except Exception as e: - logger.error(f"移动物料失败: {e}") - return False - - # ============ 资源转换接口 ============ - - def convert_to_unilab_format(self, plr_resource: PLRResource) -> Dict[str, Any]: - """将PyLabRobot资源转换为UniLab格式""" - return resource_plr_to_ulab(plr_resource) - - def convert_from_unilab_format(self, unilab_resource: Dict[str, Any]) -> PLRResource: - """将UniLab格式转换为PyLabRobot资源""" - return resource_ulab_to_plr(unilab_resource) - - def get_deck_state(self) -> Dict[str, Any]: - """获取Deck状态""" - try: - return { - "deck_info": { - "name": self.plr_deck.name, - "size": { - "x": self.plr_deck.size_x, - "y": self.plr_deck.size_y, - "z": self.plr_deck.size_z - }, - "children_count": len(self.plr_deck.children) - }, - "resources": { - resource_id: self.convert_to_unilab_format(resource) - for resource_id, resource in self.plr_resources.items() - }, - "inventory": self.get_material_inventory() - } - except Exception as e: - logger.error(f"获取Deck状态失败: {e}") - return {"error": str(e)} - - # ============ 数据持久化接口 ============ - - def save_state_to_file(self, file_path: str) -> bool: - """保存状态到文件""" - try: - state = self.get_deck_state() - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(state, f, indent=2, ensure_ascii=False) - logger.info(f"状态已保存到: {file_path}") - return True - except Exception as e: - logger.error(f"保存状态失败: {e}") - return False - - def load_state_from_file(self, file_path: str) -> bool: - """从文件加载状态""" - try: - with open(file_path, 'r', encoding='utf-8') as f: - state = json.load(f) - - # 重新创建资源 - self._recreate_resources_from_state(state) - logger.info(f"状态已从文件加载: {file_path}") - return True - - except Exception as e: - logger.error(f"加载状态失败: {e}") - return False - - def _recreate_resources_from_state(self, state: Dict[str, Any]): - """从状态重新创建资源""" - # 清除现有资源 - self.plr_resources.clear() - self.plr_deck.children.clear() - - # 从状态重新创建 - resources_data = state.get("resources", {}) - for resource_id, resource_data in resources_data.items(): - try: - plr_resource = self.convert_from_unilab_format(resource_data) - self.plr_resources[resource_id] = plr_resource - self.plr_deck.assign_child_resource(plr_resource) - except Exception as e: - logger.error(f"重新创建资源失败 {resource_id}: {e}") - - -class CoinCellMaterialManagement(MaterialManagementBase): - """纽扣电池物料管理类 - - 从 button_battery_station 抽取的物料管理功能 - """ - - def _create_resource_by_type( - self, - resource_id: str, - resource_type: str, - config: Dict[str, Any], - data: Dict[str, Any], - location: PLRCoordinate - ) -> Optional[PLRResource]: - """根据类型创建纽扣电池相关资源""" - - # 导入纽扣电池资源类 - from unilabos.device_comms.button_battery_station import ( - MaterialPlate, PlateSlot, ClipMagazine, BatteryPressSlot, - TipBox64, WasteTipBox, BottleRack, Battery, ElectrodeSheet - ) - - try: - if resource_type == "material_plate": - return self._create_material_plate(resource_id, config, data, location) - - elif resource_type == "plate_slot": - return self._create_plate_slot(resource_id, config, data, location) - - elif resource_type == "clip_magazine": - return self._create_clip_magazine(resource_id, config, data, location) - - elif resource_type == "battery_press_slot": - return self._create_battery_press_slot(resource_id, config, data, location) - - elif resource_type == "tip_box": - return self._create_tip_box(resource_id, config, data, location) - - elif resource_type == "waste_tip_box": - return self._create_waste_tip_box(resource_id, config, data, location) - - elif resource_type == "bottle_rack": - return self._create_bottle_rack(resource_id, config, data, location) - - elif resource_type == "battery": - return self._create_battery(resource_id, config, data, location) - - else: - logger.warning(f"未知的资源类型: {resource_type}") - return None - - except Exception as e: - logger.error(f"创建资源失败 {resource_id} ({resource_type}): {e}") - return None - - def _create_material_plate(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建料板""" - from unilabos.device_comms.button_battery_station import MaterialPlate, ElectrodeSheet - - plate = MaterialPlate( - name=resource_id, - size_x=config.get("size_x", 80.0), - size_y=config.get("size_y", 80.0), - size_z=config.get("size_z", 10.0), - hole_diameter=config.get("hole_diameter", 15.0), - hole_depth=config.get("hole_depth", 8.0), - hole_spacing_x=config.get("hole_spacing_x", 20.0), - hole_spacing_y=config.get("hole_spacing_y", 20.0), - number=data.get("number", "") - ) - plate.location = location - - # 如果有预填充的极片数据,创建极片 - electrode_sheets = data.get("electrode_sheets", []) - for i, sheet_data in enumerate(electrode_sheets): - if i < len(plate.children): # 确保不超过洞位数量 - hole = plate.children[i] - sheet = ElectrodeSheet( - name=f"{resource_id}_sheet_{i}", - diameter=sheet_data.get("diameter", 14.0), - thickness=sheet_data.get("thickness", 0.1), - mass=sheet_data.get("mass", 0.01), - material_type=sheet_data.get("material_type", "cathode"), - info=sheet_data.get("info", "") - ) - hole.place_electrode_sheet(sheet) - - return plate - - def _create_plate_slot(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建板槽位""" - from unilabos.device_comms.button_battery_station import PlateSlot - - slot = PlateSlot( - name=resource_id, - max_plates=config.get("max_plates", 8) - ) - slot.location = location - return slot - - def _create_clip_magazine(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建子弹夹""" - from unilabos.device_comms.button_battery_station import ClipMagazine - - magazine = ClipMagazine( - name=resource_id, - size_x=config.get("size_x", 150.0), - size_y=config.get("size_y", 100.0), - size_z=config.get("size_z", 50.0), - hole_diameter=config.get("hole_diameter", 15.0), - hole_depth=config.get("hole_depth", 40.0), - hole_spacing=config.get("hole_spacing", 25.0), - max_sheets_per_hole=config.get("max_sheets_per_hole", 100) - ) - magazine.location = location - return magazine - - def _create_battery_press_slot(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建电池压制槽""" - from unilabos.device_comms.button_battery_station import BatteryPressSlot - - slot = BatteryPressSlot( - name=resource_id, - diameter=config.get("diameter", 20.0), - depth=config.get("depth", 15.0) - ) - slot.location = location - return slot - - def _create_tip_box(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建枪头盒""" - from unilabos.device_comms.button_battery_station import TipBox64 - - tip_box = TipBox64( - name=resource_id, - size_x=config.get("size_x", 127.8), - size_y=config.get("size_y", 85.5), - size_z=config.get("size_z", 60.0), - with_tips=data.get("with_tips", True) - ) - tip_box.location = location - return tip_box - - def _create_waste_tip_box(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建废枪头盒""" - from unilabos.device_comms.button_battery_station import WasteTipBox - - waste_box = WasteTipBox( - name=resource_id, - size_x=config.get("size_x", 127.8), - size_y=config.get("size_y", 85.5), - size_z=config.get("size_z", 60.0), - max_tips=config.get("max_tips", 100) - ) - waste_box.location = location - return waste_box - - def _create_bottle_rack(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建瓶架""" - from unilabos.device_comms.button_battery_station import BottleRack - - rack = BottleRack( - name=resource_id, - size_x=config.get("size_x", 210.0), - size_y=config.get("size_y", 140.0), - size_z=config.get("size_z", 100.0), - bottle_diameter=config.get("bottle_diameter", 30.0), - bottle_height=config.get("bottle_height", 100.0), - position_spacing=config.get("position_spacing", 35.0) - ) - rack.location = location - return rack - - def _create_battery(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建电池""" - from unilabos.device_comms.button_battery_station import Battery - - battery = Battery( - name=resource_id, - diameter=config.get("diameter", 20.0), - height=config.get("height", 3.2), - max_volume=config.get("max_volume", 100.0), - barcode=data.get("barcode", "") - ) - battery.location = location - return battery - - # ============ 纽扣电池特定查找方法 ============ - - def find_material_plates(self): - """查找所有料板""" - from unilabos.device_comms.button_battery_station import MaterialPlate - return self.resource_tracker.find_by_type(MaterialPlate) - - def find_batteries(self): - """查找所有电池""" - from unilabos.device_comms.button_battery_station import Battery - return self.resource_tracker.find_by_type(Battery) - - def find_electrode_sheets(self): - """查找所有极片""" - found = [] - plates = self.find_material_plates() - for plate in plates: - for hole in plate.children: - if hasattr(hole, 'has_electrode_sheet') and hole.has_electrode_sheet(): - found.append(hole._electrode_sheet) - return found - - def find_plate_slots(self): - """查找所有板槽位""" - from unilabos.device_comms.button_battery_station import PlateSlot - return self.resource_tracker.find_by_type(PlateSlot) - - def find_clip_magazines(self): - """查找所有子弹夹""" - from unilabos.device_comms.button_battery_station import ClipMagazine - return self.resource_tracker.find_by_type(ClipMagazine) - - def find_press_slots(self): - """查找所有压制槽""" - from unilabos.device_comms.button_battery_station import BatteryPressSlot - return self.resource_tracker.find_by_type(BatteryPressSlot)