mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-14 13:14:39 +00:00
Update workstation.
modify workstation_architecture docs bioyond_HR (#133) * feat: Enhance Bioyond synchronization and resource management - Implemented synchronization for all material types (consumables, samples, reagents) from Bioyond, logging detailed information for each type. - Improved error handling and logging during synchronization processes. - Added functionality to save Bioyond material IDs in UniLab resources for future updates. - Enhanced the `sync_to_external` method to handle material movements correctly, including querying and creating materials in Bioyond. - Updated warehouse configurations to support new storage types and improved layout for better resource management. - Introduced new resource types such as reactors and tip boxes, with detailed specifications. - Modified warehouse factory to support column offsets for naming conventions (e.g., A05-D08). - Improved resource tracking by merging extra attributes instead of overwriting them. - Added a new method for updating resources in Bioyond, ensuring better synchronization of resource changes. * feat: 添加TipBox和Reactor的配置到bottles.yaml * fix: 修复液体投料方法中的volume参数处理逻辑 修复solid_feeding_vials方法中的volume参数处理逻辑,优化solvents参数的使用条件 更新液体投料方法,支持通过溶剂信息自动计算体积,添加solvents参数并更新文档描述 Add batch creation methods for vial and solution tasks 添加批量创建90%10%小瓶投料任务和二胺溶液配置任务的功能,更新相关参数和默认值
This commit is contained in:
@@ -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});")
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
----
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 629 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
```
|
||||
|
||||
## 接口文档
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,13 +24,42 @@
|
||||
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
||||
},
|
||||
"material_type_mappings": {
|
||||
"烧杯": ["BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"],
|
||||
"试剂瓶": ["BIOYOND_PolymerStation_1BottleCarrier", ""],
|
||||
"样品板": ["BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"],
|
||||
"分装板": ["BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"],
|
||||
"样品瓶": ["BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"],
|
||||
"90%分装小瓶": ["BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"],
|
||||
"10%分装小瓶": ["BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"]
|
||||
"烧杯": [
|
||||
"BIOYOND_PolymerStation_1FlaskCarrier",
|
||||
"3a14196b-24f2-ca49-9081-0cab8021bf1a"
|
||||
],
|
||||
"试剂瓶": [
|
||||
"BIOYOND_PolymerStation_1BottleCarrier",
|
||||
""
|
||||
],
|
||||
"样品板": [
|
||||
"BIOYOND_PolymerStation_6StockCarrier",
|
||||
"3a14196e-b7a0-a5da-1931-35f3000281e9"
|
||||
],
|
||||
"分装板": [
|
||||
"BIOYOND_PolymerStation_6VialCarrier",
|
||||
"3a14196e-5dfe-6e21-0c79-fe2036d052c4"
|
||||
],
|
||||
"样品瓶": [
|
||||
"BIOYOND_PolymerStation_Solid_Stock",
|
||||
"3a14196a-cf7d-8aea-48d8-b9662c7dba94"
|
||||
],
|
||||
"90%分装小瓶": [
|
||||
"BIOYOND_PolymerStation_Solid_Vial",
|
||||
"3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"
|
||||
],
|
||||
"10%分装小瓶": [
|
||||
"BIOYOND_PolymerStation_Liquid_Vial",
|
||||
"3a14196c-76be-2279-4e22-7310d69aed68"
|
||||
],
|
||||
"枪头盒": [
|
||||
"BIOYOND_PolymerStation_TipBox",
|
||||
""
|
||||
],
|
||||
"反应器": [
|
||||
"BIOYOND_PolymerStation_Reactor",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
"deck": {
|
||||
@@ -46,8 +75,7 @@
|
||||
{
|
||||
"id": "Bioyond_Deck",
|
||||
"name": "Bioyond_Deck",
|
||||
"children": [
|
||||
],
|
||||
"children": [],
|
||||
"parent": "reaction_station_bioyond",
|
||||
"type": "deck",
|
||||
"class": "BIOYOND_PolymerReactionStation_Deck",
|
||||
@@ -69,4 +97,4 @@
|
||||
"data": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,7 @@ class BioyondV1RPC(BaseRequest):
|
||||
return response.get("data", {})
|
||||
|
||||
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
||||
"""指定库位出库物料"""
|
||||
"""指定库位出库物料(通过库位名称)"""
|
||||
location_id = LOCATION_MAPPING.get(location_name, location_name)
|
||||
|
||||
params = {
|
||||
@@ -251,7 +251,36 @@ class BioyondV1RPC(BaseRequest):
|
||||
})
|
||||
|
||||
if not response or response['code'] != 1:
|
||||
return {}
|
||||
return None
|
||||
return response
|
||||
|
||||
def material_outbound_by_id(self, material_id: str, location_id: str, quantity: int) -> dict:
|
||||
"""指定库位出库物料(直接使用location_id)
|
||||
|
||||
Args:
|
||||
material_id: 物料ID
|
||||
location_id: 库位ID(不是库位名称,是UUID)
|
||||
quantity: 数量
|
||||
|
||||
Returns:
|
||||
dict: API响应,失败返回None
|
||||
"""
|
||||
params = {
|
||||
"materialId": material_id,
|
||||
"locationId": location_id,
|
||||
"quantity": quantity
|
||||
}
|
||||
|
||||
response = self.post(
|
||||
url=f'{self.host}/api/lims/storage/outbound',
|
||||
params={
|
||||
"apiKey": self.api_key,
|
||||
"requestTime": self.get_current_time_iso8601(),
|
||||
"data": params
|
||||
})
|
||||
|
||||
if not response or response['code'] != 1:
|
||||
return None
|
||||
return response
|
||||
|
||||
# ==================== 工作流查询相关接口 ====================
|
||||
@@ -703,10 +732,10 @@ class BioyondV1RPC(BaseRequest):
|
||||
"""预加载材料列表到缓存中"""
|
||||
try:
|
||||
print("正在加载材料列表缓存...")
|
||||
|
||||
|
||||
# 加载所有类型的材料:耗材(0)、样品(1)、试剂(2)
|
||||
material_types = [1, 2]
|
||||
|
||||
|
||||
for type_mode in material_types:
|
||||
print(f"正在加载类型 {type_mode} 的材料...")
|
||||
stock_query = f'{{"typeMode": {type_mode}, "includeDetail": true}}'
|
||||
@@ -723,7 +752,7 @@ class BioyondV1RPC(BaseRequest):
|
||||
material_id = material.get("id")
|
||||
if material_name and material_id:
|
||||
self.material_cache[material_name] = material_id
|
||||
|
||||
|
||||
# 处理样品板等容器中的detail材料
|
||||
detail_materials = material.get("detail", [])
|
||||
for detail_material in detail_materials:
|
||||
|
||||
@@ -7,7 +7,7 @@ from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstati
|
||||
|
||||
class BioyondDispensingStation(BioyondWorkstation):
|
||||
def __init__(
|
||||
self,
|
||||
self,
|
||||
config,
|
||||
# 桌子
|
||||
deck,
|
||||
@@ -77,7 +77,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
- hold_m_name: 库位名称,如"C01",用于查找对应的holdMId
|
||||
|
||||
返回: 任务创建结果
|
||||
|
||||
|
||||
异常:
|
||||
- BioyondException: 各种错误情况下的统一异常
|
||||
"""
|
||||
@@ -85,7 +85,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
# 1. 参数验证
|
||||
if not hold_m_name:
|
||||
raise BioyondException("hold_m_name 是必填参数")
|
||||
|
||||
|
||||
# 检查90%物料参数的完整性
|
||||
# 90%_1物料:如果有物料名称或目标重量,就必须有全部参数
|
||||
if percent_90_1_assign_material_name or percent_90_1_target_weigh:
|
||||
@@ -93,21 +93,21 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
raise BioyondException("90%_1物料:如果提供了目标重量,必须同时提供物料名称")
|
||||
if not percent_90_1_target_weigh:
|
||||
raise BioyondException("90%_1物料:如果提供了物料名称,必须同时提供目标重量")
|
||||
|
||||
|
||||
# 90%_2物料:如果有物料名称或目标重量,就必须有全部参数
|
||||
if percent_90_2_assign_material_name or percent_90_2_target_weigh:
|
||||
if not percent_90_2_assign_material_name:
|
||||
raise BioyondException("90%_2物料:如果提供了目标重量,必须同时提供物料名称")
|
||||
if not percent_90_2_target_weigh:
|
||||
raise BioyondException("90%_2物料:如果提供了物料名称,必须同时提供目标重量")
|
||||
|
||||
|
||||
# 90%_3物料:如果有物料名称或目标重量,就必须有全部参数
|
||||
if percent_90_3_assign_material_name or percent_90_3_target_weigh:
|
||||
if not percent_90_3_assign_material_name:
|
||||
raise BioyondException("90%_3物料:如果提供了目标重量,必须同时提供物料名称")
|
||||
if not percent_90_3_target_weigh:
|
||||
raise BioyondException("90%_3物料:如果提供了物料名称,必须同时提供目标重量")
|
||||
|
||||
|
||||
# 检查10%物料参数的完整性
|
||||
# 10%_1物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数
|
||||
if any([percent_10_1_assign_material_name, percent_10_1_target_weigh, percent_10_1_volume, percent_10_1_liquid_material_name]):
|
||||
@@ -119,7 +119,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供液体体积")
|
||||
if not percent_10_1_liquid_material_name:
|
||||
raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供液体物料名称")
|
||||
|
||||
|
||||
# 10%_2物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数
|
||||
if any([percent_10_2_assign_material_name, percent_10_2_target_weigh, percent_10_2_volume, percent_10_2_liquid_material_name]):
|
||||
if not percent_10_2_assign_material_name:
|
||||
@@ -130,7 +130,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供液体体积")
|
||||
if not percent_10_2_liquid_material_name:
|
||||
raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供液体物料名称")
|
||||
|
||||
|
||||
# 10%_3物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数
|
||||
if any([percent_10_3_assign_material_name, percent_10_3_target_weigh, percent_10_3_volume, percent_10_3_liquid_material_name]):
|
||||
if not percent_10_3_assign_material_name:
|
||||
@@ -141,7 +141,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供液体体积")
|
||||
if not percent_10_3_liquid_material_name:
|
||||
raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供液体物料名称")
|
||||
|
||||
|
||||
# 2. 生成任务编码和设置默认值
|
||||
order_code = "task_vial_" + str(int(datetime.now().timestamp()))
|
||||
if order_name is None:
|
||||
@@ -152,7 +152,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
temperature = "40"
|
||||
if delay_time is None:
|
||||
delay_time = "600"
|
||||
|
||||
|
||||
# 3. 工作流ID
|
||||
workflow_id = "3a19310d-16b9-9d81-b109-0748e953694b"
|
||||
|
||||
@@ -160,22 +160,22 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
material_info = self.hardware_interface.material_id_query(workflow_id)
|
||||
if not material_info:
|
||||
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
|
||||
|
||||
|
||||
# 获取locations列表
|
||||
locations = material_info.get("locations", []) if isinstance(material_info, dict) else []
|
||||
if not locations:
|
||||
raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息")
|
||||
|
||||
|
||||
# 查找指定名称的库位
|
||||
hold_mid = None
|
||||
for location in locations:
|
||||
if location.get("holdMName") == hold_m_name:
|
||||
hold_mid = location.get("holdMId")
|
||||
break
|
||||
|
||||
|
||||
if not hold_mid:
|
||||
raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确")
|
||||
|
||||
|
||||
extend_properties = f"{{\"{ hold_mid }\": {{}}}}"
|
||||
self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}")
|
||||
|
||||
@@ -271,7 +271,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
result = self.hardware_interface.create_order(json_str)
|
||||
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}")
|
||||
return json.dumps({"suc": True})
|
||||
|
||||
|
||||
except BioyondException:
|
||||
# 重新抛出BioyondException
|
||||
raise
|
||||
@@ -307,7 +307,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
- hold_m_name: 库位名称,如"ODA-1",用于查找对应的holdMId
|
||||
|
||||
返回: 任务创建结果
|
||||
|
||||
|
||||
异常:
|
||||
- BioyondException: 各种错误情况下的统一异常
|
||||
"""
|
||||
@@ -321,8 +321,8 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
raise BioyondException("volume 是必填参数")
|
||||
if not hold_m_name:
|
||||
raise BioyondException("hold_m_name 是必填参数")
|
||||
|
||||
|
||||
|
||||
|
||||
# 2. 生成任务编码和设置默认值
|
||||
order_code = "task_oda_" + str(int(datetime.now().timestamp()))
|
||||
if order_name is None:
|
||||
@@ -333,30 +333,30 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
temperature = "20"
|
||||
if delay_time is None:
|
||||
delay_time = "600"
|
||||
|
||||
|
||||
# 3. 工作流ID - 二胺溶液配置工作流
|
||||
workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5"
|
||||
|
||||
|
||||
# 4. 查询工作流对应的holdMID
|
||||
material_info = self.hardware_interface.material_id_query(workflow_id)
|
||||
if not material_info:
|
||||
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
|
||||
|
||||
|
||||
# 获取locations列表
|
||||
locations = material_info.get("locations", []) if isinstance(material_info, dict) else []
|
||||
if not locations:
|
||||
raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息")
|
||||
|
||||
|
||||
# 查找指定名称的库位
|
||||
hold_mid = None
|
||||
for location in locations:
|
||||
if location.get("holdMName") == hold_m_name:
|
||||
hold_mid = location.get("holdMId")
|
||||
break
|
||||
|
||||
|
||||
if not hold_mid:
|
||||
raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确")
|
||||
|
||||
|
||||
extend_properties = f"{{\"{ hold_mid }\": {{}}}}"
|
||||
self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}")
|
||||
|
||||
@@ -397,9 +397,9 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
# 7. 调用create_order方法创建任务
|
||||
result = self.hardware_interface.create_order(json_str)
|
||||
self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}")
|
||||
|
||||
|
||||
return json.dumps({"suc": True})
|
||||
|
||||
|
||||
except BioyondException:
|
||||
# 重新抛出BioyondException
|
||||
raise
|
||||
@@ -409,17 +409,278 @@ class BioyondDispensingStation(BioyondWorkstation):
|
||||
self.hardware_interface._logger.error(error_msg)
|
||||
raise BioyondException(error_msg)
|
||||
|
||||
# 批量创建二胺溶液配置任务
|
||||
def batch_create_diamine_solution_tasks(self,
|
||||
solutions,
|
||||
liquid_material_name: str = "NMP",
|
||||
speed: str = None,
|
||||
temperature: str = None,
|
||||
delay_time: str = None) -> str:
|
||||
"""
|
||||
批量创建二胺溶液配置任务
|
||||
|
||||
参数说明:
|
||||
- solutions: 溶液列表(数组)或JSON字符串,格式如下:
|
||||
[
|
||||
{
|
||||
"name": "MDA",
|
||||
"order": 0,
|
||||
"solid_mass": 5.0,
|
||||
"solvent_volume": 20,
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
- liquid_material_name: 液体物料名称,默认为"NMP"
|
||||
- speed: 搅拌速度,如果为None则使用默认值400
|
||||
- temperature: 温度,如果为None则使用默认值20
|
||||
- delay_time: 延迟时间,如果为None则使用默认值600
|
||||
|
||||
返回: JSON字符串格式的任务创建结果
|
||||
|
||||
异常:
|
||||
- BioyondException: 各种错误情况下的统一异常
|
||||
"""
|
||||
try:
|
||||
# 参数类型转换:如果是字符串则解析为列表
|
||||
if isinstance(solutions, str):
|
||||
try:
|
||||
solutions = json.loads(solutions)
|
||||
except json.JSONDecodeError as e:
|
||||
raise BioyondException(f"solutions JSON解析失败: {str(e)}")
|
||||
|
||||
# 参数验证
|
||||
if not isinstance(solutions, list):
|
||||
raise BioyondException("solutions 必须是列表类型或有效的JSON数组字符串")
|
||||
|
||||
if not solutions:
|
||||
raise BioyondException("solutions 列表不能为空")
|
||||
|
||||
# 批量创建任务
|
||||
results = []
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for idx, solution in enumerate(solutions):
|
||||
try:
|
||||
# 提取参数
|
||||
name = solution.get("name")
|
||||
solid_mass = solution.get("solid_mass")
|
||||
solvent_volume = solution.get("solvent_volume")
|
||||
order = solution.get("order")
|
||||
|
||||
if not all([name, solid_mass is not None, solvent_volume is not None]):
|
||||
self.hardware_interface._logger.warning(
|
||||
f"跳过第 {idx + 1} 个溶液:缺少必要参数"
|
||||
)
|
||||
results.append({
|
||||
"index": idx + 1,
|
||||
"name": name,
|
||||
"success": False,
|
||||
"error": "缺少必要参数"
|
||||
})
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
# 生成库位名称(直接使用物料名称)
|
||||
# 如果需要其他命名规则,可以在这里调整
|
||||
hold_m_name = name
|
||||
|
||||
# 调用单个任务创建方法
|
||||
result = self.create_diamine_solution_task(
|
||||
order_name=f"二胺溶液配置-{name}",
|
||||
material_name=name,
|
||||
target_weigh=str(solid_mass),
|
||||
volume=str(solvent_volume),
|
||||
liquid_material_name=liquid_material_name,
|
||||
speed=speed,
|
||||
temperature=temperature,
|
||||
delay_time=delay_time,
|
||||
hold_m_name=hold_m_name
|
||||
)
|
||||
|
||||
results.append({
|
||||
"index": idx + 1,
|
||||
"name": name,
|
||||
"success": True,
|
||||
"hold_m_name": hold_m_name
|
||||
})
|
||||
success_count += 1
|
||||
self.hardware_interface._logger.info(
|
||||
f"成功创建二胺溶液配置任务: {name}"
|
||||
)
|
||||
|
||||
except BioyondException as e:
|
||||
results.append({
|
||||
"index": idx + 1,
|
||||
"name": solution.get("name", "unknown"),
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
})
|
||||
failed_count += 1
|
||||
self.hardware_interface._logger.error(
|
||||
f"创建第 {idx + 1} 个任务失败: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
results.append({
|
||||
"index": idx + 1,
|
||||
"name": solution.get("name", "unknown"),
|
||||
"success": False,
|
||||
"error": f"未知错误: {str(e)}"
|
||||
})
|
||||
failed_count += 1
|
||||
self.hardware_interface._logger.error(
|
||||
f"创建第 {idx + 1} 个任务时发生未知错误: {str(e)}"
|
||||
)
|
||||
|
||||
# 返回汇总结果
|
||||
summary = {
|
||||
"total": len(solutions),
|
||||
"success": success_count,
|
||||
"failed": failed_count,
|
||||
"details": results
|
||||
}
|
||||
|
||||
self.hardware_interface._logger.info(
|
||||
f"批量创建二胺溶液配置任务完成: 总数={len(solutions)}, "
|
||||
f"成功={success_count}, 失败={failed_count}"
|
||||
)
|
||||
|
||||
# 返回JSON字符串格式
|
||||
return json.dumps(summary, ensure_ascii=False)
|
||||
|
||||
except BioyondException:
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"批量创建二胺溶液配置任务时发生未预期的错误: {str(e)}"
|
||||
self.hardware_interface._logger.error(error_msg)
|
||||
raise BioyondException(error_msg)
|
||||
|
||||
# 批量创建90%10%小瓶投料任务
|
||||
def batch_create_90_10_vial_feeding_tasks(self,
|
||||
titration,
|
||||
hold_m_name: str = None,
|
||||
speed: str = None,
|
||||
temperature: str = None,
|
||||
delay_time: str = None,
|
||||
liquid_material_name: str = "NMP") -> str:
|
||||
"""
|
||||
批量创建90%10%小瓶投料任务(仅创建1个任务,但包含所有90%和10%物料)
|
||||
|
||||
参数说明:
|
||||
- titration: 滴定信息的字典或JSON字符串,格式如下:
|
||||
{
|
||||
"name": "BTDA",
|
||||
"main_portion": 1.9152351915461294, # 主称固体质量(g) -> 90%物料
|
||||
"titration_portion": 0.05923407808905555, # 滴定固体质量(g) -> 10%物料固体
|
||||
"titration_solvent": 3.050555021586361 # 滴定溶液体积(mL) -> 10%物料液体
|
||||
}
|
||||
- hold_m_name: 库位名称,如"C01"。必填参数
|
||||
- speed: 搅拌速度,如果为None则使用默认值400
|
||||
- temperature: 温度,如果为None则使用默认值40
|
||||
- delay_time: 延迟时间,如果为None则使用默认值600
|
||||
- liquid_material_name: 10%物料的液体物料名称,默认为"NMP"
|
||||
|
||||
返回: JSON字符串格式的任务创建结果
|
||||
|
||||
异常:
|
||||
- BioyondException: 各种错误情况下的统一异常
|
||||
"""
|
||||
try:
|
||||
# 参数类型转换:如果是字符串则解析为字典
|
||||
if isinstance(titration, str):
|
||||
try:
|
||||
titration = json.loads(titration)
|
||||
except json.JSONDecodeError as e:
|
||||
raise BioyondException(f"titration参数JSON解析失败: {str(e)}")
|
||||
|
||||
# 参数验证
|
||||
if not isinstance(titration, dict):
|
||||
raise BioyondException("titration 必须是字典类型或有效的JSON字符串")
|
||||
|
||||
if not hold_m_name:
|
||||
raise BioyondException("hold_m_name 是必填参数")
|
||||
|
||||
if not titration:
|
||||
raise BioyondException("titration 参数不能为空")
|
||||
|
||||
# 提取滴定数据
|
||||
name = titration.get("name")
|
||||
main_portion = titration.get("main_portion") # 主称固体质量
|
||||
titration_portion = titration.get("titration_portion") # 滴定固体质量
|
||||
titration_solvent = titration.get("titration_solvent") # 滴定溶液体积
|
||||
|
||||
if not all([name, main_portion is not None, titration_portion is not None, titration_solvent is not None]):
|
||||
raise BioyondException("titration 数据缺少必要参数")
|
||||
|
||||
# 将main_portion平均分成3份作为90%物料(3个小瓶)
|
||||
portion_90 = main_portion / 3
|
||||
|
||||
# 调用单个任务创建方法
|
||||
result = self.create_90_10_vial_feeding_task(
|
||||
order_name=f"90%10%小瓶投料-{name}",
|
||||
speed=speed,
|
||||
temperature=temperature,
|
||||
delay_time=delay_time,
|
||||
# 90%物料 - 主称固体平均分成3份
|
||||
percent_90_1_assign_material_name=name,
|
||||
percent_90_1_target_weigh=str(round(portion_90, 6)),
|
||||
percent_90_2_assign_material_name=name,
|
||||
percent_90_2_target_weigh=str(round(portion_90, 6)),
|
||||
percent_90_3_assign_material_name=name,
|
||||
percent_90_3_target_weigh=str(round(portion_90, 6)),
|
||||
# 10%物料 - 滴定固体 + 滴定溶剂(只使用第1个10%小瓶)
|
||||
percent_10_1_assign_material_name=name,
|
||||
percent_10_1_target_weigh=str(round(titration_portion, 6)),
|
||||
percent_10_1_volume=str(round(titration_solvent, 6)),
|
||||
percent_10_1_liquid_material_name=liquid_material_name,
|
||||
hold_m_name=hold_m_name
|
||||
)
|
||||
|
||||
summary = {
|
||||
"success": True,
|
||||
"hold_m_name": hold_m_name,
|
||||
"material_name": name,
|
||||
"90_vials": {
|
||||
"count": 3,
|
||||
"weight_per_vial": round(portion_90, 6),
|
||||
"total_weight": round(main_portion, 6)
|
||||
},
|
||||
"10_vials": {
|
||||
"count": 1,
|
||||
"solid_weight": round(titration_portion, 6),
|
||||
"liquid_volume": round(titration_solvent, 6)
|
||||
}
|
||||
}
|
||||
|
||||
self.hardware_interface._logger.info(
|
||||
f"成功创建90%10%小瓶投料任务: {hold_m_name}, "
|
||||
f"90%物料={portion_90:.6f}g×3, 10%物料={titration_portion:.6f}g+{titration_solvent:.6f}mL"
|
||||
)
|
||||
|
||||
# 返回JSON字符串格式
|
||||
return json.dumps(summary, ensure_ascii=False)
|
||||
|
||||
except BioyondException:
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"批量创建90%10%小瓶投料任务时发生未预期的错误: {str(e)}"
|
||||
self.hardware_interface._logger.error(error_msg)
|
||||
raise BioyondException(error_msg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
bioyond = BioyondDispensingStation(config={
|
||||
"api_key": "DE9BDDA0",
|
||||
"api_host": "http://192.168.1.200:44388"
|
||||
})
|
||||
|
||||
|
||||
# ============ 原有示例代码 ============
|
||||
|
||||
# 示例1:使用material_id_query查询工作流对应的holdMID
|
||||
workflow_id_1 = "3a15d4a1-3bbe-76f9-a458-292896a338f5" # 二胺溶液配置工作流ID
|
||||
workflow_id_2 = "3a19310d-16b9-9d81-b109-0748e953694b" # 90%10%小瓶投料工作流ID
|
||||
|
||||
|
||||
#示例2:创建二胺溶液配置任务 - ODA,指定库位名称
|
||||
# bioyond.create_diamine_solution_task(
|
||||
# order_code="task_oda_" + str(int(datetime.now().timestamp())),
|
||||
@@ -433,7 +694,7 @@ if __name__ == "__main__":
|
||||
# delay_time="600",
|
||||
# hold_m_name="烧杯ODA"
|
||||
# )
|
||||
|
||||
|
||||
# bioyond.create_diamine_solution_task(
|
||||
# order_code="task_pda_" + str(int(datetime.now().timestamp())),
|
||||
# order_name="二胺溶液配置-PDA",
|
||||
@@ -446,7 +707,7 @@ if __name__ == "__main__":
|
||||
# delay_time="600",
|
||||
# hold_m_name="烧杯PDA-2"
|
||||
# )
|
||||
|
||||
|
||||
# bioyond.create_diamine_solution_task(
|
||||
# order_code="task_mpda_" + str(int(datetime.now().timestamp())),
|
||||
# order_name="二胺溶液配置-MPDA",
|
||||
@@ -462,8 +723,8 @@ if __name__ == "__main__":
|
||||
|
||||
bioyond.material_id_query("3a19310d-16b9-9d81-b109-0748e953694b")
|
||||
bioyond.material_id_query("3a15d4a1-3bbe-76f9-a458-292896a338f5")
|
||||
|
||||
|
||||
|
||||
|
||||
#示例4:创建90%10%小瓶投料任务
|
||||
# vial_result = bioyond.create_90_10_vial_feeding_task(
|
||||
# order_code="task_vial_" + str(int(datetime.now().timestamp())),
|
||||
@@ -487,7 +748,7 @@ if __name__ == "__main__":
|
||||
# delay_time="1200",
|
||||
# hold_m_name="8.4分装板-1"
|
||||
# )
|
||||
|
||||
|
||||
# vial_result = bioyond.create_90_10_vial_feeding_task(
|
||||
# order_code="task_vial_" + str(int(datetime.now().timestamp())),
|
||||
# order_name="90%10%小瓶投料-2",
|
||||
@@ -510,7 +771,7 @@ if __name__ == "__main__":
|
||||
# delay_time="1200",
|
||||
# hold_m_name="8.4分装板-2"
|
||||
# )
|
||||
|
||||
|
||||
#启动调度器
|
||||
#bioyond.scheduler_start()
|
||||
|
||||
@@ -529,7 +790,7 @@ if __name__ == "__main__":
|
||||
material_data_yp = {
|
||||
"typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "8.4样品板",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
@@ -540,7 +801,7 @@ if __name__ == "__main__":
|
||||
"name": "BTDA-1",
|
||||
"quantity": 20,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"y": 1,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
@@ -585,7 +846,7 @@ if __name__ == "__main__":
|
||||
material_data_yp = {
|
||||
"typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "8.7样品板",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
@@ -596,7 +857,7 @@ if __name__ == "__main__":
|
||||
"name": "mianfen",
|
||||
"quantity": 13,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"y": 1,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
@@ -620,7 +881,7 @@ if __name__ == "__main__":
|
||||
material_data_fzb_1 = {
|
||||
"typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "8.7分装板",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
@@ -631,7 +892,7 @@ if __name__ == "__main__":
|
||||
"name": "10%小瓶1",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"y": 1,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
@@ -642,7 +903,7 @@ if __name__ == "__main__":
|
||||
"name": "10%小瓶2",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"y": 2,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
@@ -653,7 +914,7 @@ if __name__ == "__main__":
|
||||
"name": "10%小瓶3",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 3,
|
||||
"y": 3,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
@@ -697,7 +958,7 @@ if __name__ == "__main__":
|
||||
material_data_fzb_2 = {
|
||||
"typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "8.4分装板-2",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
@@ -708,7 +969,7 @@ if __name__ == "__main__":
|
||||
"name": "10%小瓶1",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"y": 1,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
@@ -719,7 +980,7 @@ if __name__ == "__main__":
|
||||
"name": "10%小瓶2",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"y": 2,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
@@ -730,7 +991,7 @@ if __name__ == "__main__":
|
||||
"name": "10%小瓶3",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 3,
|
||||
"y": 3,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
@@ -775,7 +1036,7 @@ if __name__ == "__main__":
|
||||
material_data_sb_oda = {
|
||||
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "mianfen1",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
@@ -785,7 +1046,7 @@ if __name__ == "__main__":
|
||||
material_data_sb_pda_2 = {
|
||||
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "mianfen2",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
@@ -795,7 +1056,7 @@ if __name__ == "__main__":
|
||||
# material_data_sb_mpda = {
|
||||
# "typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
||||
# #"code": "物料编码001",
|
||||
# #"barCode": "物料条码001",
|
||||
# #"barCode": "物料条码001",
|
||||
# "name": "烧杯MPDA",
|
||||
# "unit": "个",
|
||||
# "quantity": 1,
|
||||
|
||||
@@ -208,7 +208,8 @@ class BioyondReactionStation(BioyondWorkstation):
|
||||
def liquid_feeding_solvents(
|
||||
self,
|
||||
assign_material_name: str,
|
||||
volume: str,
|
||||
volume: str = None,
|
||||
solvents = None,
|
||||
titration_type: str = "1",
|
||||
time: str = "360",
|
||||
torque_variation: int = 2,
|
||||
@@ -218,12 +219,41 @@ class BioyondReactionStation(BioyondWorkstation):
|
||||
|
||||
Args:
|
||||
assign_material_name: 物料名称
|
||||
volume: 分液量(μL)
|
||||
volume: 分液量(μL),直接指定体积(可选,如果提供solvents则自动计算)
|
||||
solvents: 溶剂信息的字典或JSON字符串(可选),格式如下:
|
||||
{
|
||||
"additional_solvent": 33.55092503597727, # 溶剂体积(mL)
|
||||
"total_liquid_volume": 48.00916988195499
|
||||
}
|
||||
如果提供solvents,则从中提取additional_solvent并转换为μL
|
||||
titration_type: 是否滴定(1=否, 2=是)
|
||||
time: 观察时间(分钟)
|
||||
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||
temperature: 温度设定(°C)
|
||||
"""
|
||||
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
|
||||
if not volume and solvents is not None:
|
||||
# 参数类型转换:如果是字符串则解析为字典
|
||||
if isinstance(solvents, str):
|
||||
try:
|
||||
solvents = json.loads(solvents)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"solvents参数JSON解析失败: {str(e)}")
|
||||
|
||||
# 参数验证
|
||||
if not isinstance(solvents, dict):
|
||||
raise ValueError("solvents 必须是字典类型或有效的JSON字符串")
|
||||
|
||||
# 提取 additional_solvent 值
|
||||
additional_solvent = solvents.get("additional_solvent")
|
||||
if additional_solvent is None:
|
||||
raise ValueError("solvents 中没有找到 additional_solvent 字段")
|
||||
|
||||
# 转换为微升(μL) - 从毫升(mL)转换
|
||||
volume = str(float(additional_solvent) * 1000)
|
||||
elif volume is None:
|
||||
raise ValueError("必须提供 volume 或 solvents 参数之一")
|
||||
|
||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}')
|
||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||
if material_id is None:
|
||||
|
||||
@@ -85,8 +85,90 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
|
||||
def sync_to_external(self, resource: Any) -> bool:
|
||||
"""将本地物料数据变更同步到Bioyond系统"""
|
||||
try:
|
||||
if self.bioyond_api_client is None:
|
||||
logger.error("Bioyond API客户端未初始化")
|
||||
# ✅ 跳过仓库类型的资源 - 仓库是容器,不是物料
|
||||
resource_category = getattr(resource, "category", None)
|
||||
if resource_category == "warehouse":
|
||||
logger.debug(f"[同步→Bioyond] 跳过仓库类型资源: {resource.name} (仓库是容器,不需要同步为物料)")
|
||||
return True
|
||||
|
||||
logger.info(f"[同步→Bioyond] 收到物料变更: {resource.name}")
|
||||
|
||||
# 获取物料的 Bioyond ID
|
||||
extra_info = getattr(resource, "unilabos_extra", {})
|
||||
material_bioyond_id = extra_info.get("material_bioyond_id")
|
||||
|
||||
# ⭐ 如果没有 Bioyond ID,尝试从 Bioyond 系统中按名称查询
|
||||
if not material_bioyond_id:
|
||||
logger.warning(f"[同步→Bioyond] 物料 {resource.name} 没有 Bioyond ID,尝试按名称查询...")
|
||||
try:
|
||||
# 查询所有类型的物料:0=耗材, 1=样品, 2=试剂
|
||||
import json
|
||||
all_materials = []
|
||||
|
||||
for type_mode in [0, 1, 2]:
|
||||
query_params = json.dumps({
|
||||
"typeMode": type_mode,
|
||||
"filter": "", # 空字符串表示查询所有
|
||||
"includeDetail": True
|
||||
})
|
||||
materials = self.bioyond_api_client.stock_material(query_params)
|
||||
if materials:
|
||||
all_materials.extend(materials)
|
||||
|
||||
logger.info(f"[同步→Bioyond] 查询到 {len(all_materials)} 个物料")
|
||||
|
||||
# 按名称匹配
|
||||
for mat in all_materials:
|
||||
if mat.get("name") == resource.name:
|
||||
material_bioyond_id = mat.get("id")
|
||||
mat_type = mat.get("typeName", "未知")
|
||||
logger.info(f"✅ 找到物料 {resource.name} ({mat_type}) 的 Bioyond ID: {material_bioyond_id[:8]}...")
|
||||
# 保存 ID 到资源对象
|
||||
extra_info["material_bioyond_id"] = material_bioyond_id
|
||||
setattr(resource, "unilabos_extra", extra_info)
|
||||
break
|
||||
|
||||
if not material_bioyond_id:
|
||||
logger.warning(f"⚠️ 在 Bioyond 系统中未找到名为 {resource.name} 的物料")
|
||||
logger.info(f"[同步→Bioyond] 这是一个新物料,将创建并入库到 Bioyond 系统")
|
||||
# 不返回,继续执行后续的创建+入库流程
|
||||
except Exception as e:
|
||||
logger.error(f"查询 Bioyond 物料失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
# 检查是否有位置更新请求
|
||||
update_site = extra_info.get("update_resource_site")
|
||||
|
||||
if not update_site:
|
||||
logger.debug(f"[同步→Bioyond] 无位置更新请求")
|
||||
return True
|
||||
|
||||
# ===== 物料移动/创建流程 =====
|
||||
if material_bioyond_id:
|
||||
logger.info(f"[同步→Bioyond] 🔄 开始移动物料 {resource.name} 到 {update_site}")
|
||||
else:
|
||||
logger.info(f"[同步→Bioyond] ➕ 开始创建新物料 {resource.name} 并入库到 {update_site}") # 第1步:获取仓库配置
|
||||
from .config import WAREHOUSE_MAPPING
|
||||
warehouse_mapping = WAREHOUSE_MAPPING
|
||||
|
||||
# 确定目标仓库名称(通过遍历所有仓库的库位配置)
|
||||
parent_name = None
|
||||
target_location_uuid = None
|
||||
|
||||
for warehouse_name, warehouse_info in warehouse_mapping.items():
|
||||
site_uuids = warehouse_info.get("site_uuids", {})
|
||||
if update_site in site_uuids:
|
||||
parent_name = warehouse_name
|
||||
target_location_uuid = site_uuids[update_site]
|
||||
logger.info(f"[同步] 目标仓库: {parent_name}/{update_site}")
|
||||
logger.info(f"[同步] 目标库位UUID: {target_location_uuid[:8]}...")
|
||||
break
|
||||
|
||||
if not parent_name or not target_location_uuid:
|
||||
logger.error(f"❌ 库位 {update_site} 没有在 WAREHOUSE_MAPPING 中配置")
|
||||
logger.debug(f"可用仓库: {list(warehouse_mapping.keys())}")
|
||||
return False
|
||||
|
||||
bioyond_material = resource_plr_to_bioyond(
|
||||
@@ -171,11 +253,22 @@ class BioyondWorkstation(WorkstationBase):
|
||||
|
||||
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||
self._ros_node = ros_node
|
||||
|
||||
# ⭐ 上传 deck(包括所有 warehouses 及其中的物料)
|
||||
# 注意:如果有从 Bioyond 同步的物料,它们已经被放置到 warehouse 中了
|
||||
# 所以只需要上传 deck,物料会作为 warehouse 的 children 一起上传
|
||||
logger.info("正在上传 deck(包括 warehouses 和物料)到云端...")
|
||||
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
||||
"resources": [self.deck]
|
||||
})
|
||||
|
||||
# 清理临时变量(物料已经在 deck 的 warehouse children 中,不需要单独上传)
|
||||
if hasattr(self, "_synced_resources"):
|
||||
logger.info(f"✅ {len(self._synced_resources)} 个从Bioyond同步的物料已包含在 deck 中")
|
||||
self._synced_resources = []
|
||||
|
||||
def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot):
|
||||
time.sleep(3)
|
||||
ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
|
||||
"plr_resources": resource,
|
||||
"target_device_id": mount_device_id,
|
||||
@@ -246,7 +339,7 @@ class BioyondWorkstation(WorkstationBase):
|
||||
}
|
||||
|
||||
# ==================== 工作流合并与参数设置 API ====================
|
||||
|
||||
|
||||
def append_to_workflow_sequence(self, web_workflow_name: str) -> bool:
|
||||
# 检查是否为JSON格式的字符串
|
||||
actual_workflow_name = web_workflow_name
|
||||
@@ -257,7 +350,7 @@ class BioyondWorkstation(WorkstationBase):
|
||||
print(f"解析JSON格式工作流名称: {web_workflow_name} -> {actual_workflow_name}")
|
||||
except json.JSONDecodeError:
|
||||
print(f"JSON解析失败,使用原始字符串: {web_workflow_name}")
|
||||
|
||||
|
||||
workflow_id = self._get_workflow(actual_workflow_name)
|
||||
if workflow_id:
|
||||
self.workflow_sequence.append(workflow_id)
|
||||
@@ -322,7 +415,7 @@ class BioyondWorkstation(WorkstationBase):
|
||||
# ============ 工作站状态管理 ============
|
||||
def get_station_info(self) -> Dict[str, Any]:
|
||||
"""获取工作站基础信息
|
||||
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 工作站基础信息,包括设备ID、状态等
|
||||
"""
|
||||
@@ -450,8 +543,8 @@ class BioyondWorkstation(WorkstationBase):
|
||||
|
||||
# 转换为UniLab格式
|
||||
unilab_resources = resource_bioyond_to_plr(
|
||||
bioyond_data,
|
||||
type_mapping=self.bioyond_config["material_type_mappings"],
|
||||
bioyond_data,
|
||||
type_mapping=self.bioyond_config["material_type_mappings"],
|
||||
deck=self.deck
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -5,6 +5,157 @@ bioyond_dispensing_station:
|
||||
- bioyond_dispensing_station
|
||||
class:
|
||||
action_value_mappings:
|
||||
batch_create_90_10_vial_feeding_tasks:
|
||||
feedback: {}
|
||||
goal:
|
||||
delay_time: delay_time
|
||||
hold_m_name: hold_m_name
|
||||
liquid_material_name: liquid_material_name
|
||||
speed: speed
|
||||
temperature: temperature
|
||||
titration: titration
|
||||
goal_default:
|
||||
delay_time: '600'
|
||||
hold_m_name: ''
|
||||
liquid_material_name: NMP
|
||||
speed: '400'
|
||||
temperature: '40'
|
||||
titration: ''
|
||||
handles:
|
||||
input:
|
||||
- data_key: titration
|
||||
data_source: handle
|
||||
data_type: object
|
||||
handler_key: titration
|
||||
io_type: source
|
||||
label: Titration Data From Calculation Node
|
||||
result:
|
||||
return_info: return_info
|
||||
schema:
|
||||
description: 批量创建90%10%小瓶投料任务。从计算节点接收titration数据,包含物料名称、主称固体质量、滴定固体质量和滴定溶剂体积。
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: BatchCreate9010VialFeedingTasks_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
delay_time:
|
||||
default: '600'
|
||||
description: 延迟时间(秒),默认600
|
||||
type: string
|
||||
hold_m_name:
|
||||
description: 库位名称,如"C01",必填参数
|
||||
type: string
|
||||
liquid_material_name:
|
||||
default: NMP
|
||||
description: 10%物料的液体物料名称,默认为"NMP"
|
||||
type: string
|
||||
speed:
|
||||
default: '400'
|
||||
description: 搅拌速度,默认400
|
||||
type: string
|
||||
temperature:
|
||||
default: '40'
|
||||
description: 温度(℃),默认40
|
||||
type: string
|
||||
titration:
|
||||
description: '滴定信息对象,包含: name(物料名称), main_portion(主称固体质量g), titration_portion(滴定固体质量g),
|
||||
titration_solvent(滴定溶液体积mL)'
|
||||
type: string
|
||||
required:
|
||||
- titration
|
||||
- hold_m_name
|
||||
title: BatchCreate9010VialFeedingTasks_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: BatchCreate9010VialFeedingTasks_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: BatchCreate9010VialFeedingTasks
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
batch_create_diamine_solution_tasks:
|
||||
feedback: {}
|
||||
goal:
|
||||
delay_time: delay_time
|
||||
liquid_material_name: liquid_material_name
|
||||
solutions: solutions
|
||||
speed: speed
|
||||
temperature: temperature
|
||||
goal_default:
|
||||
delay_time: '600'
|
||||
liquid_material_name: NMP
|
||||
solutions: ''
|
||||
speed: '400'
|
||||
temperature: '20'
|
||||
handles:
|
||||
input:
|
||||
- data_key: solutions
|
||||
data_source: handle
|
||||
data_type: array
|
||||
handler_key: solutions
|
||||
io_type: source
|
||||
label: Solution Data From Python
|
||||
result:
|
||||
return_info: return_info
|
||||
schema:
|
||||
description: 批量创建二胺溶液配置任务。自动为多个二胺样品创建溶液配置任务,每个任务包含固体物料称量、溶剂添加、搅拌混合等步骤。
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: BatchCreateDiamineSolutionTasks_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
delay_time:
|
||||
default: '600'
|
||||
description: 溶液配置完成后的延迟时间(秒),用于充分混合和溶解,默认600秒
|
||||
type: string
|
||||
liquid_material_name:
|
||||
default: NMP
|
||||
description: 液体溶剂名称,用于溶解固体物料,默认为NMP(N-甲基吡咯烷酮)
|
||||
type: string
|
||||
solutions:
|
||||
description: '溶液列表,JSON数组格式,每个元素包含: name(物料名称), order(序号), solid_mass(固体质量g),
|
||||
solvent_volume(溶剂体积mL)。示例: [{"name": "MDA", "order": 0, "solid_mass":
|
||||
5.0, "solvent_volume": 20}, {"name": "MPDA", "order": 1, "solid_mass":
|
||||
4.5, "solvent_volume": 18}]'
|
||||
type: string
|
||||
speed:
|
||||
default: '400'
|
||||
description: 搅拌速度(rpm),用于混合溶液,默认400转/分钟
|
||||
type: string
|
||||
temperature:
|
||||
default: '20'
|
||||
description: 配置温度(℃),溶液配置过程的目标温度,默认20℃(室温)
|
||||
type: string
|
||||
required:
|
||||
- solutions
|
||||
title: BatchCreateDiamineSolutionTasks_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
description: 批量任务创建结果汇总,JSON格式包含总数、成功数、失败数及每个任务的详细信息
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: BatchCreateDiamineSolutionTasks_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: BatchCreateDiamineSolutionTasks
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
create_90_10_vial_feeding_task:
|
||||
feedback: {}
|
||||
goal:
|
||||
|
||||
@@ -120,6 +120,7 @@ reaction_station.bioyond:
|
||||
feedback: {}
|
||||
goal:
|
||||
assign_material_name: assign_material_name
|
||||
solvents: solvents
|
||||
temperature: temperature
|
||||
time: time
|
||||
titration_type: titration_type
|
||||
@@ -127,15 +128,23 @@ reaction_station.bioyond:
|
||||
volume: volume
|
||||
goal_default:
|
||||
assign_material_name: ''
|
||||
temperature: ''
|
||||
time: ''
|
||||
titration_type: ''
|
||||
torque_variation: ''
|
||||
solvents: ''
|
||||
temperature: '25.00'
|
||||
time: '360'
|
||||
titration_type: '1'
|
||||
torque_variation: '2'
|
||||
volume: ''
|
||||
handles: {}
|
||||
handles:
|
||||
input:
|
||||
- data_key: solvents
|
||||
data_source: handle
|
||||
data_type: object
|
||||
handler_key: solvents
|
||||
io_type: source
|
||||
label: Solvents Data From Calculation Node
|
||||
result: {}
|
||||
schema:
|
||||
description: 液体投料-溶剂
|
||||
description: 液体投料-溶剂。可以直接提供volume(μL),或通过solvents对象自动从additional_solvent(mL)计算volume。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
@@ -143,28 +152,30 @@ reaction_station.bioyond:
|
||||
assign_material_name:
|
||||
description: 物料名称
|
||||
type: string
|
||||
solvents:
|
||||
description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume'
|
||||
type: string
|
||||
temperature:
|
||||
description: 温度设定(°C)
|
||||
default: '25.00'
|
||||
description: 温度设定(°C),默认25.00
|
||||
type: string
|
||||
time:
|
||||
description: 观察时间(分钟)
|
||||
default: '360'
|
||||
description: 观察时间(分钟),默认360
|
||||
type: string
|
||||
titration_type:
|
||||
description: 是否滴定(1=否, 2=是)
|
||||
default: '1'
|
||||
description: 是否滴定(1=否, 2=是),默认1
|
||||
type: string
|
||||
torque_variation:
|
||||
description: 是否观察 (1=否, 2=是)
|
||||
default: '2'
|
||||
description: 是否观察 (1=否, 2=是),默认2
|
||||
type: string
|
||||
volume:
|
||||
description: 分液公式(μL)
|
||||
description: 分液量(μL)。可直接提供,或通过solvents参数自动计算
|
||||
type: string
|
||||
required:
|
||||
- volume
|
||||
- assign_material_name
|
||||
- time
|
||||
- torque_variation
|
||||
- titration_type
|
||||
- temperature
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
|
||||
@@ -48,3 +48,25 @@ BIOYOND_PolymerStation_Solution_Beaker:
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
BIOYOND_PolymerStation_TipBox:
|
||||
category:
|
||||
- bottles
|
||||
- tip_boxes
|
||||
class:
|
||||
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_TipBox
|
||||
type: pylabrobot
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
BIOYOND_PolymerStation_Reactor:
|
||||
category:
|
||||
- bottles
|
||||
- reactors
|
||||
class:
|
||||
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Reactor
|
||||
type: pylabrobot
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
|
||||
@@ -90,3 +90,89 @@ def BIOYOND_PolymerStation_Reagent_Bottle(
|
||||
barcode=barcode,
|
||||
model="BIOYOND_PolymerStation_Reagent_Bottle",
|
||||
)
|
||||
|
||||
|
||||
def BIOYOND_PolymerStation_Reactor(
|
||||
name: str,
|
||||
diameter: float = 30.0,
|
||||
height: float = 80.0,
|
||||
max_volume: float = 50000.0, # 50mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建反应器"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="BIOYOND_PolymerStation_Reactor",
|
||||
)
|
||||
|
||||
|
||||
def BIOYOND_PolymerStation_TipBox(
|
||||
name: str,
|
||||
size_x: float = 127.76, # 枪头盒宽度
|
||||
size_y: float = 85.48, # 枪头盒长度
|
||||
size_z: float = 100.0, # 枪头盒高度
|
||||
barcode: str = None,
|
||||
):
|
||||
"""创建4×6枪头盒 (24个枪头)
|
||||
|
||||
Args:
|
||||
name: 枪头盒名称
|
||||
size_x: 枪头盒宽度 (mm)
|
||||
size_y: 枪头盒长度 (mm)
|
||||
size_z: 枪头盒高度 (mm)
|
||||
barcode: 条形码
|
||||
|
||||
Returns:
|
||||
TipBoxCarrier: 包含24个枪头孔位的枪头盒
|
||||
"""
|
||||
from pylabrobot.resources import Container, Coordinate
|
||||
|
||||
# 创建枪头盒容器
|
||||
tip_box = Container(
|
||||
name=name,
|
||||
size_x=size_x,
|
||||
size_y=size_y,
|
||||
size_z=size_z,
|
||||
category="tip_rack",
|
||||
model="BIOYOND_PolymerStation_TipBox_4x6",
|
||||
)
|
||||
|
||||
# 设置自定义属性
|
||||
tip_box.barcode = barcode
|
||||
tip_box.tip_count = 24 # 4行×6列
|
||||
tip_box.num_items_x = 6 # 6列
|
||||
tip_box.num_items_y = 4 # 4行
|
||||
|
||||
# 创建24个枪头孔位 (4行×6列)
|
||||
# 假设孔位间距为 9mm
|
||||
tip_spacing_x = 9.0 # 列间距
|
||||
tip_spacing_y = 9.0 # 行间距
|
||||
start_x = 14.38 # 第一个孔位的x偏移
|
||||
start_y = 11.24 # 第一个孔位的y偏移
|
||||
|
||||
for row in range(4): # A, B, C, D
|
||||
for col in range(6): # 1-6
|
||||
spot_name = f"{chr(65 + row)}{col + 1}" # A1, A2, ..., D6
|
||||
x = start_x + col * tip_spacing_x
|
||||
y = start_y + row * tip_spacing_y
|
||||
|
||||
# 创建枪头孔位容器
|
||||
tip_spot = Container(
|
||||
name=spot_name,
|
||||
size_x=8.0, # 单个枪头孔位大小
|
||||
size_y=8.0,
|
||||
size_z=size_z - 10.0, # 略低于盒子高度
|
||||
category="tip_spot",
|
||||
)
|
||||
|
||||
# 添加到枪头盒
|
||||
tip_box.assign_child_resource(
|
||||
tip_spot,
|
||||
location=Coordinate(x=x, y=y, z=0)
|
||||
)
|
||||
|
||||
return tip_box
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
from os import name
|
||||
from pylabrobot.resources import Deck, Coordinate, Rotation
|
||||
|
||||
from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling, bioyond_warehouse_1x2x2, bioyond_warehouse_1x3x3, bioyond_warehouse_10x1x1, bioyond_warehouse_3x3x1, bioyond_warehouse_3x3x1_2, bioyond_warehouse_5x1x1
|
||||
from unilabos.resources.bioyond.warehouses import (
|
||||
bioyond_warehouse_1x4x4,
|
||||
bioyond_warehouse_1x4x4_right, # 新增:右侧仓库 (A05~D08)
|
||||
bioyond_warehouse_1x4x2,
|
||||
bioyond_warehouse_liquid_and_lid_handling,
|
||||
bioyond_warehouse_1x2x2,
|
||||
bioyond_warehouse_1x3x3,
|
||||
bioyond_warehouse_10x1x1,
|
||||
bioyond_warehouse_3x3x1,
|
||||
bioyond_warehouse_3x3x1_2,
|
||||
bioyond_warehouse_5x1x1,
|
||||
bioyond_warehouse_1x8x4,
|
||||
bioyond_warehouse_reagent_storage,
|
||||
bioyond_warehouse_liquid_preparation,
|
||||
bioyond_warehouse_tipbox_storage, # 新增:Tip盒堆栈
|
||||
)
|
||||
|
||||
|
||||
class BIOYOND_PolymerReactionStation_Deck(Deck):
|
||||
def __init__(
|
||||
self,
|
||||
self,
|
||||
name: str = "PolymerReactionStation_Deck",
|
||||
size_x: float = 2700.0,
|
||||
size_y: float = 1080.0,
|
||||
@@ -20,15 +35,22 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
|
||||
|
||||
def setup(self) -> None:
|
||||
# 添加仓库
|
||||
# 说明: 堆栈1物理上分为左右两部分
|
||||
# - 堆栈1左: A01~D04 (4行×4列, 位于反应站左侧)
|
||||
# - 堆栈1右: A05~D08 (4行×4列, 位于反应站右侧)
|
||||
self.warehouses = {
|
||||
"堆栈1": bioyond_warehouse_1x4x4("堆栈1"),
|
||||
"堆栈2": bioyond_warehouse_1x4x4("堆栈2"),
|
||||
"站内试剂存放堆栈": bioyond_warehouse_liquid_and_lid_handling("站内试剂存放堆栈"),
|
||||
"堆栈1左": bioyond_warehouse_1x4x4("堆栈1左"), # 左侧堆栈: A01~D04
|
||||
"堆栈1右": bioyond_warehouse_1x4x4_right("堆栈1右"), # 右侧堆栈: A05~D08
|
||||
"站内试剂存放堆栈": bioyond_warehouse_reagent_storage("站内试剂存放堆栈"), # A01~A02
|
||||
"移液站内10%分装液体准备仓库": bioyond_warehouse_liquid_preparation("移液站内10%分装液体准备仓库"), # A01~B04
|
||||
"站内Tip盒堆栈": bioyond_warehouse_tipbox_storage("站内Tip盒堆栈"), # A01~B03, 存放枪头盒
|
||||
}
|
||||
self.warehouse_locations = {
|
||||
"堆栈1": Coordinate(0.0, 430.0, 0.0),
|
||||
"堆栈2": Coordinate(2550.0, 430.0, 0.0),
|
||||
"站内试剂存放堆栈": Coordinate(800.0, 475.0, 0.0),
|
||||
"堆栈1左": Coordinate(0.0, 430.0, 0.0), # 左侧位置
|
||||
"堆栈1右": Coordinate(2500.0, 430.0, 0.0), # 右侧位置
|
||||
"站内试剂存放堆栈": Coordinate(1100.0, 475.0, 0.0),
|
||||
"移液站内10%分装液体准备仓库": Coordinate(1500.0, 300.0, 0.0),
|
||||
"站内Tip盒堆栈": Coordinate(1800.0, 300.0, 0.0), # TODO: 根据实际位置调整坐标
|
||||
}
|
||||
self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90)
|
||||
|
||||
@@ -38,7 +60,7 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
|
||||
|
||||
class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
||||
def __init__(
|
||||
self,
|
||||
self,
|
||||
name: str = "PolymerPreparationStation_Deck",
|
||||
size_x: float = 2700.0,
|
||||
size_y: float = 1080.0,
|
||||
@@ -70,7 +92,7 @@ class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
||||
|
||||
class BIOYOND_YB_Deck(Deck):
|
||||
def __init__(
|
||||
self,
|
||||
self,
|
||||
name: str = "YB_Deck",
|
||||
size_x: float = 4150,
|
||||
size_y: float = 1400.0,
|
||||
@@ -114,7 +136,7 @@ class BIOYOND_YB_Deck(Deck):
|
||||
|
||||
for warehouse_name, warehouse in self.warehouses.items():
|
||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||
|
||||
|
||||
def YB_Deck(name: str) -> Deck:
|
||||
by=BIOYOND_YB_Deck(name=name)
|
||||
by.setup()
|
||||
|
||||
@@ -2,7 +2,7 @@ from unilabos.resources.warehouse import WareHouse, warehouse_factory
|
||||
|
||||
|
||||
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
||||
"""创建BioYond 4x1x4仓库"""
|
||||
"""创建BioYond 4x4x1仓库 (左侧堆栈: A01~D04)"""
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=4,
|
||||
@@ -15,6 +15,25 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
||||
item_dy=106.0,
|
||||
item_dz=130.0,
|
||||
category="warehouse",
|
||||
col_offset=0, # 从01开始: A01, A02, A03, A04
|
||||
)
|
||||
|
||||
|
||||
def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse:
|
||||
"""创建BioYond 4x4x1仓库 (右侧堆栈: A05~D08)"""
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=4,
|
||||
num_items_y=4,
|
||||
num_items_z=1,
|
||||
dx=10.0,
|
||||
dy=10.0,
|
||||
dz=10.0,
|
||||
item_dx=147.0,
|
||||
item_dy=106.0,
|
||||
item_dz=130.0,
|
||||
category="warehouse",
|
||||
col_offset=4, # 从05开始: A05, A06, A07, A08
|
||||
)
|
||||
|
||||
|
||||
@@ -158,4 +177,72 @@ def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
|
||||
item_dz=120.0,
|
||||
category="warehouse",
|
||||
removed_positions=None
|
||||
)
|
||||
|
||||
|
||||
def bioyond_warehouse_1x8x4(name: str) -> WareHouse:
|
||||
"""创建BioYond 8x4x1反应站堆栈(A01~D08)"""
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=8, # 8列(01-08)
|
||||
num_items_y=4, # 4行(A-D)
|
||||
num_items_z=1, # 1层
|
||||
dx=10.0,
|
||||
dy=10.0,
|
||||
dz=10.0,
|
||||
item_dx=147.0,
|
||||
item_dy=106.0,
|
||||
item_dz=130.0,
|
||||
category="warehouse",
|
||||
)
|
||||
|
||||
|
||||
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
|
||||
"""创建BioYond站内试剂存放堆栈(A01~A02, 1行×2列)"""
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=2, # 2列(01-02)
|
||||
num_items_y=1, # 1行(A)
|
||||
num_items_z=1, # 1层
|
||||
dx=10.0,
|
||||
dy=10.0,
|
||||
dz=10.0,
|
||||
item_dx=137.0,
|
||||
item_dy=96.0,
|
||||
item_dz=120.0,
|
||||
category="warehouse",
|
||||
)
|
||||
|
||||
|
||||
def bioyond_warehouse_liquid_preparation(name: str) -> WareHouse:
|
||||
"""创建BioYond移液站内10%分装液体准备仓库(A01~B04)"""
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=4, # 4列(01-04)
|
||||
num_items_y=2, # 2行(A-B)
|
||||
num_items_z=1, # 1层
|
||||
dx=10.0,
|
||||
dy=10.0,
|
||||
dz=10.0,
|
||||
item_dx=137.0,
|
||||
item_dy=96.0,
|
||||
item_dz=120.0,
|
||||
category="warehouse",
|
||||
)
|
||||
|
||||
|
||||
def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse:
|
||||
"""创建BioYond站内Tip盒堆栈(A01~B03),用于存放枪头盒"""
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=3, # 3列(01-03)
|
||||
num_items_y=2, # 2行(A-B)
|
||||
num_items_z=1, # 1层
|
||||
dx=10.0,
|
||||
dy=10.0,
|
||||
dz=10.0,
|
||||
item_dx=137.0,
|
||||
item_dy=96.0,
|
||||
item_dz=120.0,
|
||||
category="warehouse",
|
||||
)
|
||||
@@ -580,6 +580,8 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
|
||||
"trash": "trash",
|
||||
"deck": "deck",
|
||||
"tip_rack": "tip_rack",
|
||||
"warehouse": "warehouse",
|
||||
"container": "container",
|
||||
}
|
||||
if source in replace_info:
|
||||
return replace_info[source]
|
||||
@@ -632,9 +634,24 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
||||
type_mapping.get(material.get("typeName"), ("RegularContainer", ""))[0] if type_mapping else "RegularContainer"
|
||||
)
|
||||
|
||||
plr_material: ResourcePLR = initialize_resource(
|
||||
plr_material_result = initialize_resource(
|
||||
{"name": material["name"], "class": className}, resource_type=ResourcePLR
|
||||
)
|
||||
|
||||
# initialize_resource 可能返回列表或单个对象
|
||||
if isinstance(plr_material_result, list):
|
||||
if len(plr_material_result) == 0:
|
||||
logger.warning(f"物料 {material['name']} 初始化失败,跳过")
|
||||
continue
|
||||
plr_material = plr_material_result[0]
|
||||
else:
|
||||
plr_material = plr_material_result
|
||||
|
||||
# 确保 plr_material 是 ResourcePLR 实例
|
||||
if not isinstance(plr_material, ResourcePLR):
|
||||
logger.warning(f"物料 {material['name']} 不是有效的 ResourcePLR 实例,类型: {type(plr_material)}")
|
||||
continue
|
||||
|
||||
plr_material.code = material.get("code", "") and material.get("barCode", "") or ""
|
||||
plr_material.unilabos_uuid = str(uuid.uuid4())
|
||||
|
||||
@@ -659,25 +676,66 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
||||
]
|
||||
bottle.code = detail.get("code", "")
|
||||
else:
|
||||
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
|
||||
bottle.tracker.liquids = [
|
||||
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
|
||||
]
|
||||
# 只对有 capacity 属性的容器(液体容器)处理液体追踪
|
||||
if hasattr(plr_material, 'capacity'):
|
||||
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
|
||||
bottle.tracker.liquids = [
|
||||
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
|
||||
]
|
||||
|
||||
plr_materials.append(plr_material)
|
||||
|
||||
if deck and hasattr(deck, "warehouses"):
|
||||
for loc in material.get("locations", []):
|
||||
if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses:
|
||||
warehouse = deck.warehouses[loc["whName"]]
|
||||
idx = (
|
||||
(loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y
|
||||
+ (loc.get("x", 0) - 1) * warehouse.num_items_x
|
||||
+ (loc.get("z", 0) - 1)
|
||||
)
|
||||
wh_name = loc.get("whName")
|
||||
|
||||
# 特殊处理: Bioyond的"堆栈1"需要映射到"堆栈1左"或"堆栈1右"
|
||||
# 根据列号(x)判断: 1-4映射到左侧, 5-8映射到右侧
|
||||
if wh_name == "堆栈1":
|
||||
x_val = loc.get("x", 1)
|
||||
if 1 <= x_val <= 4:
|
||||
wh_name = "堆栈1左"
|
||||
elif 5 <= x_val <= 8:
|
||||
wh_name = "堆栈1右"
|
||||
else:
|
||||
logger.warning(f"物料 {material['name']} 的列号 x={x_val} 超出范围,无法映射到堆栈1左或堆栈1右")
|
||||
continue
|
||||
|
||||
if hasattr(deck, "warehouses") and wh_name in deck.warehouses:
|
||||
warehouse = deck.warehouses[wh_name]
|
||||
|
||||
# Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1)
|
||||
# PyLabRobot warehouse是列优先存储: A01,B01,C01,D01, A02,B02,C02,D02, ...
|
||||
x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D)
|
||||
y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...)
|
||||
z = loc.get("z", 1) # 层号 (1-based, 通常为1)
|
||||
|
||||
# 如果是右侧堆栈,需要调整列号 (5→1, 6→2, 7→3, 8→4)
|
||||
if wh_name == "堆栈1右":
|
||||
y = y - 4 # 将5-8映射到1-4
|
||||
|
||||
# 特殊处理:对于1行×N列的横向warehouse(如站内试剂存放堆栈)
|
||||
# Bioyond的y坐标表示线性位置序号,而不是列号
|
||||
if warehouse.num_items_y == 1:
|
||||
# 1行warehouse: 直接用y作为线性索引
|
||||
idx = y - 1
|
||||
logger.debug(f"1行warehouse {wh_name}: y={y} → idx={idx}")
|
||||
else:
|
||||
# 多行warehouse: 使用列优先索引 (与Bioyond坐标系统一致)
|
||||
# warehouse keys顺序: A01,B01,C01,D01, A02,B02,C02,D02, ...
|
||||
# 索引计算: idx = (col-1) * num_rows + (row-1) + (layer-1) * (rows * cols)
|
||||
row_idx = x - 1 # x表示行: 转为0-based
|
||||
col_idx = y - 1 # y表示列: 转为0-based
|
||||
layer_idx = z - 1 # 转为0-based
|
||||
idx = layer_idx * (warehouse.num_items_x * warehouse.num_items_y) + col_idx * warehouse.num_items_y + row_idx
|
||||
logger.debug(f"多行warehouse {wh_name}: x={x}(行),y={y}(列) → row={row_idx},col={col_idx} → idx={idx}")
|
||||
|
||||
if 0 <= idx < warehouse.capacity:
|
||||
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
|
||||
warehouse[idx] = plr_material
|
||||
logger.debug(f"✅ 物料 {material['name']} 放置到 {wh_name}[{idx}] (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')})")
|
||||
else:
|
||||
logger.warning(f"物料 {material['name']} 的索引 {idx} 超出仓库 {wh_name} 容量 {warehouse.capacity}")
|
||||
|
||||
return plr_materials
|
||||
|
||||
@@ -714,8 +772,8 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict
|
||||
bottle = resource[0] if resource.capacity > 0 else resource
|
||||
material = {
|
||||
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
||||
"name": resource.get("name", ""),
|
||||
"unit": "",
|
||||
"name": resource.name if hasattr(resource, "name") else "",
|
||||
"unit": "个", # 修复:Bioyond API 要求 unit 字段不能为空
|
||||
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
|
||||
"Parameters": "{}"
|
||||
}
|
||||
@@ -759,6 +817,8 @@ def initialize_resource(resource_config: dict, resource_type: Any = None) -> Uni
|
||||
elif type(resource_class_config) == str:
|
||||
# Allow special resource class names to be used
|
||||
if resource_class_config not in lab_registry.resource_type_registry:
|
||||
logger.warning(f"❌ 类 {resource_class_config} 不在 registry 中,返回原始配置")
|
||||
logger.debug(f" 可用的类: {list(lab_registry.resource_type_registry.keys())[:10]}...")
|
||||
return [resource_config]
|
||||
# If the resource class is a string, look up the class in the
|
||||
# resource_type_registry and import it
|
||||
|
||||
@@ -23,6 +23,7 @@ def warehouse_factory(
|
||||
empty: bool = False,
|
||||
category: str = "warehouse",
|
||||
model: Optional[str] = None,
|
||||
col_offset: int = 0, # 新增:列起始偏移量,用于生成A05-D08等命名
|
||||
):
|
||||
# 创建16个板架位 (4层 x 4位置)
|
||||
locations = []
|
||||
@@ -44,9 +45,11 @@ def warehouse_factory(
|
||||
name_prefix=name,
|
||||
)
|
||||
len_x, len_y = (num_items_x, num_items_y) if num_items_z == 1 else (num_items_y, num_items_z) if num_items_x == 1 else (num_items_x, num_items_z)
|
||||
keys = [f"{LETTERS[j]}{i + 1}" for i in range(len_x) for j in range(len_y)]
|
||||
# 应用列偏移量,支持A05-D08等命名
|
||||
# 使用列优先顺序生成keys (与Bioyond坐标系统一致): A01,B01,C01,D01, A02,B02,C02,D02, ...
|
||||
keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for i in range(len_x) for j in range(len_y)]
|
||||
sites = {i: site for i, site in zip(keys, _sites.values())}
|
||||
|
||||
|
||||
return WareHouse(
|
||||
name=name,
|
||||
size_x=dx + item_dx * num_items_x,
|
||||
|
||||
@@ -848,9 +848,15 @@ class DeviceNodeResourceTracker(object):
|
||||
extra: extra字典值
|
||||
"""
|
||||
if isinstance(resource, dict):
|
||||
resource["extra"] = extra
|
||||
# ⭐ 修复:合并extra而不是覆盖
|
||||
current_extra = resource.get("extra", {})
|
||||
current_extra.update(extra)
|
||||
resource["extra"] = current_extra
|
||||
else:
|
||||
setattr(resource, "unilabos_extra", extra)
|
||||
# ⭐ 修复:合并unilabos_extra而不是覆盖
|
||||
current_extra = getattr(resource, "unilabos_extra", {})
|
||||
current_extra.update(extra)
|
||||
setattr(resource, "unilabos_extra", current_extra)
|
||||
|
||||
def _traverse_and_process(self, resource, process_func) -> int:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user