mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 21:11:12 +00:00
Compare commits
7 Commits
workstatio
...
39bb7dc627
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39bb7dc627 | ||
|
|
0fda155f55 | ||
|
|
6e3eacd2f0 | ||
|
|
062f1a2153 | ||
|
|
61e8d67800 | ||
|
|
d0884cdbd8 | ||
|
|
545ea45024 |
@@ -24,6 +24,7 @@ extensions = [
|
|||||||
"sphinx.ext.autodoc",
|
"sphinx.ext.autodoc",
|
||||||
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
|
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
|
||||||
"sphinx_rtd_theme",
|
"sphinx_rtd_theme",
|
||||||
|
"sphinxcontrib.mermaid"
|
||||||
]
|
]
|
||||||
|
|
||||||
source_suffix = {
|
source_suffix = {
|
||||||
@@ -42,6 +43,8 @@ myst_enable_extensions = [
|
|||||||
"substitution",
|
"substitution",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
myst_fence_as_directive = ["mermaid"]
|
||||||
|
|
||||||
templates_path = ["_templates"]
|
templates_path = ["_templates"]
|
||||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
|
|
||||||
@@ -203,3 +206,5 @@ def generate_action_includes(app):
|
|||||||
|
|
||||||
def setup(app):
|
def setup(app):
|
||||||
app.connect("builder-inited", generate_action_includes)
|
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`
|
### `SendCmd`
|
||||||
|
|
||||||
```{literalinclude} ../../unilabos_msgs/action/SendCmd.action
|
```{literalinclude} ../../unilabos_msgs/action/SendCmd.action
|
||||||
:language: yaml
|
: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#),包含有机合成实验中常见的操作,如加热、搅拌、冷却等。
|
Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab.io/chemputer/xdl/standard/full_steps_specification.html#),包含有机合成实验中常见的操作,如加热、搅拌、冷却等。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### `Clean`
|
### `Clean`
|
||||||
|
|
||||||
```{literalinclude} ../../unilabos_msgs/action/Clean.action
|
```{literalinclude} ../../unilabos_msgs/action/Clean.action
|
||||||
:language: yaml
|
: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`
|
### `HeatChillStart`
|
||||||
|
|
||||||
@@ -90,7 +28,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `HeatChillStop`
|
### `HeatChillStop`
|
||||||
|
|
||||||
@@ -98,7 +36,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `PumpTransfer`
|
### `PumpTransfer`
|
||||||
|
|
||||||
@@ -106,195 +44,12 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
|||||||
:language: yaml
|
: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),包含生物实验中常见的操作,如移液、混匀、离心等。
|
Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.org/user_guide/index.html),包含生物实验中常见的操作,如移液、混匀、离心等。
|
||||||
|
|
||||||
### `LiquidHandlerAspirate`
|
|
||||||
|
|
||||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAspirate.action
|
|
||||||
:language: yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `LiquidHandlerDiscardTips`
|
### `LiquidHandlerDiscardTips`
|
||||||
|
|
||||||
@@ -302,15 +57,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `LiquidHandlerDispense`
|
|
||||||
|
|
||||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerDispense.action
|
|
||||||
:language: yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `LiquidHandlerDropTips`
|
### `LiquidHandlerDropTips`
|
||||||
|
|
||||||
@@ -318,7 +65,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `LiquidHandlerDropTips96`
|
### `LiquidHandlerDropTips96`
|
||||||
|
|
||||||
@@ -326,7 +73,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `LiquidHandlerMoveLid`
|
### `LiquidHandlerMoveLid`
|
||||||
|
|
||||||
@@ -334,7 +81,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `LiquidHandlerMovePlate`
|
### `LiquidHandlerMovePlate`
|
||||||
|
|
||||||
@@ -342,7 +89,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `LiquidHandlerMoveResource`
|
### `LiquidHandlerMoveResource`
|
||||||
|
|
||||||
@@ -350,7 +97,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `LiquidHandlerPickUpTips`
|
### `LiquidHandlerPickUpTips`
|
||||||
|
|
||||||
@@ -358,7 +105,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `LiquidHandlerPickUpTips96`
|
### `LiquidHandlerPickUpTips96`
|
||||||
|
|
||||||
@@ -366,7 +113,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `LiquidHandlerReturnTips`
|
### `LiquidHandlerReturnTips`
|
||||||
|
|
||||||
@@ -374,7 +121,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `LiquidHandlerReturnTips96`
|
### `LiquidHandlerReturnTips96`
|
||||||
|
|
||||||
@@ -382,7 +129,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `LiquidHandlerStamp`
|
### `LiquidHandlerStamp`
|
||||||
|
|
||||||
@@ -390,129 +137,17 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
: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`
|
### `AGVTransfer`
|
||||||
|
|
||||||
```{literalinclude} ../../unilabos_msgs/action/AGVTransfer.action
|
```{literalinclude} ../../unilabos_msgs/action/AGVTransfer.action
|
||||||
:language: yaml
|
:language: yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `WorkStationRun`
|
### `WorkStationRun`
|
||||||
|
|
||||||
@@ -520,64 +155,12 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
|||||||
:language: yaml
|
: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`:
|
Uni-Lab 机械臂、机器人、夹爪和导航指令集沿用 ROS2 的 `control_msgs` 和 `nav2_msgs`:
|
||||||
|
|
||||||
|
|
||||||
### `FollowJointTrajectory`
|
### `FollowJointTrajectory`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -645,8 +228,7 @@ trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_error
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `GripperCommand`
|
### `GripperCommand`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -664,19 +246,42 @@ bool reached_goal # True iff the gripper position has reached the commanded setp
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `JointTrajectory`
|
### `JointTrajectory`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
trajectory_msgs/JointTrajectory trajectory
|
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`
|
### `PointHead`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -686,13 +291,12 @@ string pointing_frame
|
|||||||
builtin_interfaces/Duration min_duration
|
builtin_interfaces/Duration min_duration
|
||||||
float64 max_velocity
|
float64 max_velocity
|
||||||
---
|
---
|
||||||
|
|
||||||
---
|
---
|
||||||
float64 pointing_angle_error
|
float64 pointing_angle_error
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `SingleJointPosition`
|
### `SingleJointPosition`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -700,16 +304,15 @@ float64 position
|
|||||||
builtin_interfaces/Duration min_duration
|
builtin_interfaces/Duration min_duration
|
||||||
float64 max_velocity
|
float64 max_velocity
|
||||||
---
|
---
|
||||||
|
|
||||||
---
|
---
|
||||||
std_msgs/Header header
|
std_msgs/Header header
|
||||||
float64 position
|
float64 position
|
||||||
float64 velocity
|
float64 velocity
|
||||||
float64 error
|
float64 error
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `AssistedTeleop`
|
### `AssistedTeleop`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -721,10 +324,10 @@ builtin_interfaces/Duration total_elapsed_time
|
|||||||
---
|
---
|
||||||
#feedback
|
#feedback
|
||||||
builtin_interfaces/Duration current_teleop_duration
|
builtin_interfaces/Duration current_teleop_duration
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `BackUp`
|
### `BackUp`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -738,10 +341,10 @@ builtin_interfaces/Duration total_elapsed_time
|
|||||||
---
|
---
|
||||||
#feedback definition
|
#feedback definition
|
||||||
float32 distance_traveled
|
float32 distance_traveled
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `ComputePathThroughPoses`
|
### `ComputePathThroughPoses`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -756,10 +359,10 @@ nav_msgs/Path path
|
|||||||
builtin_interfaces/Duration planning_time
|
builtin_interfaces/Duration planning_time
|
||||||
---
|
---
|
||||||
#feedback definition
|
#feedback definition
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `ComputePathToPose`
|
### `ComputePathToPose`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -774,10 +377,10 @@ nav_msgs/Path path
|
|||||||
builtin_interfaces/Duration planning_time
|
builtin_interfaces/Duration planning_time
|
||||||
---
|
---
|
||||||
#feedback definition
|
#feedback definition
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `DriveOnHeading`
|
### `DriveOnHeading`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -791,10 +394,10 @@ builtin_interfaces/Duration total_elapsed_time
|
|||||||
---
|
---
|
||||||
#feedback definition
|
#feedback definition
|
||||||
float32 distance_traveled
|
float32 distance_traveled
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `DummyBehavior`
|
### `DummyBehavior`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -805,10 +408,10 @@ std_msgs/String command
|
|||||||
builtin_interfaces/Duration total_elapsed_time
|
builtin_interfaces/Duration total_elapsed_time
|
||||||
---
|
---
|
||||||
#feedback definition
|
#feedback definition
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `FollowPath`
|
### `FollowPath`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -823,10 +426,10 @@ std_msgs/Empty result
|
|||||||
#feedback definition
|
#feedback definition
|
||||||
float32 distance_to_goal
|
float32 distance_to_goal
|
||||||
float32 speed
|
float32 speed
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `FollowWaypoints`
|
### `FollowWaypoints`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -838,10 +441,10 @@ int32[] missed_waypoints
|
|||||||
---
|
---
|
||||||
#feedback definition
|
#feedback definition
|
||||||
uint32 current_waypoint
|
uint32 current_waypoint
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `NavigateThroughPoses`
|
### `NavigateThroughPoses`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -859,10 +462,10 @@ builtin_interfaces/Duration estimated_time_remaining
|
|||||||
int16 number_of_recoveries
|
int16 number_of_recoveries
|
||||||
float32 distance_remaining
|
float32 distance_remaining
|
||||||
int16 number_of_poses_remaining
|
int16 number_of_poses_remaining
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `NavigateToPose`
|
### `NavigateToPose`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -879,10 +482,10 @@ builtin_interfaces/Duration navigation_time
|
|||||||
builtin_interfaces/Duration estimated_time_remaining
|
builtin_interfaces/Duration estimated_time_remaining
|
||||||
int16 number_of_recoveries
|
int16 number_of_recoveries
|
||||||
float32 distance_remaining
|
float32 distance_remaining
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `SmoothPath`
|
### `SmoothPath`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -898,10 +501,10 @@ builtin_interfaces/Duration smoothing_duration
|
|||||||
bool was_completed
|
bool was_completed
|
||||||
---
|
---
|
||||||
#feedback definition
|
#feedback definition
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `Spin`
|
### `Spin`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -914,10 +517,10 @@ builtin_interfaces/Duration total_elapsed_time
|
|||||||
---
|
---
|
||||||
#feedback definition
|
#feedback definition
|
||||||
float32 angular_distance_traveled
|
float32 angular_distance_traveled
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
----
|
||||||
|
|
||||||
### `Wait`
|
### `Wait`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -929,6 +532,7 @@ builtin_interfaces/Duration total_elapsed_time
|
|||||||
---
|
---
|
||||||
#feedback definition
|
#feedback definition
|
||||||
builtin_interfaces/Duration time_left
|
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_device
|
||||||
developer_guide/add_action
|
developer_guide/add_action
|
||||||
developer_guide/actions
|
developer_guide/actions
|
||||||
|
developer_guide/workstation_architecture
|
||||||
developer_guide/add_protocol
|
developer_guide/add_protocol
|
||||||
developer_guide/add_batteryPLC
|
|
||||||
developer_guide/materials_tutorial.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 接口文档
|
## 接口文档
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
sphinx>=7.0.0
|
sphinx>=7.0.0
|
||||||
sphinx-rtd-theme>=2.0.0
|
sphinx-rtd-theme>=2.0.0
|
||||||
myst-parser>=2.0.0
|
myst-parser>=2.0.0
|
||||||
|
sphinxcontrib-mermaid
|
||||||
|
|
||||||
# 用于支持Jupyter notebook文档
|
# 用于支持Jupyter notebook文档
|
||||||
myst-nb>=1.0.0
|
myst-nb>=1.0.0
|
||||||
|
|||||||
@@ -24,13 +24,42 @@
|
|||||||
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
||||||
},
|
},
|
||||||
"material_type_mappings": {
|
"material_type_mappings": {
|
||||||
"烧杯": ["BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"],
|
"烧杯": [
|
||||||
"试剂瓶": ["BIOYOND_PolymerStation_1BottleCarrier", ""],
|
"BIOYOND_PolymerStation_1FlaskCarrier",
|
||||||
"样品板": ["BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"],
|
"3a14196b-24f2-ca49-9081-0cab8021bf1a"
|
||||||
"分装板": ["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"],
|
"BIOYOND_PolymerStation_1BottleCarrier",
|
||||||
"10%分装小瓶": ["BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"]
|
""
|
||||||
|
],
|
||||||
|
"样品板": [
|
||||||
|
"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": {
|
"deck": {
|
||||||
@@ -46,8 +75,7 @@
|
|||||||
{
|
{
|
||||||
"id": "Bioyond_Deck",
|
"id": "Bioyond_Deck",
|
||||||
"name": "Bioyond_Deck",
|
"name": "Bioyond_Deck",
|
||||||
"children": [
|
"children": [],
|
||||||
],
|
|
||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "deck",
|
"type": "deck",
|
||||||
"class": "BIOYOND_PolymerReactionStation_Deck",
|
"class": "BIOYOND_PolymerReactionStation_Deck",
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Dict, Any, Optional, List
|
from typing import Dict, Any, List
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class SmartPumpController:
|
class SmartPumpController:
|
||||||
@@ -14,6 +15,8 @@ class SmartPumpController:
|
|||||||
适用于实验室自动化系统中的液体处理任务。
|
适用于实验室自动化系统中的液体处理任务。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = "smart_pump_01", port: str = "/dev/ttyUSB0"):
|
def __init__(self, device_id: str = "smart_pump_01", port: str = "/dev/ttyUSB0"):
|
||||||
"""
|
"""
|
||||||
初始化智能泵控制器
|
初始化智能泵控制器
|
||||||
@@ -30,6 +33,9 @@ class SmartPumpController:
|
|||||||
self.calibration_factor = 1.0
|
self.calibration_factor = 1.0
|
||||||
self.pump_mode = "continuous" # continuous, volume, rate
|
self.pump_mode = "continuous" # continuous, volume, rate
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def connect_device(self, timeout: int = 10) -> bool:
|
def connect_device(self, timeout: int = 10) -> bool:
|
||||||
"""
|
"""
|
||||||
连接到泵设备
|
连接到泵设备
|
||||||
@@ -90,7 +96,7 @@ class SmartPumpController:
|
|||||||
pump_time = (volume / flow_rate) * 60 # 转换为秒
|
pump_time = (volume / flow_rate) * 60 # 转换为秒
|
||||||
|
|
||||||
self.current_flow_rate = flow_rate
|
self.current_flow_rate = flow_rate
|
||||||
await asyncio.sleep(min(pump_time, 3.0)) # 模拟泵送过程
|
await self._ros_node.sleep(min(pump_time, 3.0)) # 模拟泵送过程
|
||||||
|
|
||||||
self.total_volume_pumped += volume
|
self.total_volume_pumped += volume
|
||||||
self.current_flow_rate = 0.0
|
self.current_flow_rate = 0.0
|
||||||
@@ -170,6 +176,8 @@ class AdvancedTemperatureController:
|
|||||||
适用于需要精确温度控制的化学反应和材料处理过程。
|
适用于需要精确温度控制的化学反应和材料处理过程。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, controller_id: str = "temp_controller_01"):
|
def __init__(self, controller_id: str = "temp_controller_01"):
|
||||||
"""
|
"""
|
||||||
初始化温度控制器
|
初始化温度控制器
|
||||||
@@ -185,6 +193,9 @@ class AdvancedTemperatureController:
|
|||||||
self.pid_enabled = True
|
self.pid_enabled = True
|
||||||
self.temperature_history: List[Dict] = []
|
self.temperature_history: List[Dict] = []
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def set_target_temperature(self, temperature: float, rate: float = 10.0) -> bool:
|
def set_target_temperature(self, temperature: float, rate: float = 10.0) -> bool:
|
||||||
"""
|
"""
|
||||||
设置目标温度
|
设置目标温度
|
||||||
@@ -238,7 +249,7 @@ class AdvancedTemperatureController:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncio.sleep(step_time)
|
await self._ros_node.sleep(step_time)
|
||||||
|
|
||||||
# 保持历史记录不超过100条
|
# 保持历史记录不超过100条
|
||||||
if len(self.temperature_history) > 100:
|
if len(self.temperature_history) > 100:
|
||||||
@@ -330,6 +341,8 @@ class MultiChannelAnalyzer:
|
|||||||
常用于光谱分析、电化学测量等应用场景。
|
常用于光谱分析、电化学测量等应用场景。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, analyzer_id: str = "analyzer_01", channels: int = 8):
|
def __init__(self, analyzer_id: str = "analyzer_01", channels: int = 8):
|
||||||
"""
|
"""
|
||||||
初始化多通道分析仪
|
初始化多通道分析仪
|
||||||
@@ -344,6 +357,9 @@ class MultiChannelAnalyzer:
|
|||||||
self.is_measuring = False
|
self.is_measuring = False
|
||||||
self.sample_rate = 1000 # Hz
|
self.sample_rate = 1000 # Hz
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def configure_channel(self, channel: int, enabled: bool = True, unit: str = "V") -> bool:
|
def configure_channel(self, channel: int, enabled: bool = True, unit: str = "V") -> bool:
|
||||||
"""
|
"""
|
||||||
配置通道
|
配置通道
|
||||||
@@ -376,7 +392,7 @@ class MultiChannelAnalyzer:
|
|||||||
|
|
||||||
# 模拟数据采集
|
# 模拟数据采集
|
||||||
measurements = []
|
measurements = []
|
||||||
for second in range(duration):
|
for _ in range(duration):
|
||||||
timestamp = asyncio.get_event_loop().time()
|
timestamp = asyncio.get_event_loop().time()
|
||||||
frame_data = {}
|
frame_data = {}
|
||||||
|
|
||||||
@@ -391,7 +407,7 @@ class MultiChannelAnalyzer:
|
|||||||
|
|
||||||
measurements.append({"timestamp": timestamp, "data": frame_data})
|
measurements.append({"timestamp": timestamp, "data": frame_data})
|
||||||
|
|
||||||
await asyncio.sleep(1.0) # 每秒采集一次
|
await self._ros_node.sleep(1.0) # 每秒采集一次
|
||||||
|
|
||||||
self.is_measuring = False
|
self.is_measuring = False
|
||||||
|
|
||||||
@@ -465,6 +481,8 @@ class AutomatedDispenser:
|
|||||||
集成称重功能,确保分配精度和重现性。
|
集成称重功能,确保分配精度和重现性。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, dispenser_id: str = "dispenser_01"):
|
def __init__(self, dispenser_id: str = "dispenser_01"):
|
||||||
"""
|
"""
|
||||||
初始化自动分配器
|
初始化自动分配器
|
||||||
@@ -479,6 +497,9 @@ class AutomatedDispenser:
|
|||||||
self.container_capacity = 1000.0 # mL
|
self.container_capacity = 1000.0 # mL
|
||||||
self.precision_mode = True
|
self.precision_mode = True
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def move_to_position(self, x: float, y: float, z: float) -> bool:
|
def move_to_position(self, x: float, y: float, z: float) -> bool:
|
||||||
"""
|
"""
|
||||||
移动到指定位置
|
移动到指定位置
|
||||||
@@ -517,7 +538,7 @@ class AutomatedDispenser:
|
|||||||
if viscosity == "high":
|
if viscosity == "high":
|
||||||
dispense_time *= 2 # 高粘度液体需要更长时间
|
dispense_time *= 2 # 高粘度液体需要更长时间
|
||||||
|
|
||||||
await asyncio.sleep(min(dispense_time, 5.0)) # 最多等待5秒
|
await self._ros_node.sleep(min(dispense_time, 5.0)) # 最多等待5秒
|
||||||
|
|
||||||
self.dispensed_total += volume
|
self.dispensed_total += volume
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from serial import Serial
|
|||||||
from serial.serialutil import SerialException
|
from serial.serialutil import SerialException
|
||||||
|
|
||||||
from unilabos.messages import Point3D
|
from unilabos.messages import Point3D
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class GrblCNCConnectionError(Exception):
|
class GrblCNCConnectionError(Exception):
|
||||||
@@ -32,6 +33,7 @@ class GrblCNCInfo:
|
|||||||
class GrblCNCAsync:
|
class GrblCNCAsync:
|
||||||
_status: str = "Offline"
|
_status: str = "Offline"
|
||||||
_position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
|
_position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, port: str, address: str = "1", limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, 0, 60)):
|
def __init__(self, port: str, address: str = "1", limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, 0, 60)):
|
||||||
self.port = port
|
self.port = port
|
||||||
@@ -58,6 +60,9 @@ class GrblCNCAsync:
|
|||||||
self._run_future: Optional[Future[Any]] = None
|
self._run_future: Optional[Future[Any]] = None
|
||||||
self._run_lock = Lock()
|
self._run_lock = Lock()
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def _read_all(self):
|
def _read_all(self):
|
||||||
data = self._serial.read_until(b"\n")
|
data = self._serial.read_until(b"\n")
|
||||||
data_decoded = data.decode()
|
data_decoded = data.decode()
|
||||||
@@ -148,7 +153,7 @@ class GrblCNCAsync:
|
|||||||
try:
|
try:
|
||||||
await self._query(command)
|
await self._query(command)
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(0.2) # Wait for 0.5 seconds before polling again
|
await self._ros_node.sleep(0.2) # Wait for 0.5 seconds before polling again
|
||||||
|
|
||||||
status = await self.get_status()
|
status = await self.get_status()
|
||||||
if "Idle" in status:
|
if "Idle" in status:
|
||||||
@@ -214,7 +219,7 @@ class GrblCNCAsync:
|
|||||||
self._pose_number = i
|
self._pose_number = i
|
||||||
self.pose_number_remaining = len(points) - i
|
self.pose_number_remaining = len(points) - i
|
||||||
await self.set_position(point)
|
await self.set_position(point)
|
||||||
await asyncio.sleep(0.5)
|
await self._ros_node.sleep(0.5)
|
||||||
self._step_number = -1
|
self._step_number = -1
|
||||||
|
|
||||||
async def stop_operation(self):
|
async def stop_operation(self):
|
||||||
@@ -235,7 +240,7 @@ class GrblCNCAsync:
|
|||||||
async def open(self):
|
async def open(self):
|
||||||
if self._read_task:
|
if self._read_task:
|
||||||
raise GrblCNCConnectionError
|
raise GrblCNCConnectionError
|
||||||
self._read_task = asyncio.create_task(self._read_loop())
|
self._read_task = self._ros_node.create_task(self._read_loop())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.get_status()
|
await self.get_status()
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import time
|
|||||||
import asyncio
|
import asyncio
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class Point3D(BaseModel):
|
class Point3D(BaseModel):
|
||||||
x: float
|
x: float
|
||||||
@@ -14,10 +16,15 @@ def d(a: Point3D, b: Point3D) -> float:
|
|||||||
|
|
||||||
|
|
||||||
class MockCNCAsync:
|
class MockCNCAsync:
|
||||||
|
_ros_node: BaseROS2DeviceNode["MockCNCAsync"]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
|
self._position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
|
|
||||||
|
def post_create(self, ros_node):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def position(self) -> Point3D:
|
def position(self) -> Point3D:
|
||||||
return self._position
|
return self._position
|
||||||
@@ -38,5 +45,5 @@ class MockCNCAsync:
|
|||||||
self._position.x = current_pos.x + (position.x - current_pos.x) / 20 * (i+1)
|
self._position.x = current_pos.x + (position.x - current_pos.x) / 20 * (i+1)
|
||||||
self._position.y = current_pos.y + (position.y - current_pos.y) / 20 * (i+1)
|
self._position.y = current_pos.y + (position.y - current_pos.y) / 20 * (i+1)
|
||||||
self._position.z = current_pos.z + (position.z - current_pos.z) / 20 * (i+1)
|
self._position.z = current_pos.z + (position.z - current_pos.z) / 20 * (i+1)
|
||||||
await asyncio.sleep(move_time / 20)
|
await self._ros_node.sleep(move_time / 20)
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ from typing import List, Optional, Dict, Any, Union, Tuple
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
# 基础导入
|
# 基础导入
|
||||||
try:
|
try:
|
||||||
from pylabrobot.resources import Deck, Plate, TipRack, Tip, Resource, Well
|
from pylabrobot.resources import Deck, Plate, TipRack, Tip, Resource, Well
|
||||||
|
|
||||||
PYLABROBOT_AVAILABLE = True
|
PYLABROBOT_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# 如果 pylabrobot 不可用,创建基础的模拟类
|
# 如果 pylabrobot 不可用,创建基础的模拟类
|
||||||
@@ -42,17 +45,16 @@ except ImportError:
|
|||||||
class Well(Resource):
|
class Well(Resource):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# LaiYu_Liquid 控制器导入
|
# LaiYu_Liquid 控制器导入
|
||||||
try:
|
try:
|
||||||
from .controllers.pipette_controller import (
|
from .controllers.pipette_controller import PipetteController, TipStatus, LiquidClass, LiquidParameters
|
||||||
PipetteController, TipStatus, LiquidClass, LiquidParameters
|
from .controllers.xyz_controller import XYZController, MachineConfig, CoordinateOrigin, MotorAxis
|
||||||
)
|
|
||||||
from .controllers.xyz_controller import (
|
|
||||||
XYZController, MachineConfig, CoordinateOrigin, MotorAxis
|
|
||||||
)
|
|
||||||
CONTROLLERS_AVAILABLE = True
|
CONTROLLERS_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CONTROLLERS_AVAILABLE = False
|
CONTROLLERS_AVAILABLE = False
|
||||||
|
|
||||||
# 创建模拟的控制器类
|
# 创建模拟的控制器类
|
||||||
class PipetteController:
|
class PipetteController:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -71,17 +73,20 @@ except ImportError:
|
|||||||
def connect_device(self):
|
def connect_device(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LaiYuLiquidError(RuntimeError):
|
class LaiYuLiquidError(RuntimeError):
|
||||||
"""LaiYu_Liquid 设备异常"""
|
"""LaiYu_Liquid 设备异常"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LaiYuLiquidConfig:
|
class LaiYuLiquidConfig:
|
||||||
"""LaiYu_Liquid 设备配置"""
|
"""LaiYu_Liquid 设备配置"""
|
||||||
|
|
||||||
port: str = "/dev/cu.usbserial-3130" # RS485转USB端口
|
port: str = "/dev/cu.usbserial-3130" # RS485转USB端口
|
||||||
address: int = 1 # 设备地址
|
address: int = 1 # 设备地址
|
||||||
baudrate: int = 9600 # 波特率
|
baudrate: int = 9600 # 波特率
|
||||||
@@ -155,7 +160,17 @@ class LaiYuLiquidDeck:
|
|||||||
class LaiYuLiquidContainer:
|
class LaiYuLiquidContainer:
|
||||||
"""LaiYu_Liquid 容器类"""
|
"""LaiYu_Liquid 容器类"""
|
||||||
|
|
||||||
def __init__(self, name: str, size_x: float = 0, size_y: float = 0, size_z: float = 0, container_type: str = "", volume: float = 0.0, max_volume: float = 1000.0, lid_height: float = 0.0):
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float = 0,
|
||||||
|
size_y: float = 0,
|
||||||
|
size_z: float = 0,
|
||||||
|
container_type: str = "",
|
||||||
|
volume: float = 0.0,
|
||||||
|
max_volume: float = 1000.0,
|
||||||
|
lid_height: float = 0.0,
|
||||||
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.size_x = size_x
|
self.size_x = size_x
|
||||||
self.size_y = size_y
|
self.size_y = size_y
|
||||||
@@ -197,17 +212,22 @@ class LaiYuLiquidContainer:
|
|||||||
|
|
||||||
def assign_child_resource(self, resource, location=None):
|
def assign_child_resource(self, resource, location=None):
|
||||||
"""分配子资源 - 与 PyLabRobot 资源管理系统兼容"""
|
"""分配子资源 - 与 PyLabRobot 资源管理系统兼容"""
|
||||||
if hasattr(resource, 'name'):
|
if hasattr(resource, "name"):
|
||||||
self.child_resources[resource.name] = {
|
self.child_resources[resource.name] = {"resource": resource, "location": location}
|
||||||
'resource': resource,
|
|
||||||
'location': location
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class LaiYuLiquidTipRack:
|
class LaiYuLiquidTipRack:
|
||||||
"""LaiYu_Liquid 吸头架类"""
|
"""LaiYu_Liquid 吸头架类"""
|
||||||
|
|
||||||
def __init__(self, name: str, size_x: float = 0, size_y: float = 0, size_z: float = 0, tip_count: int = 96, tip_volume: float = 1000.0):
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float = 0,
|
||||||
|
size_y: float = 0,
|
||||||
|
size_z: float = 0,
|
||||||
|
tip_count: int = 96,
|
||||||
|
tip_volume: float = 1000.0,
|
||||||
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.size_x = size_x
|
self.size_x = size_x
|
||||||
self.size_y = size_y
|
self.size_y = size_y
|
||||||
@@ -240,10 +260,7 @@ class LaiYuLiquidTipRack:
|
|||||||
|
|
||||||
def assign_child_resource(self, resource, location=None):
|
def assign_child_resource(self, resource, location=None):
|
||||||
"""分配子资源到指定位置"""
|
"""分配子资源到指定位置"""
|
||||||
self.child_resources[resource.name] = {
|
self.child_resources[resource.name] = {"resource": resource, "location": location}
|
||||||
'resource': resource,
|
|
||||||
'location': location
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_module_info():
|
def get_module_info():
|
||||||
@@ -253,24 +270,17 @@ def get_module_info():
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "LaiYu液体处理工作站模块,提供移液器控制、XYZ轴控制和资源管理功能",
|
"description": "LaiYu液体处理工作站模块,提供移液器控制、XYZ轴控制和资源管理功能",
|
||||||
"author": "UniLabOS Team",
|
"author": "UniLabOS Team",
|
||||||
"capabilities": [
|
"capabilities": ["移液器控制", "XYZ轴运动控制", "吸头架管理", "板和容器管理", "资源位置管理"],
|
||||||
"移液器控制",
|
"dependencies": {"required": ["serial"], "optional": ["pylabrobot"]},
|
||||||
"XYZ轴运动控制",
|
|
||||||
"吸头架管理",
|
|
||||||
"板和容器管理",
|
|
||||||
"资源位置管理"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"required": ["serial"],
|
|
||||||
"optional": ["pylabrobot"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LaiYuLiquidBackend:
|
class LaiYuLiquidBackend:
|
||||||
"""LaiYu_Liquid 硬件通信后端"""
|
"""LaiYu_Liquid 硬件通信后端"""
|
||||||
|
|
||||||
def __init__(self, config: LaiYuLiquidConfig, deck: Optional['LaiYuLiquidDeck'] = None):
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
|
def __init__(self, config: LaiYuLiquidConfig, deck: Optional["LaiYuLiquidDeck"] = None):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.deck = deck # 工作台引用,用于获取资源位置信息
|
self.deck = deck # 工作台引用,用于获取资源位置信息
|
||||||
self.pipette_controller = None
|
self.pipette_controller = None
|
||||||
@@ -283,6 +293,9 @@ class LaiYuLiquidBackend:
|
|||||||
self.tip_attached = False
|
self.tip_attached = False
|
||||||
self.current_volume = 0.0
|
self.current_volume = 0.0
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def _validate_position(self, x: float, y: float, z: float) -> bool:
|
def _validate_position(self, x: float, y: float, z: float) -> bool:
|
||||||
"""验证位置是否在安全范围内"""
|
"""验证位置是否在安全范围内"""
|
||||||
try:
|
try:
|
||||||
@@ -348,7 +361,7 @@ class LaiYuLiquidBackend:
|
|||||||
safe_position = (
|
safe_position = (
|
||||||
self.config.deck_width / 2, # 工作台中心X
|
self.config.deck_width / 2, # 工作台中心X
|
||||||
self.config.deck_height / 2, # 工作台中心Y
|
self.config.deck_height / 2, # 工作台中心Y
|
||||||
self.config.safe_height # 安全高度Z
|
self.config.safe_height, # 安全高度Z
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self._validate_position(*safe_position):
|
if not self._validate_position(*safe_position):
|
||||||
@@ -375,17 +388,12 @@ class LaiYuLiquidBackend:
|
|||||||
try:
|
try:
|
||||||
if CONTROLLERS_AVAILABLE:
|
if CONTROLLERS_AVAILABLE:
|
||||||
# 初始化移液器控制器
|
# 初始化移液器控制器
|
||||||
self.pipette_controller = PipetteController(
|
self.pipette_controller = PipetteController(port=self.config.port, address=self.config.address)
|
||||||
port=self.config.port,
|
|
||||||
address=self.config.address
|
|
||||||
)
|
|
||||||
|
|
||||||
# 初始化XYZ控制器
|
# 初始化XYZ控制器
|
||||||
machine_config = MachineConfig()
|
machine_config = MachineConfig()
|
||||||
self.xyz_controller = XYZController(
|
self.xyz_controller = XYZController(
|
||||||
port=self.config.port,
|
port=self.config.port, baudrate=self.config.baudrate, machine_config=machine_config
|
||||||
baudrate=self.config.baudrate,
|
|
||||||
machine_config=machine_config
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 连接设备
|
# 连接设备
|
||||||
@@ -412,10 +420,10 @@ class LaiYuLiquidBackend:
|
|||||||
async def stop(self):
|
async def stop(self):
|
||||||
"""停止设备"""
|
"""停止设备"""
|
||||||
try:
|
try:
|
||||||
if self.pipette_controller and hasattr(self.pipette_controller, 'disconnect'):
|
if self.pipette_controller and hasattr(self.pipette_controller, "disconnect"):
|
||||||
await asyncio.to_thread(self.pipette_controller.disconnect)
|
await asyncio.to_thread(self.pipette_controller.disconnect)
|
||||||
|
|
||||||
if self.xyz_controller and hasattr(self.xyz_controller, 'disconnect'):
|
if self.xyz_controller and hasattr(self.xyz_controller, "disconnect"):
|
||||||
await asyncio.to_thread(self.xyz_controller.disconnect)
|
await asyncio.to_thread(self.xyz_controller.disconnect)
|
||||||
|
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
@@ -432,7 +440,7 @@ class LaiYuLiquidBackend:
|
|||||||
raise LaiYuLiquidError("设备未连接")
|
raise LaiYuLiquidError("设备未连接")
|
||||||
|
|
||||||
# 模拟移动
|
# 模拟移动
|
||||||
await asyncio.sleep(0.1) # 模拟移动时间
|
await self._ros_node.sleep(0.1) # 模拟移动时间
|
||||||
self.current_position = (x, y, z)
|
self.current_position = (x, y, z)
|
||||||
logger.debug(f"移动到位置: ({x}, {y}, {z})")
|
logger.debug(f"移动到位置: ({x}, {y}, {z})")
|
||||||
return True
|
return True
|
||||||
@@ -472,9 +480,11 @@ class LaiYuLiquidBackend:
|
|||||||
pickup_z = tip_z - self.config.tip_pickup_force_depth
|
pickup_z = tip_z - self.config.tip_pickup_force_depth
|
||||||
retract_z = tip_z + self.config.tip_pickup_retract_height
|
retract_z = tip_z + self.config.tip_pickup_retract_height
|
||||||
|
|
||||||
if not (self._validate_position(tip_x, tip_y, safe_z) and
|
if not (
|
||||||
self._validate_position(tip_x, tip_y, pickup_z) and
|
self._validate_position(tip_x, tip_y, safe_z)
|
||||||
self._validate_position(tip_x, tip_y, retract_z)):
|
and self._validate_position(tip_x, tip_y, pickup_z)
|
||||||
|
and self._validate_position(tip_x, tip_y, retract_z)
|
||||||
|
):
|
||||||
logger.error("枪头拾取位置超出安全范围")
|
logger.error("枪头拾取位置超出安全范围")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -487,8 +497,7 @@ class LaiYuLiquidBackend:
|
|||||||
safe_z = tip_z + self.config.tip_approach_height
|
safe_z = tip_z + self.config.tip_approach_height
|
||||||
logger.info(f"移动到枪头上方安全位置: ({tip_x:.2f}, {tip_y:.2f}, {safe_z:.2f})")
|
logger.info(f"移动到枪头上方安全位置: ({tip_x:.2f}, {tip_y:.2f}, {safe_z:.2f})")
|
||||||
move_success = await asyncio.to_thread(
|
move_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, tip_x, tip_y, safe_z
|
||||||
tip_x, tip_y, safe_z
|
|
||||||
)
|
)
|
||||||
if not move_success:
|
if not move_success:
|
||||||
logger.error("移动到枪头上方失败")
|
logger.error("移动到枪头上方失败")
|
||||||
@@ -498,22 +507,20 @@ class LaiYuLiquidBackend:
|
|||||||
pickup_z = tip_z - self.config.tip_pickup_force_depth
|
pickup_z = tip_z - self.config.tip_pickup_force_depth
|
||||||
logger.info(f"Z轴下降到枪头拾取位置: {pickup_z:.2f}mm")
|
logger.info(f"Z轴下降到枪头拾取位置: {pickup_z:.2f}mm")
|
||||||
z_down_success = await asyncio.to_thread(
|
z_down_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, tip_x, tip_y, pickup_z
|
||||||
tip_x, tip_y, pickup_z
|
|
||||||
)
|
)
|
||||||
if not z_down_success:
|
if not z_down_success:
|
||||||
logger.error("Z轴下降到枪头位置失败")
|
logger.error("Z轴下降到枪头位置失败")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 3. 等待一小段时间确保枪头牢固附着
|
# 3. 等待一小段时间确保枪头牢固附着
|
||||||
await asyncio.sleep(0.2)
|
await self._ros_node.sleep(0.2)
|
||||||
|
|
||||||
# 4. Z轴上升到回退高度
|
# 4. Z轴上升到回退高度
|
||||||
retract_z = tip_z + self.config.tip_pickup_retract_height
|
retract_z = tip_z + self.config.tip_pickup_retract_height
|
||||||
logger.info(f"Z轴上升到回退高度: {retract_z:.2f}mm")
|
logger.info(f"Z轴上升到回退高度: {retract_z:.2f}mm")
|
||||||
z_up_success = await asyncio.to_thread(
|
z_up_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, tip_x, tip_y, retract_z
|
||||||
tip_x, tip_y, retract_z
|
|
||||||
)
|
)
|
||||||
if not z_up_success:
|
if not z_up_success:
|
||||||
logger.error("Z轴上升失败")
|
logger.error("Z轴上升失败")
|
||||||
@@ -533,7 +540,7 @@ class LaiYuLiquidBackend:
|
|||||||
else:
|
else:
|
||||||
# 模拟模式
|
# 模拟模式
|
||||||
logger.info("模拟模式:执行枪头拾取动作")
|
logger.info("模拟模式:执行枪头拾取动作")
|
||||||
await asyncio.sleep(1.0) # 模拟整个拾取过程的时间
|
await self._ros_node.sleep(1.0) # 模拟整个拾取过程的时间
|
||||||
self.current_position = (tip_x, tip_y, tip_z + self.config.tip_pickup_retract_height)
|
self.current_position = (tip_x, tip_y, tip_z + self.config.tip_pickup_retract_height)
|
||||||
|
|
||||||
# 6. 标记枪头已附着
|
# 6. 标记枪头已附着
|
||||||
@@ -578,8 +585,10 @@ class LaiYuLiquidBackend:
|
|||||||
safe_z = drop_z + self.config.safe_height
|
safe_z = drop_z + self.config.safe_height
|
||||||
drop_height_z = drop_z + self.config.tip_drop_height
|
drop_height_z = drop_z + self.config.tip_drop_height
|
||||||
|
|
||||||
if not (self._validate_position(drop_x, drop_y, safe_z) and
|
if not (
|
||||||
self._validate_position(drop_x, drop_y, drop_height_z)):
|
self._validate_position(drop_x, drop_y, safe_z)
|
||||||
|
and self._validate_position(drop_x, drop_y, drop_height_z)
|
||||||
|
):
|
||||||
logger.error("枪头丢弃位置超出安全范围")
|
logger.error("枪头丢弃位置超出安全范围")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -592,8 +601,7 @@ class LaiYuLiquidBackend:
|
|||||||
safe_z = drop_z + self.config.tip_drop_height
|
safe_z = drop_z + self.config.tip_drop_height
|
||||||
logger.info(f"移动到丢弃位置上方: ({drop_x:.2f}, {drop_y:.2f}, {safe_z:.2f})")
|
logger.info(f"移动到丢弃位置上方: ({drop_x:.2f}, {drop_y:.2f}, {safe_z:.2f})")
|
||||||
move_success = await asyncio.to_thread(
|
move_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, drop_x, drop_y, safe_z
|
||||||
drop_x, drop_y, safe_z
|
|
||||||
)
|
)
|
||||||
if not move_success:
|
if not move_success:
|
||||||
logger.error("移动到丢弃位置上方失败")
|
logger.error("移动到丢弃位置上方失败")
|
||||||
@@ -602,8 +610,7 @@ class LaiYuLiquidBackend:
|
|||||||
# 2. Z轴下降到丢弃高度
|
# 2. Z轴下降到丢弃高度
|
||||||
logger.info(f"Z轴下降到丢弃高度: {drop_z:.2f}mm")
|
logger.info(f"Z轴下降到丢弃高度: {drop_z:.2f}mm")
|
||||||
z_down_success = await asyncio.to_thread(
|
z_down_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, drop_x, drop_y, drop_z
|
||||||
drop_x, drop_y, drop_z
|
|
||||||
)
|
)
|
||||||
if not z_down_success:
|
if not z_down_success:
|
||||||
logger.error("Z轴下降到丢弃位置失败")
|
logger.error("Z轴下降到丢弃位置失败")
|
||||||
@@ -619,13 +626,12 @@ class LaiYuLiquidBackend:
|
|||||||
logger.warning(f"枪头弹出命令失败: {e}")
|
logger.warning(f"枪头弹出命令失败: {e}")
|
||||||
|
|
||||||
# 4. 等待一小段时间确保枪头完全脱离
|
# 4. 等待一小段时间确保枪头完全脱离
|
||||||
await asyncio.sleep(0.3)
|
await self._ros_node.sleep(0.3)
|
||||||
|
|
||||||
# 5. Z轴上升到安全高度
|
# 5. Z轴上升到安全高度
|
||||||
logger.info(f"Z轴上升到安全高度: {safe_z:.2f}mm")
|
logger.info(f"Z轴上升到安全高度: {safe_z:.2f}mm")
|
||||||
z_up_success = await asyncio.to_thread(
|
z_up_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, drop_x, drop_y, safe_z
|
||||||
drop_x, drop_y, safe_z
|
|
||||||
)
|
)
|
||||||
if not z_up_success:
|
if not z_up_success:
|
||||||
logger.error("Z轴上升失败")
|
logger.error("Z轴上升失败")
|
||||||
@@ -645,7 +651,7 @@ class LaiYuLiquidBackend:
|
|||||||
else:
|
else:
|
||||||
# 模拟模式
|
# 模拟模式
|
||||||
logger.info("模拟模式:执行枪头丢弃动作")
|
logger.info("模拟模式:执行枪头丢弃动作")
|
||||||
await asyncio.sleep(0.8) # 模拟整个丢弃过程的时间
|
await self._ros_node.sleep(0.8) # 模拟整个丢弃过程的时间
|
||||||
self.current_position = (drop_x, drop_y, drop_z + self.config.tip_drop_height)
|
self.current_position = (drop_x, drop_y, drop_z + self.config.tip_drop_height)
|
||||||
|
|
||||||
# 7. 标记枪头已脱离,清空体积
|
# 7. 标记枪头已脱离,清空体积
|
||||||
@@ -671,7 +677,7 @@ class LaiYuLiquidBackend:
|
|||||||
raise LaiYuLiquidError(f"体积超出范围: {volume}")
|
raise LaiYuLiquidError(f"体积超出范围: {volume}")
|
||||||
|
|
||||||
# 模拟吸取
|
# 模拟吸取
|
||||||
await asyncio.sleep(0.3)
|
await self._ros_node.sleep(0.3)
|
||||||
self.current_volume += volume
|
self.current_volume += volume
|
||||||
logger.debug(f"从 {location} 吸取 {volume} μL")
|
logger.debug(f"从 {location} 吸取 {volume} μL")
|
||||||
return True
|
return True
|
||||||
@@ -693,7 +699,7 @@ class LaiYuLiquidBackend:
|
|||||||
raise LaiYuLiquidError(f"分配体积无效: {volume}")
|
raise LaiYuLiquidError(f"分配体积无效: {volume}")
|
||||||
|
|
||||||
# 模拟分配
|
# 模拟分配
|
||||||
await asyncio.sleep(0.3)
|
await self._ros_node.sleep(0.3)
|
||||||
self.current_volume -= volume
|
self.current_volume -= volume
|
||||||
logger.debug(f"向 {location} 分配 {volume} μL")
|
logger.debug(f"向 {location} 分配 {volume} μL")
|
||||||
return True
|
return True
|
||||||
@@ -765,8 +771,9 @@ class LaiYuLiquid:
|
|||||||
await self.backend.stop()
|
await self.backend.stop()
|
||||||
self.is_setup = False
|
self.is_setup = False
|
||||||
|
|
||||||
async def transfer(self, source: str, target: str, volume: float,
|
async def transfer(
|
||||||
tip_rack: str = "tip_rack_1", tip_position: int = 0) -> bool:
|
self, source: str, target: str, volume: float, tip_rack: str = "tip_rack_1", tip_position: int = 0
|
||||||
|
) -> bool:
|
||||||
"""液体转移"""
|
"""液体转移"""
|
||||||
try:
|
try:
|
||||||
if not self.is_setup:
|
if not self.is_setup:
|
||||||
@@ -788,7 +795,7 @@ class LaiYuLiquid:
|
|||||||
("吸取液体", self.backend.aspirate(volume, source)),
|
("吸取液体", self.backend.aspirate(volume, source)),
|
||||||
("移动到目标位置", self.backend.move_to(*target_pos)),
|
("移动到目标位置", self.backend.move_to(*target_pos)),
|
||||||
("分配液体", self.backend.dispense(volume, target)),
|
("分配液体", self.backend.dispense(volume, target)),
|
||||||
("丢弃吸头", self.backend.drop_tip())
|
("丢弃吸头", self.backend.drop_tip()),
|
||||||
]
|
]
|
||||||
|
|
||||||
for step_name, step_coro in steps:
|
for step_name, step_coro in steps:
|
||||||
@@ -823,7 +830,7 @@ class LaiYuLiquid:
|
|||||||
"current_position": self.backend.current_position,
|
"current_position": self.backend.current_position,
|
||||||
"tip_attached": self.backend.tip_attached,
|
"tip_attached": self.backend.tip_attached,
|
||||||
"current_volume": self.backend.current_volume,
|
"current_volume": self.backend.current_volume,
|
||||||
"resources": self.deck.list_resources()
|
"resources": self.deck.list_resources(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -846,7 +853,7 @@ def create_quick_setup() -> LaiYuLiquidDeck:
|
|||||||
create_tip_rack_1000ul,
|
create_tip_rack_1000ul,
|
||||||
create_tip_rack_200ul,
|
create_tip_rack_200ul,
|
||||||
create_96_well_plate,
|
create_96_well_plate,
|
||||||
create_waste_container
|
create_waste_container,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加基本资源
|
# 添加基本资源
|
||||||
@@ -877,5 +884,5 @@ __all__ = [
|
|||||||
"LaiYuLiquidTipRack",
|
"LaiYuLiquidTipRack",
|
||||||
"LaiYuLiquidError",
|
"LaiYuLiquidError",
|
||||||
"create_quick_setup",
|
"create_quick_setup",
|
||||||
"get_module_info"
|
"get_module_info",
|
||||||
]
|
]
|
||||||
@@ -25,6 +25,8 @@ from pylabrobot.resources import (
|
|||||||
Tip,
|
Tip,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class LiquidHandlerMiddleware(LiquidHandler):
|
class LiquidHandlerMiddleware(LiquidHandler):
|
||||||
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool = False, channel_num: int = 8):
|
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool = False, channel_num: int = 8):
|
||||||
@@ -536,6 +538,7 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
||||||
"""Extended LiquidHandler with additional operations."""
|
"""Extended LiquidHandler with additional operations."""
|
||||||
support_touch_tip = True
|
support_touch_tip = True
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool=False, channel_num:int = 8):
|
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool=False, channel_num:int = 8):
|
||||||
"""Initialize a LiquidHandler.
|
"""Initialize a LiquidHandler.
|
||||||
@@ -548,8 +551,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
self.group_info = dict()
|
self.group_info = dict()
|
||||||
super().__init__(backend, deck, simulator, channel_num)
|
super().__init__(backend, deck, simulator, channel_num)
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]):
|
def set_liquid(cls, wells: list[Well], liquid_names: list[str], volumes: list[float]):
|
||||||
"""Set the liquid in a well."""
|
"""Set the liquid in a well."""
|
||||||
for well, liquid_name, volume in zip(wells, liquid_names, volumes):
|
for well, liquid_name, volume in zip(wells, liquid_names, volumes):
|
||||||
well.set_liquids([(liquid_name, volume)]) # type: ignore
|
well.set_liquids([(liquid_name, volume)]) # type: ignore
|
||||||
@@ -1081,7 +1087,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
print(f"Waiting time: {msg}")
|
print(f"Waiting time: {msg}")
|
||||||
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
||||||
print(f"Time to finish: {time.strftime('%H:%M:%S', time.localtime(time.time() + seconds))}")
|
print(f"Time to finish: {time.strftime('%H:%M:%S', time.localtime(time.time() + seconds))}")
|
||||||
await asyncio.sleep(seconds)
|
await self._ros_node.sleep(seconds)
|
||||||
if msg:
|
if msg:
|
||||||
print(f"Done: {msg}")
|
print(f"Done: {msg}")
|
||||||
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from pylabrobot.liquid_handling.standard import (
|
|||||||
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash
|
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash
|
||||||
|
|
||||||
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
|
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class PRCXIError(RuntimeError):
|
class PRCXIError(RuntimeError):
|
||||||
@@ -162,6 +163,10 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
)
|
)
|
||||||
super().__init__(backend=self._unilabos_backend, deck=deck, simulator=simulator, channel_num=channel_num)
|
super().__init__(backend=self._unilabos_backend, deck=deck, simulator=simulator, channel_num=channel_num)
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
super().post_init(ros_node)
|
||||||
|
self._unilabos_backend.post_init(ros_node)
|
||||||
|
|
||||||
def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]):
|
def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]):
|
||||||
return super().set_liquid(wells, liquid_names, volumes)
|
return super().set_liquid(wells, liquid_names, volumes)
|
||||||
|
|
||||||
@@ -424,6 +429,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
|
|
||||||
_num_channels = 8 # 默认通道数为 8
|
_num_channels = 8 # 默认通道数为 8
|
||||||
_is_reset_ok = False
|
_is_reset_ok = False
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_reset_ok(self) -> bool:
|
def is_reset_ok(self) -> bool:
|
||||||
@@ -456,6 +462,9 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
self._execute_setup = setup
|
self._execute_setup = setup
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def create_protocol(self, protocol_name):
|
def create_protocol(self, protocol_name):
|
||||||
self.protocol_name = protocol_name
|
self.protocol_name = protocol_name
|
||||||
self.steps_todo_list = []
|
self.steps_todo_list = []
|
||||||
@@ -500,7 +509,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
self.api_client.call("IAutomation", "Reset")
|
self.api_client.call("IAutomation", "Reset")
|
||||||
while not self.is_reset_ok:
|
while not self.is_reset_ok:
|
||||||
print("Waiting for PRCXI9300 to reset...")
|
print("Waiting for PRCXI9300 to reset...")
|
||||||
await asyncio.sleep(1)
|
await self._ros_node.sleep(1)
|
||||||
print("PRCXI9300 reset successfully.")
|
print("PRCXI9300 reset successfully.")
|
||||||
except ConnectionRefusedError as e:
|
except ConnectionRefusedError as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@@ -533,7 +542,9 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
tipspot_index = tipspot.parent.children.index(tipspot)
|
tipspot_index = tipspot.parent.children.index(tipspot)
|
||||||
tip_columns.append(tipspot_index // 8)
|
tip_columns.append(tipspot_index // 8)
|
||||||
if len(set(tip_columns)) != 1:
|
if len(set(tip_columns)) != 1:
|
||||||
raise ValueError("All pickups must be from the same tip column. Found different columns: " + str(tip_columns))
|
raise ValueError(
|
||||||
|
"All pickups must be from the same tip column. Found different columns: " + str(tip_columns)
|
||||||
|
)
|
||||||
PlateNo = plate_indexes[0] + 1
|
PlateNo = plate_indexes[0] + 1
|
||||||
hole_col = tip_columns[0] + 1
|
hole_col = tip_columns[0] + 1
|
||||||
hole_row = 1
|
hole_row = 1
|
||||||
@@ -1109,12 +1120,15 @@ class PRCXI9300Api:
|
|||||||
"LiquidDispensingMethod": liquid_method,
|
"LiquidDispensingMethod": liquid_method,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DefaultLayout:
|
class DefaultLayout:
|
||||||
|
|
||||||
def __init__(self, product_name: str = "PRCXI9300"):
|
def __init__(self, product_name: str = "PRCXI9300"):
|
||||||
self.labresource = {}
|
self.labresource = {}
|
||||||
if product_name not in ["PRCXI9300", "PRCXI9320"]:
|
if product_name not in ["PRCXI9300", "PRCXI9320"]:
|
||||||
raise ValueError(f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported.")
|
raise ValueError(
|
||||||
|
f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported."
|
||||||
|
)
|
||||||
|
|
||||||
if product_name == "PRCXI9300":
|
if product_name == "PRCXI9300":
|
||||||
self.rows = 2
|
self.rows = 2
|
||||||
@@ -1129,24 +1143,92 @@ class DefaultLayout:
|
|||||||
self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||||
self.trash_slot = 16
|
self.trash_slot = 16
|
||||||
self.waste_liquid_slot = 12
|
self.waste_liquid_slot = 12
|
||||||
self.default_layout = {"MatrixId":f"{time.time()}","MatrixName":f"{time.time()}","MatrixCount":16,"WorkTablets":
|
self.default_layout = {
|
||||||
[{"Number": 1, "Code": "T1", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"MatrixId": f"{time.time()}",
|
||||||
{"Number": 2, "Code": "T2", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"MatrixName": f"{time.time()}",
|
||||||
{"Number": 3, "Code": "T3", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"MatrixCount": 16,
|
||||||
{"Number": 4, "Code": "T4", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"WorkTablets": [
|
||||||
{"Number": 5, "Code": "T5", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
{
|
||||||
{"Number": 6, "Code": "T6", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"Number": 1,
|
||||||
{"Number": 7, "Code": "T7", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"Code": "T1",
|
||||||
{"Number": 8, "Code": "T8", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
{"Number": 9, "Code": "T9", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
},
|
||||||
{"Number": 10, "Code": "T10", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
{
|
||||||
{"Number": 11, "Code": "T11", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"Number": 2,
|
||||||
{"Number": 12, "Code": "T12", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}}, # 这个设置成废液槽,用储液槽表示
|
"Code": "T2",
|
||||||
{"Number": 13, "Code": "T13", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
{"Number": 14, "Code": "T14", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
},
|
||||||
{"Number": 15, "Code": "T15", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
{
|
||||||
{"Number": 16, "Code": "T16", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}} # 这个设置成垃圾桶,用储液槽表示
|
"Number": 3,
|
||||||
]
|
"Code": "T3",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 4,
|
||||||
|
"Code": "T4",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 5,
|
||||||
|
"Code": "T5",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 6,
|
||||||
|
"Code": "T6",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 7,
|
||||||
|
"Code": "T7",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 8,
|
||||||
|
"Code": "T8",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 9,
|
||||||
|
"Code": "T9",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 10,
|
||||||
|
"Code": "T10",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 11,
|
||||||
|
"Code": "T11",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 12,
|
||||||
|
"Code": "T12",
|
||||||
|
"Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0},
|
||||||
|
}, # 这个设置成废液槽,用储液槽表示
|
||||||
|
{
|
||||||
|
"Number": 13,
|
||||||
|
"Code": "T13",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 14,
|
||||||
|
"Code": "T14",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 15,
|
||||||
|
"Code": "T15",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 16,
|
||||||
|
"Code": "T16",
|
||||||
|
"Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0},
|
||||||
|
}, # 这个设置成垃圾桶,用储液槽表示
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_layout(self) -> Dict[str, Any]:
|
def get_layout(self) -> Dict[str, Any]:
|
||||||
@@ -1155,7 +1237,7 @@ class DefaultLayout:
|
|||||||
"columns": self.columns,
|
"columns": self.columns,
|
||||||
"layout": self.layout,
|
"layout": self.layout,
|
||||||
"trash_slot": self.trash_slot,
|
"trash_slot": self.trash_slot,
|
||||||
"waste_liquid_slot": self.waste_liquid_slot
|
"waste_liquid_slot": self.waste_liquid_slot,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_trash_slot(self) -> int:
|
def get_trash_slot(self) -> int:
|
||||||
@@ -1181,14 +1263,16 @@ class DefaultLayout:
|
|||||||
# 计算总需求
|
# 计算总需求
|
||||||
total_needed = sum(count for _, _, count in needs)
|
total_needed = sum(count for _, _, count in needs)
|
||||||
if total_needed > len(available_positions):
|
if total_needed > len(available_positions):
|
||||||
raise ValueError(f"需要 {total_needed} 个位置,但只有 {len(available_positions)} 个可用位置(排除位置12和16)")
|
raise ValueError(
|
||||||
|
f"需要 {total_needed} 个位置,但只有 {len(available_positions)} 个可用位置(排除位置12和16)"
|
||||||
|
)
|
||||||
|
|
||||||
# 依次分配位置
|
# 依次分配位置
|
||||||
current_pos = 0
|
current_pos = 0
|
||||||
for reagent_name, material_name, count in needs:
|
for reagent_name, material_name, count in needs:
|
||||||
|
|
||||||
material_uuid = self.labresource[material_name]['uuid']
|
material_uuid = self.labresource[material_name]["uuid"]
|
||||||
material_enum = self.labresource[material_name]['materialEnum']
|
material_enum = self.labresource[material_name]["materialEnum"]
|
||||||
|
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
if current_pos >= len(available_positions):
|
if current_pos >= len(available_positions):
|
||||||
@@ -1196,17 +1280,18 @@ class DefaultLayout:
|
|||||||
|
|
||||||
position = available_positions[current_pos]
|
position = available_positions[current_pos]
|
||||||
# 找到对应的tablet并更新
|
# 找到对应的tablet并更新
|
||||||
for tablet in self.default_layout['WorkTablets']:
|
for tablet in self.default_layout["WorkTablets"]:
|
||||||
if tablet['Number'] == position:
|
if tablet["Number"] == position:
|
||||||
tablet['Material']['uuid'] = material_uuid
|
tablet["Material"]["uuid"] = material_uuid
|
||||||
tablet['Material']['materialEnum'] = material_enum
|
tablet["Material"]["materialEnum"] = material_enum
|
||||||
layout_list.append(dict(reagent_name=reagent_name, material_name=material_name, positions=position))
|
layout_list.append(
|
||||||
|
dict(reagent_name=reagent_name, material_name=material_name, positions=position)
|
||||||
|
)
|
||||||
break
|
break
|
||||||
current_pos += 1
|
current_pos += 1
|
||||||
return self.default_layout, layout_list
|
return self.default_layout, layout_list
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Example usage
|
# Example usage
|
||||||
# 1. 用导出的json,给每个T1 T2板子设定相应的物料,如果是孔板和枪头盒,要对应区分
|
# 1. 用导出的json,给每个T1 T2板子设定相应的物料,如果是孔板和枪头盒,要对应区分
|
||||||
@@ -1302,9 +1387,6 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# # # plate2.set_well_liquids(plate_2_liquids)
|
# # # plate2.set_well_liquids(plate_2_liquids)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# handler = PRCXI9300Handler(deck=deck, host="10.181.214.132", port=9999,
|
# handler = PRCXI9300Handler(deck=deck, host="10.181.214.132", port=9999,
|
||||||
# timeout=10.0, setup=False, debug=False,
|
# timeout=10.0, setup=False, debug=False,
|
||||||
# simulator=True,
|
# simulator=True,
|
||||||
@@ -1391,11 +1473,8 @@ if __name__ == "__main__":
|
|||||||
# # input("Press Enter to continue...") # Wait for user input before proceeding
|
# # input("Press Enter to continue...") # Wait for user input before proceeding
|
||||||
# # print("PRCXI9300Handler initialized with deck and host settings.")
|
# # print("PRCXI9300Handler initialized with deck and host settings.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 9320 ###
|
### 9320 ###
|
||||||
|
|
||||||
|
|
||||||
deck = PRCXI9300Deck(name="PRCXI_Deck", size_x=100, size_y=100, size_z=100)
|
deck = PRCXI9300Deck(name="PRCXI_Deck", size_x=100, size_y=100, size_z=100)
|
||||||
|
|
||||||
from pylabrobot.resources.opentrons.tip_racks import tipone_96_tiprack_200ul, opentrons_96_tiprack_10ul
|
from pylabrobot.resources.opentrons.tip_racks import tipone_96_tiprack_200ul, opentrons_96_tiprack_10ul
|
||||||
@@ -1415,9 +1494,12 @@ if __name__ == "__main__":
|
|||||||
def get_tip_rack(name: str, child_prefix: str = "tip") -> PRCXI9300Container:
|
def get_tip_rack(name: str, child_prefix: str = "tip") -> PRCXI9300Container:
|
||||||
tip_racks = opentrons_96_tiprack_10ul(name).serialize()
|
tip_racks = opentrons_96_tiprack_10ul(name).serialize()
|
||||||
tip_rack = PRCXI9300Container(
|
tip_rack = PRCXI9300Container(
|
||||||
name=name, size_x=50, size_y=50, size_z=10, category="tip_rack", ordering=collections.OrderedDict({
|
name=name,
|
||||||
k: f"{child_prefix}_{k}" for k, v in tip_racks["ordering"].items()
|
size_x=50,
|
||||||
})
|
size_y=50,
|
||||||
|
size_z=10,
|
||||||
|
category="tip_rack",
|
||||||
|
ordering=collections.OrderedDict({k: f"{child_prefix}_{k}" for k, v in tip_racks["ordering"].items()}),
|
||||||
)
|
)
|
||||||
tip_rack_serialized = tip_rack.serialize()
|
tip_rack_serialized = tip_rack.serialize()
|
||||||
tip_rack_serialized["parent_name"] = deck.name
|
tip_rack_serialized["parent_name"] = deck.name
|
||||||
@@ -1629,6 +1711,7 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
backend: PRCXI9300Backend = handler.backend
|
backend: PRCXI9300Backend = handler.backend
|
||||||
from pylabrobot.resources import set_volume_tracking
|
from pylabrobot.resources import set_volume_tracking
|
||||||
|
|
||||||
set_volume_tracking(enabled=True)
|
set_volume_tracking(enabled=True)
|
||||||
# res = backend.api_client.get_all_materials()
|
# res = backend.api_client.get_all_materials()
|
||||||
asyncio.run(handler.setup()) # Initialize the handler and setup the connection
|
asyncio.run(handler.setup()) # Initialize the handler and setup the connection
|
||||||
@@ -1671,7 +1754,6 @@ if __name__ == "__main__":
|
|||||||
# Initialize the backend and setup the connection
|
# Initialize the backend and setup the connection
|
||||||
asyncio.run(handler.transfer_group("water", "master_mix", 10)) # Reset tip tracking
|
asyncio.run(handler.transfer_group("water", "master_mix", 10)) # Reset tip tracking
|
||||||
|
|
||||||
|
|
||||||
# asyncio.run(handler.pick_up_tips([plate8.children[8]],[0]))
|
# asyncio.run(handler.pick_up_tips([plate8.children[8]],[0]))
|
||||||
# print(plate8.children[8])
|
# print(plate8.children[8])
|
||||||
# asyncio.run(handler.run_protocol())
|
# asyncio.run(handler.run_protocol())
|
||||||
@@ -1703,9 +1785,6 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# asyncio.run(handler.run_protocol()) # Run the protocol
|
# asyncio.run(handler.run_protocol()) # Run the protocol
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# # # asyncio.run(handler.transfer_liquid(
|
# # # asyncio.run(handler.transfer_liquid(
|
||||||
# # # asp_vols=[10]*2,
|
# # # asp_vols=[10]*2,
|
||||||
# # # dis_vols=[10]*2,
|
# # # dis_vols=[10]*2,
|
||||||
@@ -1740,7 +1819,6 @@ if __name__ == "__main__":
|
|||||||
# # # ], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
|
# # # ], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
|
||||||
# # #print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
|
# # #print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
|
||||||
|
|
||||||
|
|
||||||
# # # asyncio.run(handler.remove_liquid(
|
# # # asyncio.run(handler.remove_liquid(
|
||||||
# # # vols=[100]*16,
|
# # # vols=[100]*16,
|
||||||
# # # sources=well_containers.children[-16:],
|
# # # sources=well_containers.children[-16:],
|
||||||
@@ -1775,31 +1853,32 @@ if __name__ == "__main__":
|
|||||||
# # # input("Press Enter to continue...") # Wait for user input before proceeding
|
# # # input("Press Enter to continue...") # Wait for user input before proceeding
|
||||||
# # # print("PRCXI9300Handler initialized with deck and host settings.")
|
# # # print("PRCXI9300Handler initialized with deck and host settings.")
|
||||||
|
|
||||||
|
|
||||||
# 一些推荐版位组合的测试样例:
|
# 一些推荐版位组合的测试样例:
|
||||||
|
|
||||||
# 一些推荐版位组合的测试样例:
|
# 一些推荐版位组合的测试样例:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
with open("prcxi_material.json", "r") as f:
|
with open("prcxi_material.json", "r") as f:
|
||||||
material_info = json.load(f)
|
material_info = json.load(f)
|
||||||
|
|
||||||
layout = DefaultLayout("PRCXI9320")
|
layout = DefaultLayout("PRCXI9320")
|
||||||
layout.add_lab_resource(material_info)
|
layout.add_lab_resource(material_info)
|
||||||
MatrixLayout_1, dict_1 = layout.recommend_layout([
|
MatrixLayout_1, dict_1 = layout.recommend_layout(
|
||||||
|
[
|
||||||
("reagent_1", "96 细胞培养皿", 3),
|
("reagent_1", "96 细胞培养皿", 3),
|
||||||
("reagent_2", "12道储液槽", 1),
|
("reagent_2", "12道储液槽", 1),
|
||||||
("reagent_3", "200μL Tip头", 7),
|
("reagent_3", "200μL Tip头", 7),
|
||||||
("reagent_4", "10μL加长 Tip头", 1),
|
("reagent_4", "10μL加长 Tip头", 1),
|
||||||
])
|
]
|
||||||
|
)
|
||||||
print(dict_1)
|
print(dict_1)
|
||||||
MatrixLayout_2, dict_2 = layout.recommend_layout([
|
MatrixLayout_2, dict_2 = layout.recommend_layout(
|
||||||
|
[
|
||||||
("reagent_1", "96深孔板", 4),
|
("reagent_1", "96深孔板", 4),
|
||||||
("reagent_2", "12道储液槽", 1),
|
("reagent_2", "12道储液槽", 1),
|
||||||
("reagent_3", "200μL Tip头", 1),
|
("reagent_3", "200μL Tip头", 1),
|
||||||
("reagent_4", "10μL加长 Tip头", 1),
|
("reagent_4", "10μL加长 Tip头", 1),
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# with open("prcxi_material.json", "r") as f:
|
# with open("prcxi_material.json", "r") as f:
|
||||||
# material_info = json.load(f)
|
# material_info = json.load(f)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import serial.tools.list_ports
|
|||||||
from serial import Serial
|
from serial import Serial
|
||||||
from serial.serialutil import SerialException
|
from serial.serialutil import SerialException
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class RunzeSyringePumpMode(Enum):
|
class RunzeSyringePumpMode(Enum):
|
||||||
Normal = 0
|
Normal = 0
|
||||||
@@ -77,6 +79,8 @@ class RunzeSyringePumpInfo:
|
|||||||
|
|
||||||
|
|
||||||
class RunzeSyringePumpAsync:
|
class RunzeSyringePumpAsync:
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, port: str, address: str = "1", volume: float = 25000, mode: RunzeSyringePumpMode = None):
|
def __init__(self, port: str, address: str = "1", volume: float = 25000, mode: RunzeSyringePumpMode = None):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.address = address
|
self.address = address
|
||||||
@@ -102,6 +106,9 @@ class RunzeSyringePumpAsync:
|
|||||||
self._run_future: Optional[Future[Any]] = None
|
self._run_future: Optional[Future[Any]] = None
|
||||||
self._run_lock = Lock()
|
self._run_lock = Lock()
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def _adjust_total_steps(self):
|
def _adjust_total_steps(self):
|
||||||
self.total_steps = 6000 if self.mode == RunzeSyringePumpMode.Normal else 48000
|
self.total_steps = 6000 if self.mode == RunzeSyringePumpMode.Normal else 48000
|
||||||
self.total_steps_vel = 48000 if self.mode == RunzeSyringePumpMode.AccuratePosVel else 6000
|
self.total_steps_vel = 48000 if self.mode == RunzeSyringePumpMode.AccuratePosVel else 6000
|
||||||
@@ -182,7 +189,7 @@ class RunzeSyringePumpAsync:
|
|||||||
try:
|
try:
|
||||||
await self._query(command)
|
await self._query(command)
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(0.5) # Wait for 0.5 seconds before polling again
|
await self._ros_node.sleep(0.5) # Wait for 0.5 seconds before polling again
|
||||||
|
|
||||||
status = await self.query_device_status()
|
status = await self.query_device_status()
|
||||||
if status == '`':
|
if status == '`':
|
||||||
@@ -364,7 +371,7 @@ class RunzeSyringePumpAsync:
|
|||||||
if self._read_task:
|
if self._read_task:
|
||||||
raise RunzeSyringePumpConnectionError
|
raise RunzeSyringePumpConnectionError
|
||||||
|
|
||||||
self._read_task = asyncio.create_task(self._read_loop())
|
self._read_task = self._ros_node.create_task(self._read_loop())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.query_device_status()
|
await self.query_device_status()
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import logging
|
|||||||
import time as time_module
|
import time as time_module
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class VirtualCentrifuge:
|
class VirtualCentrifuge:
|
||||||
"""Virtual centrifuge device - 简化版,只保留核心功能"""
|
"""Virtual centrifuge device - 简化版,只保留核心功能"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and "id" in kwargs:
|
if device_id is None and "id" in kwargs:
|
||||||
@@ -33,6 +37,9 @@ class VirtualCentrifuge:
|
|||||||
if key not in skip_keys and not hasattr(self, key):
|
if key not in skip_keys and not hasattr(self, key):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual centrifuge"""
|
"""Initialize virtual centrifuge"""
|
||||||
self.logger.info(f"Initializing virtual centrifuge {self.device_id}")
|
self.logger.info(f"Initializing virtual centrifuge {self.device_id}")
|
||||||
@@ -132,7 +139,7 @@ class VirtualCentrifuge:
|
|||||||
break
|
break
|
||||||
|
|
||||||
# 每秒更新一次
|
# 每秒更新一次
|
||||||
await asyncio.sleep(1.0)
|
await self._ros_node.sleep(1.0)
|
||||||
|
|
||||||
# 离心完成
|
# 离心完成
|
||||||
self.data.update({
|
self.data.update({
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
class VirtualColumn:
|
class VirtualColumn:
|
||||||
"""Virtual column device for RunColumn protocol 🏛️"""
|
"""Virtual column device for RunColumn protocol 🏛️"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and 'id' in kwargs:
|
if device_id is None and 'id' in kwargs:
|
||||||
@@ -28,6 +32,9 @@ class VirtualColumn:
|
|||||||
print(f"🏛️ === 虚拟色谱柱 {self.device_id} 已创建 === ✨")
|
print(f"🏛️ === 虚拟色谱柱 {self.device_id} 已创建 === ✨")
|
||||||
print(f"📏 柱参数: 流速={self._max_flow_rate}mL/min | 长度={self._column_length}cm | 直径={self._column_diameter}cm 🔬")
|
print(f"📏 柱参数: 流速={self._max_flow_rate}mL/min | 长度={self._column_length}cm | 直径={self._column_diameter}cm 🔬")
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual column 🚀"""
|
"""Initialize virtual column 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟色谱柱 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟色谱柱 {self.device_id} ✨")
|
||||||
@@ -101,7 +108,7 @@ class VirtualColumn:
|
|||||||
step_time = separation_time / steps
|
step_time = separation_time / steps
|
||||||
|
|
||||||
for i in range(steps):
|
for i in range(steps):
|
||||||
await asyncio.sleep(step_time)
|
await self._ros_node.sleep(step_time)
|
||||||
|
|
||||||
progress = (i + 1) / steps * 100
|
progress = (i + 1) / steps * 100
|
||||||
volume_processed = (i + 1) * 5.0 # 假设每步处理5mL
|
volume_processed = (i + 1) * 5.0 # 假设每步处理5mL
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import time as time_module
|
|||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
from unilabos.compile.utils.vessel_parser import get_vessel
|
from unilabos.compile.utils.vessel_parser import get_vessel
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class VirtualFilter:
|
class VirtualFilter:
|
||||||
"""Virtual filter device - 完全按照 Filter.action 规范 🌊"""
|
"""Virtual filter device - 完全按照 Filter.action 规范 🌊"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
if device_id is None and 'id' in kwargs:
|
if device_id is None and "id" in kwargs:
|
||||||
device_id = kwargs.pop('id')
|
device_id = kwargs.pop("id")
|
||||||
if config is None and 'config' in kwargs:
|
if config is None and "config" in kwargs:
|
||||||
config = kwargs.pop('config')
|
config = kwargs.pop("config")
|
||||||
|
|
||||||
self.device_id = device_id or "unknown_filter"
|
self.device_id = device_id or "unknown_filter"
|
||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
@@ -21,29 +24,34 @@ class VirtualFilter:
|
|||||||
self.data = {}
|
self.data = {}
|
||||||
|
|
||||||
# 从config或kwargs中获取配置参数
|
# 从config或kwargs中获取配置参数
|
||||||
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
|
self.port = self.config.get("port") or kwargs.get("port", "VIRTUAL")
|
||||||
self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 100.0)
|
self._max_temp = self.config.get("max_temp") or kwargs.get("max_temp", 100.0)
|
||||||
self._max_stir_speed = self.config.get('max_stir_speed') or kwargs.get('max_stir_speed', 1000.0)
|
self._max_stir_speed = self.config.get("max_stir_speed") or kwargs.get("max_stir_speed", 1000.0)
|
||||||
self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 500.0)
|
self._max_volume = self.config.get("max_volume") or kwargs.get("max_volume", 500.0)
|
||||||
|
|
||||||
# 处理其他kwargs参数
|
# 处理其他kwargs参数
|
||||||
skip_keys = {'port', 'max_temp', 'max_stir_speed', 'max_volume'}
|
skip_keys = {"port", "max_temp", "max_stir_speed", "max_volume"}
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
if key not in skip_keys and not hasattr(self, key):
|
if key not in skip_keys and not hasattr(self, key):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual filter 🚀"""
|
"""Initialize virtual filter 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟过滤器 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟过滤器 {self.device_id} ✨")
|
||||||
|
|
||||||
# 按照 Filter.action 的 feedback 字段初始化
|
# 按照 Filter.action 的 feedback 字段初始化
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"status": "Idle",
|
"status": "Idle",
|
||||||
"progress": 0.0, # Filter.action feedback
|
"progress": 0.0, # Filter.action feedback
|
||||||
"current_temp": 25.0, # Filter.action feedback
|
"current_temp": 25.0, # Filter.action feedback
|
||||||
"filtered_volume": 0.0, # Filter.action feedback
|
"filtered_volume": 0.0, # Filter.action feedback
|
||||||
"message": "Ready for filtration"
|
"message": "Ready for filtration",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f"✅ 过滤器 {self.device_id} 初始化完成 🌊")
|
self.logger.info(f"✅ 过滤器 {self.device_id} 初始化完成 🌊")
|
||||||
return True
|
return True
|
||||||
@@ -52,9 +60,7 @@ class VirtualFilter:
|
|||||||
"""Cleanup virtual filter 🧹"""
|
"""Cleanup virtual filter 🧹"""
|
||||||
self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚")
|
self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update({"status": "Offline"})
|
||||||
"status": "Offline"
|
|
||||||
})
|
|
||||||
|
|
||||||
self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤")
|
self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤")
|
||||||
return True
|
return True
|
||||||
@@ -67,7 +73,7 @@ class VirtualFilter:
|
|||||||
stir_speed: float = 300.0,
|
stir_speed: float = 300.0,
|
||||||
temp: float = 25.0,
|
temp: float = 25.0,
|
||||||
continue_heatchill: bool = False,
|
continue_heatchill: bool = False,
|
||||||
volume: float = 0.0
|
volume: float = 0.0,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Execute filter action - 完全按照 Filter.action 参数 🌊"""
|
"""Execute filter action - 完全按照 Filter.action 参数 🌊"""
|
||||||
vessel_id, _ = get_vessel(vessel)
|
vessel_id, _ = get_vessel(vessel)
|
||||||
@@ -92,41 +98,34 @@ class VirtualFilter:
|
|||||||
if temp > self._max_temp or temp < 4.0:
|
if temp > self._max_temp or temp < 4.0:
|
||||||
error_msg = f"🌡️ 温度 {temp}°C 超出范围 (4-{self._max_temp}°C) ⚠️"
|
error_msg = f"🌡️ 温度 {temp}°C 超出范围 (4-{self._max_temp}°C) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({"status": f"Error: 温度超出范围 ⚠️", "message": error_msg})
|
||||||
"status": f"Error: 温度超出范围 ⚠️",
|
|
||||||
"message": error_msg
|
|
||||||
})
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if stir and stir_speed > self._max_stir_speed:
|
if stir and stir_speed > self._max_stir_speed:
|
||||||
error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM) ⚠️"
|
error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({"status": f"Error: 搅拌速度超出范围 ⚠️", "message": error_msg})
|
||||||
"status": f"Error: 搅拌速度超出范围 ⚠️",
|
|
||||||
"message": error_msg
|
|
||||||
})
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if volume > self._max_volume:
|
if volume > self._max_volume:
|
||||||
error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️"
|
error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({"status": f"Error", "message": error_msg})
|
||||||
"status": f"Error",
|
|
||||||
"message": error_msg
|
|
||||||
})
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 开始过滤
|
# 开始过滤
|
||||||
filter_volume = volume if volume > 0 else 50.0
|
filter_volume = volume if volume > 0 else 50.0
|
||||||
self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧")
|
self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"status": f"Running",
|
"status": f"Running",
|
||||||
"current_temp": temp,
|
"current_temp": temp,
|
||||||
"filtered_volume": 0.0,
|
"filtered_volume": 0.0,
|
||||||
"progress": 0.0,
|
"progress": 0.0,
|
||||||
"message": f"🚀 Starting filtration: {vessel_id} → {filtrate_vessel_id}"
|
"message": f"🚀 Starting filtration: {vessel_id} → {filtrate_vessel_id}",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 过滤过程 - 实时更新进度
|
# 过滤过程 - 实时更新进度
|
||||||
@@ -157,13 +156,15 @@ class VirtualFilter:
|
|||||||
status_msg += f" | 🌪️ 搅拌: {stir_speed} RPM"
|
status_msg += f" | 🌪️ 搅拌: {stir_speed} RPM"
|
||||||
status_msg += f" | 🌡️ {temp}°C | 📊 {progress:.1f}% | 💧 已过滤: {current_filtered:.1f}mL"
|
status_msg += f" | 🌡️ {temp}°C | 📊 {progress:.1f}% | 💧 已过滤: {current_filtered:.1f}mL"
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"progress": progress, # Filter.action feedback
|
"progress": progress, # Filter.action feedback
|
||||||
"current_temp": temp, # Filter.action feedback
|
"current_temp": temp, # Filter.action feedback
|
||||||
"filtered_volume": current_filtered, # Filter.action feedback
|
"filtered_volume": current_filtered, # Filter.action feedback
|
||||||
"status": "Running",
|
"status": "Running",
|
||||||
"message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered"
|
"message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 进度日志(每25%打印一次)
|
# 进度日志(每25%打印一次)
|
||||||
if progress >= 25 and progress % 25 < 1:
|
if progress >= 25 and progress % 25 < 1:
|
||||||
@@ -172,7 +173,7 @@ class VirtualFilter:
|
|||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
await asyncio.sleep(1.0)
|
await self._ros_node.sleep(1.0)
|
||||||
|
|
||||||
# 过滤完成
|
# 过滤完成
|
||||||
final_temp = temp if continue_heatchill else 25.0
|
final_temp = temp if continue_heatchill else 25.0
|
||||||
@@ -181,13 +182,15 @@ class VirtualFilter:
|
|||||||
final_status += " | 🔥 继续加热搅拌"
|
final_status += " | 🔥 继续加热搅拌"
|
||||||
self.logger.info(f"🔥 继续保持加热搅拌状态 🌪️")
|
self.logger.info(f"🔥 继续保持加热搅拌状态 🌪️")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"status": final_status,
|
"status": final_status,
|
||||||
"progress": 100.0, # Filter.action feedback
|
"progress": 100.0, # Filter.action feedback
|
||||||
"current_temp": final_temp, # Filter.action feedback
|
"current_temp": final_temp, # Filter.action feedback
|
||||||
"filtered_volume": filter_volume, # Filter.action feedback
|
"filtered_volume": filter_volume, # Filter.action feedback
|
||||||
"message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel_id}"
|
"message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel_id}",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel_id} 过滤到 {filtrate_vessel_id} ✨")
|
self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel_id} 过滤到 {filtrate_vessel_id} ✨")
|
||||||
self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁")
|
self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁")
|
||||||
@@ -196,10 +199,7 @@ class VirtualFilter:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"过滤过程中发生错误: {str(e)} 💥"
|
error_msg = f"过滤过程中发生错误: {str(e)} 💥"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({"status": f"Error", "message": f"❌ Filtration failed: {str(e)}"})
|
||||||
"status": f"Error",
|
|
||||||
"message": f"❌ Filtration failed: {str(e)}"
|
|
||||||
})
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# === 核心状态属性 - 按照 Filter.action feedback 字段 ===
|
# === 核心状态属性 - 按照 Filter.action feedback 字段 ===
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import logging
|
|||||||
import time as time_module # 重命名time模块,避免与参数冲突
|
import time as time_module # 重命名time模块,避免与参数冲突
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
class VirtualHeatChill:
|
class VirtualHeatChill:
|
||||||
"""Virtual heat chill device for HeatChillProtocol testing 🌡️"""
|
"""Virtual heat chill device for HeatChillProtocol testing 🌡️"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and 'id' in kwargs:
|
if device_id is None and 'id' in kwargs:
|
||||||
@@ -35,6 +39,9 @@ class VirtualHeatChill:
|
|||||||
print(f"🌡️ === 虚拟温控设备 {self.device_id} 已创建 === ✨")
|
print(f"🌡️ === 虚拟温控设备 {self.device_id} 已创建 === ✨")
|
||||||
print(f"🔥 温度范围: {self._min_temp}°C ~ {self._max_temp}°C | 🌪️ 最大搅拌: {self._max_stir_speed} RPM")
|
print(f"🔥 温度范围: {self._min_temp}°C ~ {self._max_temp}°C | 🌪️ 最大搅拌: {self._max_stir_speed} RPM")
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual heat chill 🚀"""
|
"""Initialize virtual heat chill 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟温控设备 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟温控设备 {self.device_id} ✨")
|
||||||
@@ -177,7 +184,7 @@ class VirtualHeatChill:
|
|||||||
break
|
break
|
||||||
|
|
||||||
# 等待1秒后再次检查
|
# 等待1秒后再次检查
|
||||||
await asyncio.sleep(1.0)
|
await self._ros_node.sleep(1.0)
|
||||||
|
|
||||||
# 操作完成
|
# 操作完成
|
||||||
final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
|
final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
|
||||||
|
|||||||
@@ -3,13 +3,19 @@ import logging
|
|||||||
import time as time_module
|
import time as time_module
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出 🔍"""
|
"""调试输出 🔍"""
|
||||||
print(f"🌪️ [ROTAVAP] {message}", flush=True)
|
print(f"🌪️ [ROTAVAP] {message}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
class VirtualRotavap:
|
class VirtualRotavap:
|
||||||
"""Virtual rotary evaporator device - 简化版,只保留核心功能 🌪️"""
|
"""Virtual rotary evaporator device - 简化版,只保留核心功能 🌪️"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and "id" in kwargs:
|
if device_id is None and "id" in kwargs:
|
||||||
@@ -38,12 +44,16 @@ class VirtualRotavap:
|
|||||||
print(f"🌪️ === 虚拟旋转蒸发仪 {self.device_id} 已创建 === ✨")
|
print(f"🌪️ === 虚拟旋转蒸发仪 {self.device_id} 已创建 === ✨")
|
||||||
print(f"🔥 温度范围: 10°C ~ {self._max_temp}°C | 🌀 转速范围: 10 ~ {self._max_rotation_speed} RPM")
|
print(f"🔥 温度范围: 10°C ~ {self._max_temp}°C | 🌀 转速范围: 10 ~ {self._max_rotation_speed} RPM")
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual rotary evaporator 🚀"""
|
"""Initialize virtual rotary evaporator 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟旋转蒸发仪 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟旋转蒸发仪 {self.device_id} ✨")
|
||||||
|
|
||||||
# 只保留核心状态
|
# 只保留核心状态
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"status": "🏠 待机中",
|
"status": "🏠 待机中",
|
||||||
"rotavap_state": "Ready", # Ready, Evaporating, Completed, Error
|
"rotavap_state": "Ready", # Ready, Evaporating, Completed, Error
|
||||||
"current_temp": 25.0,
|
"current_temp": 25.0,
|
||||||
@@ -53,25 +63,30 @@ class VirtualRotavap:
|
|||||||
"evaporated_volume": 0.0,
|
"evaporated_volume": 0.0,
|
||||||
"progress": 0.0,
|
"progress": 0.0,
|
||||||
"remaining_time": 0.0,
|
"remaining_time": 0.0,
|
||||||
"message": "🌪️ Ready for evaporation"
|
"message": "🌪️ Ready for evaporation",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 初始化完成 🌪️")
|
self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 初始化完成 🌪️")
|
||||||
self.logger.info(f"📊 设备规格: 温度范围 10°C ~ {self._max_temp}°C | 转速范围 10 ~ {self._max_rotation_speed} RPM")
|
self.logger.info(
|
||||||
|
f"📊 设备规格: 温度范围 10°C ~ {self._max_temp}°C | 转速范围 10 ~ {self._max_rotation_speed} RPM"
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def cleanup(self) -> bool:
|
async def cleanup(self) -> bool:
|
||||||
"""Cleanup virtual rotary evaporator 🧹"""
|
"""Cleanup virtual rotary evaporator 🧹"""
|
||||||
self.logger.info(f"🧹 清理虚拟旋转蒸发仪 {self.device_id} 🔚")
|
self.logger.info(f"🧹 清理虚拟旋转蒸发仪 {self.device_id} 🔚")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"status": "💤 离线",
|
"status": "💤 离线",
|
||||||
"rotavap_state": "Offline",
|
"rotavap_state": "Offline",
|
||||||
"current_temp": 25.0,
|
"current_temp": 25.0,
|
||||||
"rotation_speed": 0.0,
|
"rotation_speed": 0.0,
|
||||||
"vacuum_pressure": 1.0,
|
"vacuum_pressure": 1.0,
|
||||||
"message": "💤 System offline"
|
"message": "💤 System offline",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 清理完成 💤")
|
self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 清理完成 💤")
|
||||||
return True
|
return True
|
||||||
@@ -84,7 +99,7 @@ class VirtualRotavap:
|
|||||||
time: float = 180.0,
|
time: float = 180.0,
|
||||||
stir_speed: float = 100.0,
|
stir_speed: float = 100.0,
|
||||||
solvent: str = "",
|
solvent: str = "",
|
||||||
**kwargs
|
**kwargs,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Execute evaporate action - 简化版 🌪️"""
|
"""Execute evaporate action - 简化版 🌪️"""
|
||||||
|
|
||||||
@@ -114,11 +129,11 @@ class VirtualRotavap:
|
|||||||
self.logger.info(f"🧪 识别到溶剂: {solvent}")
|
self.logger.info(f"🧪 识别到溶剂: {solvent}")
|
||||||
# 根据溶剂调整参数
|
# 根据溶剂调整参数
|
||||||
solvent_lower = solvent.lower()
|
solvent_lower = solvent.lower()
|
||||||
if any(s in solvent_lower for s in ['water', 'aqueous']):
|
if any(s in solvent_lower for s in ["water", "aqueous"]):
|
||||||
temp = max(temp, 80.0)
|
temp = max(temp, 80.0)
|
||||||
pressure = max(pressure, 0.2)
|
pressure = max(pressure, 0.2)
|
||||||
self.logger.info(f"💧 水系溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
|
self.logger.info(f"💧 水系溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
|
||||||
elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
|
elif any(s in solvent_lower for s in ["ethanol", "methanol", "acetone"]):
|
||||||
temp = min(temp, 50.0)
|
temp = min(temp, 50.0)
|
||||||
pressure = min(pressure, 0.05)
|
pressure = min(pressure, 0.05)
|
||||||
self.logger.info(f"⚡ 易挥发溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
|
self.logger.info(f"⚡ 易挥发溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
|
||||||
@@ -136,46 +151,53 @@ class VirtualRotavap:
|
|||||||
if temp > self._max_temp or temp < 10.0:
|
if temp > self._max_temp or temp < 10.0:
|
||||||
error_msg = f"🌡️ 温度 {temp}°C 超出范围 (10-{self._max_temp}°C) ⚠️"
|
error_msg = f"🌡️ 温度 {temp}°C 超出范围 (10-{self._max_temp}°C) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"status": f"❌ 错误: 温度超出范围",
|
"status": f"❌ 错误: 温度超出范围",
|
||||||
"rotavap_state": "Error",
|
"rotavap_state": "Error",
|
||||||
"current_temp": 25.0,
|
"current_temp": 25.0,
|
||||||
"progress": 0.0,
|
"progress": 0.0,
|
||||||
"evaporated_volume": 0.0,
|
"evaporated_volume": 0.0,
|
||||||
"message": error_msg
|
"message": error_msg,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if stir_speed > self._max_rotation_speed or stir_speed < 10.0:
|
if stir_speed > self._max_rotation_speed or stir_speed < 10.0:
|
||||||
error_msg = f"🌀 旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM) ⚠️"
|
error_msg = f"🌀 旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"status": f"❌ 错误: 转速超出范围",
|
"status": f"❌ 错误: 转速超出范围",
|
||||||
"rotavap_state": "Error",
|
"rotavap_state": "Error",
|
||||||
"current_temp": 25.0,
|
"current_temp": 25.0,
|
||||||
"progress": 0.0,
|
"progress": 0.0,
|
||||||
"evaporated_volume": 0.0,
|
"evaporated_volume": 0.0,
|
||||||
"message": error_msg
|
"message": error_msg,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if pressure < 0.01 or pressure > 1.0:
|
if pressure < 0.01 or pressure > 1.0:
|
||||||
error_msg = f"💨 真空度 {pressure} bar 超出范围 (0.01-1.0 bar) ⚠️"
|
error_msg = f"💨 真空度 {pressure} bar 超出范围 (0.01-1.0 bar) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"status": f"❌ 错误: 压力超出范围",
|
"status": f"❌ 错误: 压力超出范围",
|
||||||
"rotavap_state": "Error",
|
"rotavap_state": "Error",
|
||||||
"current_temp": 25.0,
|
"current_temp": 25.0,
|
||||||
"progress": 0.0,
|
"progress": 0.0,
|
||||||
"evaporated_volume": 0.0,
|
"evaporated_volume": 0.0,
|
||||||
"message": error_msg
|
"message": error_msg,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 开始蒸发 - 🔧 现在time已经确保是float类型
|
# 开始蒸发 - 🔧 现在time已经确保是float类型
|
||||||
self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️")
|
self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"status": f"🌪️ 蒸发中: {actual_vessel}",
|
"status": f"🌪️ 蒸发中: {actual_vessel}",
|
||||||
"rotavap_state": "Evaporating",
|
"rotavap_state": "Evaporating",
|
||||||
"current_temp": temp,
|
"current_temp": temp,
|
||||||
@@ -185,8 +207,9 @@ class VirtualRotavap:
|
|||||||
"remaining_time": time,
|
"remaining_time": time,
|
||||||
"progress": 0.0,
|
"progress": 0.0,
|
||||||
"evaporated_volume": 0.0,
|
"evaporated_volume": 0.0,
|
||||||
"message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
|
"message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 蒸发过程 - 实时更新进度
|
# 蒸发过程 - 实时更新进度
|
||||||
@@ -201,9 +224,9 @@ class VirtualRotavap:
|
|||||||
progress = min(100.0, (elapsed / total_time) * 100)
|
progress = min(100.0, (elapsed / total_time) * 100)
|
||||||
|
|
||||||
# 模拟蒸发体积 - 根据溶剂类型调整
|
# 模拟蒸发体积 - 根据溶剂类型调整
|
||||||
if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']):
|
if solvent and any(s in solvent.lower() for s in ["water", "aqueous"]):
|
||||||
evaporated_vol = progress * 0.6 # 水系溶剂蒸发慢
|
evaporated_vol = progress * 0.6 # 水系溶剂蒸发慢
|
||||||
elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']):
|
elif solvent and any(s in solvent.lower() for s in ["ethanol", "methanol", "acetone"]):
|
||||||
evaporated_vol = progress * 1.0 # 易挥发溶剂蒸发快
|
evaporated_vol = progress * 1.0 # 易挥发溶剂蒸发快
|
||||||
else:
|
else:
|
||||||
evaporated_vol = progress * 0.8 # 默认蒸发量
|
evaporated_vol = progress * 0.8 # 默认蒸发量
|
||||||
@@ -211,18 +234,22 @@ class VirtualRotavap:
|
|||||||
# 🔧 更新状态 - 确保包含所有必需字段
|
# 🔧 更新状态 - 确保包含所有必需字段
|
||||||
status_msg = f"🌪️ 蒸发中: {actual_vessel} | 🌡️ {temp}°C | 💨 {pressure} bar | 🌀 {stir_speed} RPM | 📊 {progress:.1f}% | ⏰ 剩余: {remaining:.0f}s"
|
status_msg = f"🌪️ 蒸发中: {actual_vessel} | 🌡️ {temp}°C | 💨 {pressure} bar | 🌀 {stir_speed} RPM | 📊 {progress:.1f}% | ⏰ 剩余: {remaining:.0f}s"
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"remaining_time": remaining,
|
"remaining_time": remaining,
|
||||||
"progress": progress,
|
"progress": progress,
|
||||||
"evaporated_volume": evaporated_vol,
|
"evaporated_volume": evaporated_vol,
|
||||||
"current_temp": temp,
|
"current_temp": temp,
|
||||||
"status": status_msg,
|
"status": status_msg,
|
||||||
"message": f"🌪️ Evaporating: {progress:.1f}% complete, 💧 {evaporated_vol:.1f}mL evaporated, ⏰ {remaining:.0f}s remaining"
|
"message": f"🌪️ Evaporating: {progress:.1f}% complete, 💧 {evaporated_vol:.1f}mL evaporated, ⏰ {remaining:.0f}s remaining",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 进度日志(每25%打印一次)
|
# 进度日志(每25%打印一次)
|
||||||
if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_progress:
|
if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_progress:
|
||||||
self.logger.info(f"📊 蒸发进度: {progress:.0f}% | 💧 已蒸发: {evaporated_vol:.1f}mL | ⏰ 剩余: {remaining:.0f}s ✨")
|
self.logger.info(
|
||||||
|
f"📊 蒸发进度: {progress:.0f}% | 💧 已蒸发: {evaporated_vol:.1f}mL | ⏰ 剩余: {remaining:.0f}s ✨"
|
||||||
|
)
|
||||||
last_logged_progress = int(progress)
|
last_logged_progress = int(progress)
|
||||||
|
|
||||||
# 时间到了,退出循环
|
# 时间到了,退出循环
|
||||||
@@ -230,17 +257,18 @@ class VirtualRotavap:
|
|||||||
break
|
break
|
||||||
|
|
||||||
# 每秒更新一次
|
# 每秒更新一次
|
||||||
await asyncio.sleep(1.0)
|
await self._ros_node.sleep(1.0)
|
||||||
|
|
||||||
# 蒸发完成
|
# 蒸发完成
|
||||||
if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']):
|
if solvent and any(s in solvent.lower() for s in ["water", "aqueous"]):
|
||||||
final_evaporated = 60.0 # 水系溶剂
|
final_evaporated = 60.0 # 水系溶剂
|
||||||
elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']):
|
elif solvent and any(s in solvent.lower() for s in ["ethanol", "methanol", "acetone"]):
|
||||||
final_evaporated = 100.0 # 易挥发溶剂
|
final_evaporated = 100.0 # 易挥发溶剂
|
||||||
else:
|
else:
|
||||||
final_evaporated = 80.0 # 默认
|
final_evaporated = 80.0 # 默认
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"status": f"✅ 蒸发完成: {actual_vessel} | 💧 蒸发量: {final_evaporated:.1f}mL",
|
"status": f"✅ 蒸发完成: {actual_vessel} | 💧 蒸发量: {final_evaporated:.1f}mL",
|
||||||
"rotavap_state": "Completed",
|
"rotavap_state": "Completed",
|
||||||
"evaporated_volume": final_evaporated,
|
"evaporated_volume": final_evaporated,
|
||||||
@@ -249,8 +277,9 @@ class VirtualRotavap:
|
|||||||
"remaining_time": 0.0,
|
"remaining_time": 0.0,
|
||||||
"rotation_speed": 0.0,
|
"rotation_speed": 0.0,
|
||||||
"vacuum_pressure": 1.0,
|
"vacuum_pressure": 1.0,
|
||||||
"message": f"✅ Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}"
|
"message": f"✅ Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f"🎉 蒸发操作完成! ✨")
|
self.logger.info(f"🎉 蒸发操作完成! ✨")
|
||||||
self.logger.info(f"📊 蒸发结果:")
|
self.logger.info(f"📊 蒸发结果:")
|
||||||
@@ -270,7 +299,8 @@ class VirtualRotavap:
|
|||||||
error_msg = f"蒸发过程中发生错误: {str(e)} 💥"
|
error_msg = f"蒸发过程中发生错误: {str(e)} 💥"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
|
{
|
||||||
"status": f"❌ 蒸发错误: {str(e)}",
|
"status": f"❌ 蒸发错误: {str(e)}",
|
||||||
"rotavap_state": "Error",
|
"rotavap_state": "Error",
|
||||||
"current_temp": 25.0,
|
"current_temp": 25.0,
|
||||||
@@ -278,8 +308,9 @@ class VirtualRotavap:
|
|||||||
"evaporated_volume": 0.0,
|
"evaporated_volume": 0.0,
|
||||||
"rotation_speed": 0.0,
|
"rotation_speed": 0.0,
|
||||||
"vacuum_pressure": 1.0,
|
"vacuum_pressure": 1.0,
|
||||||
"message": f"❌ Evaporation failed: {str(e)}"
|
"message": f"❌ Evaporation failed: {str(e)}",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# === 核心状态属性 ===
|
# === 核心状态属性 ===
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class VirtualSeparator:
|
class VirtualSeparator:
|
||||||
"""Virtual separator device for SeparateProtocol testing"""
|
"""Virtual separator device for SeparateProtocol testing"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and "id" in kwargs:
|
if device_id is None and "id" in kwargs:
|
||||||
@@ -36,6 +40,9 @@ class VirtualSeparator:
|
|||||||
if key not in skip_keys and not hasattr(self, key):
|
if key not in skip_keys and not hasattr(self, key):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual separator"""
|
"""Initialize virtual separator"""
|
||||||
print(f"=== VirtualSeparator {self.device_id} initialize() called! ===")
|
print(f"=== VirtualSeparator {self.device_id} initialize() called! ===")
|
||||||
@@ -119,14 +126,14 @@ class VirtualSeparator:
|
|||||||
for repeat in range(repeats):
|
for repeat in range(repeats):
|
||||||
# 搅拌阶段
|
# 搅拌阶段
|
||||||
for progress in range(0, 51, 10):
|
for progress in range(0, 51, 10):
|
||||||
await asyncio.sleep(simulation_time / (repeats * 10))
|
await self._ros_node.sleep(simulation_time / (repeats * 10))
|
||||||
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
|
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
|
||||||
self.data["progress"] = overall_progress
|
self.data["progress"] = overall_progress
|
||||||
self.data["message"] = f"第{repeat+1}次分离 - 搅拌中 ({progress}%)"
|
self.data["message"] = f"第{repeat+1}次分离 - 搅拌中 ({progress}%)"
|
||||||
|
|
||||||
# 静置分相阶段
|
# 静置分相阶段
|
||||||
for progress in range(50, 101, 10):
|
for progress in range(50, 101, 10):
|
||||||
await asyncio.sleep(simulation_time / (repeats * 10))
|
await self._ros_node.sleep(simulation_time / (repeats * 10))
|
||||||
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
|
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
|
||||||
self.data["progress"] = overall_progress
|
self.data["progress"] = overall_progress
|
||||||
self.data["message"] = f"第{repeat+1}次分离 - 静置分相中 ({progress}%)"
|
self.data["message"] = f"第{repeat+1}次分离 - 静置分相中 ({progress}%)"
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ import time
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class VirtualSolenoidValve:
|
class VirtualSolenoidValve:
|
||||||
"""
|
"""
|
||||||
虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态
|
虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
|
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
|
||||||
# 从配置中获取参数,提供默认值
|
# 从配置中获取参数,提供默认值
|
||||||
if config is None:
|
if config is None:
|
||||||
@@ -22,6 +27,9 @@ class VirtualSolenoidValve:
|
|||||||
self._valve_state = "Closed" # "Open" or "Closed"
|
self._valve_state = "Closed" # "Open" or "Closed"
|
||||||
self._is_open = False
|
self._is_open = False
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""初始化设备"""
|
"""初始化设备"""
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
@@ -63,7 +71,7 @@ class VirtualSolenoidValve:
|
|||||||
self._status = "Busy"
|
self._status = "Busy"
|
||||||
|
|
||||||
# 模拟阀门响应时间
|
# 模拟阀门响应时间
|
||||||
await asyncio.sleep(self.response_time)
|
await self._ros_node.sleep(self.response_time)
|
||||||
|
|
||||||
# 处理不同的命令格式
|
# 处理不同的命令格式
|
||||||
if isinstance(command, str):
|
if isinstance(command, str):
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
class VirtualSolidDispenser:
|
class VirtualSolidDispenser:
|
||||||
"""
|
"""
|
||||||
虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 ⚗️
|
虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 ⚗️
|
||||||
@@ -13,6 +15,8 @@ class VirtualSolidDispenser:
|
|||||||
- 简单反馈:成功/失败 + 消息 📊
|
- 简单反馈:成功/失败 + 消息 📊
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
||||||
self.device_id = device_id or "virtual_solid_dispenser"
|
self.device_id = device_id or "virtual_solid_dispenser"
|
||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
@@ -32,6 +36,9 @@ class VirtualSolidDispenser:
|
|||||||
print(f"⚗️ === 虚拟固体分配器 {self.device_id} 创建成功! === ✨")
|
print(f"⚗️ === 虚拟固体分配器 {self.device_id} 创建成功! === ✨")
|
||||||
print(f"📊 设备规格: 最大容量 {self.max_capacity}g | 精度 {self.precision}g 🎯")
|
print(f"📊 设备规格: 最大容量 {self.max_capacity}g | 精度 {self.precision}g 🎯")
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""初始化固体加样器 🚀"""
|
"""初始化固体加样器 🚀"""
|
||||||
self.logger.info(f"🔧 初始化固体分配器 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化固体分配器 {self.device_id} ✨")
|
||||||
@@ -263,7 +270,7 @@ class VirtualSolidDispenser:
|
|||||||
|
|
||||||
for i in range(steps):
|
for i in range(steps):
|
||||||
progress = (i + 1) / steps * 100
|
progress = (i + 1) / steps * 100
|
||||||
await asyncio.sleep(step_time)
|
await self._ros_node.sleep(step_time)
|
||||||
if i % 2 == 0: # 每隔一步显示进度
|
if i % 2 == 0: # 每隔一步显示进度
|
||||||
self.logger.debug(f"📊 加样进度: {progress:.0f}% | {amount_emoji} 正在分配 {reagent}...")
|
self.logger.debug(f"📊 加样进度: {progress:.0f}% | {amount_emoji} 正在分配 {reagent}...")
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import logging
|
|||||||
import time as time_module
|
import time as time_module
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
class VirtualStirrer:
|
class VirtualStirrer:
|
||||||
"""Virtual stirrer device for StirProtocol testing - 功能完整版 🌪️"""
|
"""Virtual stirrer device for StirProtocol testing - 功能完整版 🌪️"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and 'id' in kwargs:
|
if device_id is None and 'id' in kwargs:
|
||||||
@@ -34,6 +38,9 @@ class VirtualStirrer:
|
|||||||
print(f"🌪️ === 虚拟搅拌器 {self.device_id} 已创建 === ✨")
|
print(f"🌪️ === 虚拟搅拌器 {self.device_id} 已创建 === ✨")
|
||||||
print(f"🔧 速度范围: {self._min_speed} ~ {self._max_speed} RPM | 📱 端口: {self.port}")
|
print(f"🔧 速度范围: {self._min_speed} ~ {self._max_speed} RPM | 📱 端口: {self.port}")
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual stirrer 🚀"""
|
"""Initialize virtual stirrer 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟搅拌器 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟搅拌器 {self.device_id} ✨")
|
||||||
@@ -134,7 +141,7 @@ class VirtualStirrer:
|
|||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
await asyncio.sleep(1.0)
|
await self._ros_node.sleep(1.0)
|
||||||
|
|
||||||
self.logger.info(f"✅ 搅拌阶段完成! 🌪️ {stir_speed} RPM × {stir_time}s")
|
self.logger.info(f"✅ 搅拌阶段完成! 🌪️ {stir_speed} RPM × {stir_time}s")
|
||||||
|
|
||||||
@@ -176,7 +183,7 @@ class VirtualStirrer:
|
|||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
await asyncio.sleep(1.0)
|
await self._ros_node.sleep(1.0)
|
||||||
|
|
||||||
self.logger.info(f"✅ 沉降阶段完成! 🛑 静置 {settling_time}s")
|
self.logger.info(f"✅ 沉降阶段完成! 🛑 静置 {settling_time}s")
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from enum import Enum
|
|||||||
from typing import Union, Optional
|
from typing import Union, Optional
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class VirtualPumpMode(Enum):
|
class VirtualPumpMode(Enum):
|
||||||
Normal = 0
|
Normal = 0
|
||||||
@@ -14,6 +16,8 @@ class VirtualPumpMode(Enum):
|
|||||||
class VirtualTransferPump:
|
class VirtualTransferPump:
|
||||||
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰"""
|
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
|
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
初始化虚拟转移泵
|
初始化虚拟转移泵
|
||||||
@@ -53,6 +57,9 @@ class VirtualTransferPump:
|
|||||||
print(f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s")
|
print(f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s")
|
||||||
print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}")
|
print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}")
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""初始化虚拟泵 🚀"""
|
"""初始化虚拟泵 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨")
|
||||||
@@ -104,7 +111,7 @@ class VirtualTransferPump:
|
|||||||
async def _simulate_operation(self, duration: float):
|
async def _simulate_operation(self, duration: float):
|
||||||
"""模拟操作延时 ⏱️"""
|
"""模拟操作延时 ⏱️"""
|
||||||
self._status = "Busy"
|
self._status = "Busy"
|
||||||
await asyncio.sleep(duration)
|
await self._ros_node.sleep(duration)
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
|
|
||||||
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
|
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
|
||||||
@@ -223,7 +230,7 @@ class VirtualTransferPump:
|
|||||||
|
|
||||||
# 等待一小步时间
|
# 等待一小步时间
|
||||||
if i < steps and step_duration > 0:
|
if i < steps and step_duration > 0:
|
||||||
await asyncio.sleep(step_duration)
|
await self._ros_node.sleep(step_duration)
|
||||||
else:
|
else:
|
||||||
# 移动距离很小,直接完成
|
# 移动距离很小,直接完成
|
||||||
self._position = target_position
|
self._position = target_position
|
||||||
@@ -341,7 +348,7 @@ class VirtualTransferPump:
|
|||||||
|
|
||||||
# 短暂停顿
|
# 短暂停顿
|
||||||
self.logger.debug("⏸️ 短暂停顿...")
|
self.logger.debug("⏸️ 短暂停顿...")
|
||||||
await asyncio.sleep(0.1)
|
await self._ros_node.sleep(0.1)
|
||||||
|
|
||||||
# 排液
|
# 排液
|
||||||
await self.dispense(volume, dispense_velocity)
|
await self.dispense(volume, dispense_velocity)
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
||||||
"""指定库位出库物料"""
|
"""指定库位出库物料(通过库位名称)"""
|
||||||
location_id = LOCATION_MAPPING.get(location_name, location_name)
|
location_id = LOCATION_MAPPING.get(location_name, location_name)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
@@ -251,7 +251,36 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if not response or response['code'] != 1:
|
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
|
return response
|
||||||
|
|
||||||
# ==================== 工作流查询相关接口 ====================
|
# ==================== 工作流查询相关接口 ====================
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
temperature: 温度设定(°C)
|
temperature: 温度设定(°C)
|
||||||
"""
|
"""
|
||||||
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
|
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
|
||||||
if volume is None and solvents is not None:
|
if not volume and solvents is not None:
|
||||||
# 参数类型转换:如果是字符串则解析为字典
|
# 参数类型转换:如果是字符串则解析为字典
|
||||||
if isinstance(solvents, str):
|
if isinstance(solvents, str):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -85,8 +85,90 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
|
|||||||
def sync_to_external(self, resource: Any) -> bool:
|
def sync_to_external(self, resource: Any) -> bool:
|
||||||
"""将本地物料数据变更同步到Bioyond系统"""
|
"""将本地物料数据变更同步到Bioyond系统"""
|
||||||
try:
|
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
|
return False
|
||||||
|
|
||||||
bioyond_material = resource_plr_to_bioyond(
|
bioyond_material = resource_plr_to_bioyond(
|
||||||
@@ -171,11 +253,22 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
|
|
||||||
def post_init(self, ros_node: ROS2WorkstationNode):
|
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||||
self._ros_node = ros_node
|
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, **{
|
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
||||||
"resources": [self.deck]
|
"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):
|
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, **{
|
ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
|
||||||
"plr_resources": resource,
|
"plr_resources": resource,
|
||||||
"target_device_id": mount_device_id,
|
"target_device_id": mount_device_id,
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -48,3 +48,25 @@ BIOYOND_PolymerStation_Solution_Beaker:
|
|||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
version: 1.0.0
|
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,
|
barcode=barcode,
|
||||||
model="BIOYOND_PolymerStation_Reagent_Bottle",
|
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,7 +1,22 @@
|
|||||||
from os import name
|
from os import name
|
||||||
from pylabrobot.resources import Deck, Coordinate, Rotation
|
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):
|
class BIOYOND_PolymerReactionStation_Deck(Deck):
|
||||||
@@ -20,15 +35,22 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
|
|||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
# 添加仓库
|
# 添加仓库
|
||||||
|
# 说明: 堆栈1物理上分为左右两部分
|
||||||
|
# - 堆栈1左: A01~D04 (4行×4列, 位于反应站左侧)
|
||||||
|
# - 堆栈1右: A05~D08 (4行×4列, 位于反应站右侧)
|
||||||
self.warehouses = {
|
self.warehouses = {
|
||||||
"堆栈1": bioyond_warehouse_1x4x4("堆栈1"),
|
"堆栈1左": bioyond_warehouse_1x4x4("堆栈1左"), # 左侧堆栈: A01~D04
|
||||||
"堆栈2": bioyond_warehouse_1x4x4("堆栈2"),
|
"堆栈1右": bioyond_warehouse_1x4x4_right("堆栈1右"), # 右侧堆栈: A05~D08
|
||||||
"站内试剂存放堆栈": bioyond_warehouse_liquid_and_lid_handling("站内试剂存放堆栈"),
|
"站内试剂存放堆栈": 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 = {
|
self.warehouse_locations = {
|
||||||
"堆栈1": Coordinate(0.0, 430.0, 0.0),
|
"堆栈1左": Coordinate(0.0, 430.0, 0.0), # 左侧位置
|
||||||
"堆栈2": Coordinate(2550.0, 430.0, 0.0),
|
"堆栈1右": Coordinate(2500.0, 430.0, 0.0), # 右侧位置
|
||||||
"站内试剂存放堆栈": Coordinate(800.0, 475.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)
|
self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from unilabos.resources.warehouse import WareHouse, warehouse_factory
|
|||||||
|
|
||||||
|
|
||||||
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
||||||
"""创建BioYond 4x1x4仓库"""
|
"""创建BioYond 4x4x1仓库 (左侧堆栈: A01~D04)"""
|
||||||
return warehouse_factory(
|
return warehouse_factory(
|
||||||
name=name,
|
name=name,
|
||||||
num_items_x=4,
|
num_items_x=4,
|
||||||
@@ -15,6 +15,25 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
|||||||
item_dy=106.0,
|
item_dy=106.0,
|
||||||
item_dz=130.0,
|
item_dz=130.0,
|
||||||
category="warehouse",
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -159,3 +178,71 @@ def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
|
|||||||
category="warehouse",
|
category="warehouse",
|
||||||
removed_positions=None
|
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",
|
"trash": "trash",
|
||||||
"deck": "deck",
|
"deck": "deck",
|
||||||
"tip_rack": "tip_rack",
|
"tip_rack": "tip_rack",
|
||||||
|
"warehouse": "warehouse",
|
||||||
|
"container": "container",
|
||||||
}
|
}
|
||||||
if source in replace_info:
|
if source in replace_info:
|
||||||
return replace_info[source]
|
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"
|
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
|
{"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.code = material.get("code", "") and material.get("barCode", "") or ""
|
||||||
plr_material.unilabos_uuid = str(uuid.uuid4())
|
plr_material.unilabos_uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -659,6 +676,8 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
]
|
]
|
||||||
bottle.code = detail.get("code", "")
|
bottle.code = detail.get("code", "")
|
||||||
else:
|
else:
|
||||||
|
# 只对有 capacity 属性的容器(液体容器)处理液体追踪
|
||||||
|
if hasattr(plr_material, 'capacity'):
|
||||||
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
|
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
|
||||||
bottle.tracker.liquids = [
|
bottle.tracker.liquids = [
|
||||||
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
|
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
|
||||||
@@ -668,16 +687,55 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
|
|
||||||
if deck and hasattr(deck, "warehouses"):
|
if deck and hasattr(deck, "warehouses"):
|
||||||
for loc in material.get("locations", []):
|
for loc in material.get("locations", []):
|
||||||
if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses:
|
wh_name = loc.get("whName")
|
||||||
warehouse = deck.warehouses[loc["whName"]]
|
|
||||||
idx = (
|
# 特殊处理: Bioyond的"堆栈1"需要映射到"堆栈1左"或"堆栈1右"
|
||||||
(loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y
|
# 根据列号(x)判断: 1-4映射到左侧, 5-8映射到右侧
|
||||||
+ (loc.get("x", 0) - 1) * warehouse.num_items_x
|
if wh_name == "堆栈1":
|
||||||
+ (loc.get("z", 0) - 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 0 <= idx < warehouse.capacity:
|
||||||
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
|
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
|
||||||
warehouse[idx] = plr_material
|
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
|
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
|
bottle = resource[0] if resource.capacity > 0 else resource
|
||||||
material = {
|
material = {
|
||||||
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
||||||
"name": resource.get("name", ""),
|
"name": resource.name if hasattr(resource, "name") else "",
|
||||||
"unit": "",
|
"unit": "个", # 修复:Bioyond API 要求 unit 字段不能为空
|
||||||
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
|
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
|
||||||
"Parameters": "{}"
|
"Parameters": "{}"
|
||||||
}
|
}
|
||||||
@@ -759,6 +817,8 @@ def initialize_resource(resource_config: dict, resource_type: Any = None) -> Uni
|
|||||||
elif type(resource_class_config) == str:
|
elif type(resource_class_config) == str:
|
||||||
# Allow special resource class names to be used
|
# Allow special resource class names to be used
|
||||||
if resource_class_config not in lab_registry.resource_type_registry:
|
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]
|
return [resource_config]
|
||||||
# If the resource class is a string, look up the class in the
|
# If the resource class is a string, look up the class in the
|
||||||
# resource_type_registry and import it
|
# resource_type_registry and import it
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ def warehouse_factory(
|
|||||||
empty: bool = False,
|
empty: bool = False,
|
||||||
category: str = "warehouse",
|
category: str = "warehouse",
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
|
col_offset: int = 0, # 新增:列起始偏移量,用于生成A05-D08等命名
|
||||||
):
|
):
|
||||||
# 创建16个板架位 (4层 x 4位置)
|
# 创建16个板架位 (4层 x 4位置)
|
||||||
locations = []
|
locations = []
|
||||||
@@ -44,7 +45,9 @@ def warehouse_factory(
|
|||||||
name_prefix=name,
|
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)
|
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())}
|
sites = {i: site for i, site in zip(keys, _sites.values())}
|
||||||
|
|
||||||
return WareHouse(
|
return WareHouse(
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ from unilabos.ros.nodes.resource_tracker import (
|
|||||||
)
|
)
|
||||||
from unilabos.ros.x.rclpyx import get_event_loop
|
from unilabos.ros.x.rclpyx import get_event_loop
|
||||||
from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
||||||
from unilabos.utils.async_util import run_async_func
|
from rclpy.task import Task, Future
|
||||||
from unilabos.utils.import_manager import default_manager
|
from unilabos.utils.import_manager import default_manager
|
||||||
from unilabos.utils.log import info, debug, warning, error, critical, logger, trace
|
from unilabos.utils.log import info, debug, warning, error, critical, logger, trace
|
||||||
from unilabos.utils.type_check import get_type_class, TypeEncoder, get_result_info_str
|
from unilabos.utils.type_check import get_type_class, TypeEncoder, get_result_info_str
|
||||||
@@ -555,6 +555,15 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
rclpy.get_global_executor().add_node(self)
|
rclpy.get_global_executor().add_node(self)
|
||||||
self.lab_logger().debug(f"ROS节点初始化完成")
|
self.lab_logger().debug(f"ROS节点初始化完成")
|
||||||
|
|
||||||
|
async def sleep(self, rel_time: float, callback_group=None):
|
||||||
|
if callback_group is None:
|
||||||
|
callback_group = self.callback_group
|
||||||
|
await ROS2DeviceNode.async_wait_for(self, rel_time, callback_group)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create_task(cls, func, trace_error=True, **kwargs) -> Task:
|
||||||
|
return ROS2DeviceNode.run_async_func(func, trace_error, **kwargs)
|
||||||
|
|
||||||
async def update_resource(self, resources: List["ResourcePLR"]):
|
async def update_resource(self, resources: List["ResourcePLR"]):
|
||||||
r = SerialCommand.Request()
|
r = SerialCommand.Request()
|
||||||
tree_set = ResourceTreeSet.from_plr_resources(resources)
|
tree_set = ResourceTreeSet.from_plr_resources(resources)
|
||||||
@@ -647,7 +656,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
].call_async(
|
].call_async(
|
||||||
SerialCommand.Request(
|
SerialCommand.Request(
|
||||||
command=json.dumps(
|
command=json.dumps(
|
||||||
{"data": {"data": resources_uuid, "with_children": False}, "action": "get"}
|
{"data": {"data": resources_uuid, "with_children": True if action == "add" else "update"}, "action": "get"}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
@@ -1385,18 +1394,27 @@ class ROS2DeviceNode:
|
|||||||
它不继承设备类,而是通过代理模式访问设备类的属性和方法。
|
它不继承设备类,而是通过代理模式访问设备类的属性和方法。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 类变量,用于循环管理
|
@classmethod
|
||||||
_loop = None
|
def run_async_func(cls, func, trace_error=True, **kwargs) -> Task:
|
||||||
_loop_running = False
|
def _handle_future_exception(fut):
|
||||||
_loop_thread = None
|
try:
|
||||||
|
fut.result()
|
||||||
|
except Exception as e:
|
||||||
|
error(f"异步任务 {func.__name__} 报错了")
|
||||||
|
error(traceback.format_exc())
|
||||||
|
|
||||||
|
future = rclpy.get_global_executor().create_task(func(**kwargs))
|
||||||
|
if trace_error:
|
||||||
|
future.add_done_callback(_handle_future_exception)
|
||||||
|
return future
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_loop(cls):
|
async def async_wait_for(cls, node: Node, wait_time: float, callback_group=None):
|
||||||
return cls._loop
|
future = Future()
|
||||||
|
timer = node.create_timer(wait_time, lambda : future.set_result(None), callback_group=callback_group, clock=node.get_clock())
|
||||||
@classmethod
|
await future
|
||||||
def run_async_func(cls, func, trace_error=True, **kwargs):
|
timer.cancel()
|
||||||
return run_async_func(func, loop=cls._loop, trace_error=trace_error, **kwargs)
|
node.destroy_timer(timer)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def driver_instance(self):
|
def driver_instance(self):
|
||||||
@@ -1436,11 +1454,6 @@ class ROS2DeviceNode:
|
|||||||
print_publish: 是否打印发布信息
|
print_publish: 是否打印发布信息
|
||||||
driver_is_ros:
|
driver_is_ros:
|
||||||
"""
|
"""
|
||||||
# 在初始化时检查循环状态
|
|
||||||
if ROS2DeviceNode._loop_running and ROS2DeviceNode._loop_thread is not None:
|
|
||||||
pass
|
|
||||||
elif ROS2DeviceNode._loop_thread is None:
|
|
||||||
self._start_loop()
|
|
||||||
|
|
||||||
# 保存设备类是否支持异步上下文
|
# 保存设备类是否支持异步上下文
|
||||||
self._has_async_context = hasattr(driver_class, "__aenter__") and hasattr(driver_class, "__aexit__")
|
self._has_async_context = hasattr(driver_class, "__aenter__") and hasattr(driver_class, "__aexit__")
|
||||||
@@ -1529,17 +1542,6 @@ class ROS2DeviceNode:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._ros_node.lab_logger().error(f"设备后初始化失败: {e}")
|
self._ros_node.lab_logger().error(f"设备后初始化失败: {e}")
|
||||||
|
|
||||||
def _start_loop(self):
|
|
||||||
def run_event_loop():
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
ROS2DeviceNode._loop = loop
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
loop.run_forever()
|
|
||||||
|
|
||||||
ROS2DeviceNode._loop_thread = threading.Thread(target=run_event_loop, daemon=True, name="ROS2DeviceNodeLoop")
|
|
||||||
ROS2DeviceNode._loop_thread.start()
|
|
||||||
logger.info(f"循环线程已启动")
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceInfoType(TypedDict):
|
class DeviceInfoType(TypedDict):
|
||||||
id: str
|
id: str
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ from unilabos_msgs.srv import (
|
|||||||
ResourceDelete,
|
ResourceDelete,
|
||||||
ResourceUpdate,
|
ResourceUpdate,
|
||||||
ResourceList,
|
ResourceList,
|
||||||
SerialCommand, ResourceGet,
|
SerialCommand,
|
||||||
|
ResourceGet,
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||||
from unique_identifier_msgs.msg import UUID
|
from unique_identifier_msgs.msg import UUID
|
||||||
|
|||||||
@@ -848,9 +848,15 @@ class DeviceNodeResourceTracker(object):
|
|||||||
extra: extra字典值
|
extra: extra字典值
|
||||||
"""
|
"""
|
||||||
if isinstance(resource, dict):
|
if isinstance(resource, dict):
|
||||||
resource["extra"] = extra
|
# ⭐ 修复:合并extra而不是覆盖
|
||||||
|
current_extra = resource.get("extra", {})
|
||||||
|
current_extra.update(extra)
|
||||||
|
resource["extra"] = current_extra
|
||||||
else:
|
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:
|
def _traverse_and_process(self, resource, process_func) -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
from asyncio import get_event_loop
|
|
||||||
|
|
||||||
from unilabos.utils.log import error
|
|
||||||
|
|
||||||
|
|
||||||
def run_async_func(func, *, loop=None, trace_error=True, **kwargs):
|
|
||||||
if loop is None:
|
|
||||||
loop = get_event_loop()
|
|
||||||
|
|
||||||
def _handle_future_exception(fut):
|
|
||||||
try:
|
|
||||||
fut.result()
|
|
||||||
except Exception as e:
|
|
||||||
error(f"异步任务 {func.__name__} 报错了")
|
|
||||||
error(traceback.format_exc())
|
|
||||||
|
|
||||||
future = asyncio.run_coroutine_threadsafe(func(**kwargs), loop)
|
|
||||||
if trace_error:
|
|
||||||
future.add_done_callback(_handle_future_exception)
|
|
||||||
return future
|
|
||||||
Reference in New Issue
Block a user