mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-26 23:13:13 +00:00
Compare commits
81 Commits
fix/workst
...
f7db8d17c5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7db8d17c5 | ||
|
|
a354965f8e | ||
|
|
934276d2f7 | ||
|
|
803809480b | ||
|
|
5478ba3237 | ||
|
|
49f1aa9c28 | ||
|
|
d5d516f0ef | ||
|
|
4471fed4b8 | ||
|
|
30d143e1a5 | ||
|
|
75ea45f21e | ||
|
|
66af337d6c | ||
|
|
ae3c65c1d3 | ||
|
|
11e4f053f1 | ||
|
|
96f37b3b0d | ||
|
|
d7d0a27976 | ||
|
|
34151f5cb2 | ||
|
|
369a21b904 | ||
|
|
90169981c1 | ||
|
|
d297abfd19 | ||
|
|
9c515a252a | ||
|
|
ea5e7a5ce2 | ||
|
|
2e9a0a4677 | ||
|
|
4c7aa8a89a | ||
|
|
d8a0c5e715 | ||
|
|
133ffaac17 | ||
|
|
729a0fcf0c | ||
|
|
6ae77e0408 | ||
|
|
bab4b1d67a | ||
|
|
12c17ec26e | ||
|
|
6577fe12eb | ||
|
|
f1fee5fad9 | ||
|
|
9b3377aedb | ||
|
|
526327727d | ||
|
|
aaa86314e3 | ||
|
|
6a14104e6b | ||
|
|
ab0c4b708b | ||
|
|
c0b7f2decd | ||
|
|
b6c9530c61 | ||
|
|
8698821c52 | ||
|
|
3f53f88390 | ||
|
|
e840516ba4 | ||
|
|
146d8c5296 | ||
|
|
6573c9e02e | ||
|
|
c7b9c6a825 | ||
|
|
48c43d3303 | ||
|
|
55be5e8188 | ||
|
|
1b9f3c666d | ||
|
|
097114d38c | ||
|
|
5bec899479 | ||
|
|
5e86112ebf | ||
|
|
24ecb13b79 | ||
|
|
2573d34713 | ||
|
|
106d71e1db | ||
|
|
3c2a4a64ac | ||
|
|
1e00a66a65 | ||
|
|
46da42deef | ||
|
|
101c1bc3cc | ||
|
|
a62112ae26 | ||
|
|
dd5a7cab75 | ||
|
|
39de3ac58e | ||
|
|
b99969278c | ||
|
|
b957ad2f71 | ||
|
|
e1a7c3a103 | ||
|
|
e63c15997c | ||
|
|
c5a495f409 | ||
|
|
5b240cb0ea | ||
|
|
147b8f47c0 | ||
|
|
6d2489af5f | ||
|
|
807dcdd226 | ||
|
|
8a29bc5597 | ||
|
|
6f6c70ee57 | ||
|
|
478a85951c | ||
|
|
0f2555c90c | ||
|
|
d2dda6ee03 | ||
|
|
208540b307 | ||
|
|
cb7c56a1d9 | ||
|
|
ea2e9c3e3a | ||
|
|
0452a68180 | ||
|
|
90a0f3db9b | ||
|
|
055d120ba8 | ||
|
|
a948f09f60 |
132
.github/workflows/multi-platform-build.yml
vendored
Normal file
132
.github/workflows/multi-platform-build.yml
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
name: Multi-Platform Conda Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, dev ]
|
||||
tags: [ 'v*' ]
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platforms:
|
||||
description: '选择构建平台 (逗号分隔): linux-64, osx-64, osx-arm64, win-64'
|
||||
required: false
|
||||
default: 'osx-arm64'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux-64
|
||||
env_file: unilabos-linux-64.yaml
|
||||
- os: macos-13 # Intel
|
||||
platform: osx-64
|
||||
env_file: unilabos-osx-64.yaml
|
||||
- os: macos-latest # ARM64
|
||||
platform: osx-arm64
|
||||
env_file: unilabos-osx-arm64.yaml
|
||||
- os: windows-latest
|
||||
platform: win-64
|
||||
env_file: unilabos-win64.yaml
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -l {0}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if platform should be built
|
||||
id: should_build
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
elif [[ -z "${{ github.event.inputs.platforms }}" ]]; then
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.event.inputs.platforms }}" == *"${{ matrix.platform }}"* ]]; then
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Miniconda
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
miniconda-version: "latest"
|
||||
channels: conda-forge,robostack-staging,defaults
|
||||
channel-priority: strict
|
||||
activate-environment: build-env
|
||||
auto-activate-base: false
|
||||
auto-update-conda: false
|
||||
show-channel-urls: true
|
||||
|
||||
- name: Install boa and build tools
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda install -c conda-forge boa conda-build
|
||||
|
||||
- name: Show environment info
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda info
|
||||
conda list | grep -E "(boa|conda-build)"
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "OS: ${{ matrix.os }}"
|
||||
|
||||
- name: Build conda package
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
if [[ "${{ matrix.platform }}" == "osx-arm64" ]]; then
|
||||
boa build -m ./recipes/conda_build_config.yaml -m ./recipes/macos_sdk_config.yaml ./recipes/ros-humble-unilabos-msgs
|
||||
else
|
||||
boa build -m ./recipes/conda_build_config.yaml ./recipes/ros-humble-unilabos-msgs
|
||||
fi
|
||||
|
||||
- name: List built packages
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
echo "Built packages in conda-bld:"
|
||||
find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" | head -10
|
||||
ls -la $CONDA_PREFIX/conda-bld/${{ matrix.platform }}/ || echo "${{ matrix.platform }} directory not found"
|
||||
ls -la $CONDA_PREFIX/conda-bld/noarch/ || echo "noarch directory not found"
|
||||
echo "CONDA_PREFIX: $CONDA_PREFIX"
|
||||
echo "Full path would be: $CONDA_PREFIX/conda-bld/**/*.tar.bz2"
|
||||
|
||||
- name: Prepare artifacts for upload
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/conda-packages
|
||||
find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" -exec cp {} ${{ runner.temp }}/conda-packages/ \;
|
||||
echo "Copied files to temp directory:"
|
||||
ls -la ${{ runner.temp }}/conda-packages/
|
||||
|
||||
- name: Upload conda package artifacts
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: conda-package-${{ matrix.platform }}
|
||||
path: ${{ runner.temp }}/conda-packages
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
- name: Create release assets (on tags)
|
||||
if: steps.should_build.outputs.should_build == 'true' && startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
mkdir -p release-assets
|
||||
find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" -exec cp {} release-assets/ \;
|
||||
|
||||
- name: Upload to release
|
||||
if: steps.should_build.outputs.should_build == 'true' && startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: release-assets/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -234,3 +234,7 @@ CATKIN_IGNORE
|
||||
|
||||
*.graphml
|
||||
unilabos/device_mesh/view_robot.rviz
|
||||
|
||||
|
||||
# Certs
|
||||
**/.certs
|
||||
@@ -1,3 +1,5 @@
|
||||
recursive-include unilabos/registry *.yaml
|
||||
recursive-include unilabos/app/web *.html
|
||||
recursive-include unilabos/app/web *.css
|
||||
recursive-include unilabos/device_mesh/devices *
|
||||
recursive-include unilabos/device_mesh/resources *
|
||||
|
||||
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name
|
||||
|
||||
# Currently, you need to install the `unilabos_msgs` package
|
||||
# You can download the system-specific package from the Release page
|
||||
conda install ros-humble-unilabos-msgs-0.9.4-xxxxx.tar.bz2
|
||||
conda install ros-humble-unilabos-msgs-0.9.5-xxxxx.tar.bz2
|
||||
|
||||
# Install PyLabRobot and other prerequisites
|
||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||
|
||||
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
|
||||
|
||||
# 现阶段,需要安装 `unilabos_msgs` 包
|
||||
# 可以前往 Release 页面下载系统对应的包进行安装
|
||||
conda install ros-humble-unilabos-msgs-0.9.4-xxxxx.tar.bz2
|
||||
conda install ros-humble-unilabos-msgs-0.9.5-xxxxx.tar.bz2
|
||||
|
||||
# 安装PyLabRobot等前置
|
||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||
|
||||
7
recipes/macos_sdk_config.yaml
Normal file
7
recipes/macos_sdk_config.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
CONDA_BUILD_SYSROOT:
|
||||
- /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
|
||||
MACOSX_DEPLOYMENT_TARGET:
|
||||
- "11.0"
|
||||
CONDA_SUBDIR:
|
||||
- osx-arm64
|
||||
# boa build -m ./recipes/conda_build_config.yaml -m ./recipes/macos_sdk_config.yaml ./recipes/ros-humble-unilabos-msgs
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.9.4
|
||||
version: 0.9.5
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
folder: ros-humble-unilabos-msgs/src/work
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.9.4"
|
||||
version: "0.9.5"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.9.4',
|
||||
version='0.9.5',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
|
||||
@@ -2,4 +2,10 @@
|
||||
|
||||
```bash
|
||||
ros2 action send_goal /devices/host_node/create_resource_detailed unilabos_msgs/action/_resource_create_from_outer/ResourceCreateFromOuter "{ resources: [ { 'category': '', 'children': [], 'config': { 'type': 'Well', 'size_x': 6.86, 'size_y': 6.86, 'size_z': 10.67, 'rotation': { 'x': 0, 'y': 0, 'z': 0, 'type': 'Rotation' }, 'category': 'well', 'model': null, 'max_volume': 360, 'material_z_thickness': 0.5, 'compute_volume_from_height': null, 'compute_height_from_volume': null, 'bottom_type': 'flat', 'cross_section_type': 'circle' }, 'data': { 'liquids': [], 'pending_liquids': [], 'liquid_history': [] }, 'id': 'plate_well_11_7', 'name': 'plate_well_11_7', 'pose': { 'orientation': { 'w': 1.0, 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'position': { 'x': 0.0, 'y': 0.0, 'z': 0.0 } }, 'sample_id': '', 'parent': 'plate', 'type': 'device' } ], device_ids: [ 'PLR_STATION' ], bind_parent_ids: [ 'plate' ], bind_locations: [ { 'x': 0.0, 'y': 0.0, 'z': 0.0 } ], other_calling_params: [ '{}' ] }"
|
||||
```
|
||||
|
||||
使用mock_all.json启动,重新捕获MockContainerForChiller1
|
||||
|
||||
```bash
|
||||
ros2 action send_goal /devices/host_node/create_resource unilabos_msgs/action/_resource_create_from_outer_easy/ResourceCreateFromOuterEasy "{ 'res_id': 'MockContainerForChiller1', 'device_id': 'MockChiller1', 'class_name': 'container', 'parent': 'MockChiller1', 'bind_locations': { 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'liquid_input_slot': [ -1 ], 'liquid_type': [ 'CuCl2' ], 'liquid_volume': [ 100.0 ], 'slot_on_deck': '' }"
|
||||
```
|
||||
@@ -3,7 +3,9 @@
|
||||
{
|
||||
"id": "MockChiller1",
|
||||
"name": "模拟冷却器",
|
||||
"children": [],
|
||||
"children": [
|
||||
"MockContainerForChiller1"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_chiller",
|
||||
@@ -25,6 +27,22 @@
|
||||
"purpose": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MockContainerForChiller1",
|
||||
"name": "模拟容器",
|
||||
"type": "container",
|
||||
"parent": "MockChiller1",
|
||||
"position": {
|
||||
"x": 5,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquid_type": "CuCl2",
|
||||
"liquid_volume": "100"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "MockFilter1",
|
||||
"name": "模拟过滤器",
|
||||
|
||||
@@ -2,17 +2,42 @@ import numpy as np
|
||||
import networkx as nx
|
||||
|
||||
|
||||
def is_integrated_pump(node_name):
|
||||
return "pump" in node_name and "valve" in node_name
|
||||
|
||||
|
||||
def find_connected_pump(G, valve_node):
|
||||
for neighbor in G.neighbors(valve_node):
|
||||
if "pump" in G.nodes[neighbor]["class"]:
|
||||
return neighbor
|
||||
raise ValueError(f"未找到与阀 {valve_node} 唯一相连的泵节点")
|
||||
|
||||
|
||||
def build_pump_valve_maps(G, pump_backbone):
|
||||
pumps_from_node = {}
|
||||
valve_from_node = {}
|
||||
for node in pump_backbone:
|
||||
if is_integrated_pump(node):
|
||||
pumps_from_node[node] = node
|
||||
valve_from_node[node] = node
|
||||
else:
|
||||
pump_node = find_connected_pump(G, node)
|
||||
pumps_from_node[node] = pump_node
|
||||
valve_from_node[node] = node
|
||||
return pumps_from_node, valve_from_node
|
||||
|
||||
|
||||
def generate_pump_protocol(
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
volume: float,
|
||||
flowrate: float = 0.5,
|
||||
transfer_flowrate: float = 0,
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
volume: float,
|
||||
flowrate: float = 0.5,
|
||||
transfer_flowrate: float = 0,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
生成泵操作的动作序列。
|
||||
|
||||
|
||||
:param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
|
||||
:param from_vessel: 容器A
|
||||
:param to_vessel: 容器B
|
||||
@@ -21,194 +46,137 @@ def generate_pump_protocol(
|
||||
:param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同)
|
||||
:return: 泵操作的动作序列
|
||||
"""
|
||||
|
||||
|
||||
# 生成泵操作的动作序列
|
||||
pump_action_sequence = []
|
||||
|
||||
# 检查节点是否存在
|
||||
if from_vessel not in G.nodes:
|
||||
print(f"Warning: Source vessel '{from_vessel}' not found in graph. Skipping.")
|
||||
return []
|
||||
|
||||
if to_vessel not in G.nodes:
|
||||
print(f"Warning: Target vessel '{to_vessel}' not found in graph. Skipping.")
|
||||
return []
|
||||
|
||||
# 检查是否存在路径
|
||||
try:
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
|
||||
except nx.NetworkXNoPath:
|
||||
print(f"Warning: No path from '{from_vessel}' to '{to_vessel}'. Skipping.")
|
||||
return []
|
||||
except nx.NodeNotFound as e:
|
||||
print(f"Warning: Node not found: {e}. Skipping.")
|
||||
return []
|
||||
|
||||
print(f"Shortest path: {shortest_path}")
|
||||
nodes = G.nodes(data=True)
|
||||
# 从from_vessel到to_vessel的最短路径
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
|
||||
print(shortest_path)
|
||||
|
||||
pump_backbone = shortest_path
|
||||
if not from_vessel.startswith("pump"):
|
||||
pump_backbone = pump_backbone[1:]
|
||||
if not to_vessel.startswith("pump"):
|
||||
pump_backbone = pump_backbone[:-1]
|
||||
|
||||
print(f"Pump backbone: {pump_backbone}")
|
||||
|
||||
# 修复:检查pump_backbone是否为空
|
||||
if not pump_backbone:
|
||||
print(f"Warning: No pumps found in path from '{from_vessel}' to '{to_vessel}'. Skipping.")
|
||||
return []
|
||||
|
||||
|
||||
if transfer_flowrate == 0:
|
||||
transfer_flowrate = flowrate
|
||||
|
||||
# 修复:正确访问节点数据
|
||||
pump_max_volumes = []
|
||||
for pump in pump_backbone:
|
||||
# 直接使用 G.nodes[pump] 来访问节点数据
|
||||
pump_data = G.nodes[pump] if pump in G.nodes else {}
|
||||
# 尝试多种可能的键名,并提供默认值
|
||||
max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume')
|
||||
if max_vol is None:
|
||||
# 如果是设备节点,尝试从config中获取
|
||||
config = pump_data.get('config', {})
|
||||
max_vol = config.get('max_volume', 25.0)
|
||||
pump_max_volumes.append(float(max_vol))
|
||||
|
||||
if pump_max_volumes:
|
||||
min_transfer_volume = min(pump_max_volumes)
|
||||
else:
|
||||
min_transfer_volume = 25.0 # 默认值
|
||||
|
||||
|
||||
pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
|
||||
|
||||
min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
|
||||
repeats = int(np.ceil(volume / min_transfer_volume))
|
||||
if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")):
|
||||
raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.")
|
||||
|
||||
|
||||
volume_left = volume
|
||||
|
||||
|
||||
# 生成泵操作的动作序列
|
||||
for i in range(repeats):
|
||||
# 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
|
||||
if not from_vessel.startswith("pump") and pump_backbone:
|
||||
# 修复:添加边缘数据检查
|
||||
edge_data = G.get_edge_data(pump_backbone[0], from_vessel)
|
||||
if edge_data and "port" in edge_data:
|
||||
pump_action_sequence.extend([
|
||||
{
|
||||
"device_id": pump_backbone[0],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": edge_data["port"][pump_backbone[0]]
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pump_backbone[0],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": float(min(volume_left, min_transfer_volume)),
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
if not from_vessel.startswith("pump"):
|
||||
pump_action_sequence.extend([
|
||||
{
|
||||
"device_id": valve_from_node[pump_backbone[0]],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]]
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
else:
|
||||
print(f"Warning: No edge data found between {pump_backbone[0]} and {from_vessel}")
|
||||
|
||||
# 修复:检查pump_backbone长度,避免多泵操作时出错
|
||||
if len(pump_backbone) > 1:
|
||||
for pumpA, pumpB in zip(pump_backbone[:-1], pump_backbone[1:]):
|
||||
# 相邻两泵同时切换阀门至连通位置
|
||||
edge_AB = G.get_edge_data(pumpA, pumpB)
|
||||
edge_BA = G.get_edge_data(pumpB, pumpA)
|
||||
|
||||
if edge_AB and "port" in edge_AB and edge_BA and "port" in edge_BA:
|
||||
pump_action_sequence.append([
|
||||
{
|
||||
"device_id": pumpA,
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": edge_AB["port"][pumpA]
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pumpB,
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": edge_BA["port"][pumpB],
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pumps_from_node[pump_backbone[0]],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": float(min(volume_left, min_transfer_volume)),
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
])
|
||||
# 相邻两泵液体转移:泵A排出液体,泵B吸入液体
|
||||
pump_action_sequence.append([
|
||||
{
|
||||
"device_id": pumpA,
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 0.0,
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pumpB,
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": float(min(volume_left, min_transfer_volume)),
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]):
|
||||
# 相邻两泵同时切换阀门至连通位置
|
||||
pump_action_sequence.append([
|
||||
{
|
||||
"device_id": valve_from_node[nodeA],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": G.get_edge_data(nodeA, nodeB)["port"][nodeA]
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
else:
|
||||
print(f"Warning: No edge data found between {pumpA} and {pumpB}")
|
||||
|
||||
if not to_vessel.startswith("pump") and pump_backbone:
|
||||
},
|
||||
{
|
||||
"device_id": valve_from_node[nodeB],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": G.get_edge_data(nodeB, nodeA)["port"][nodeB],
|
||||
}
|
||||
}
|
||||
])
|
||||
# 相邻两泵液体转移:泵A排出液体,泵B吸入液体
|
||||
pump_action_sequence.append([
|
||||
{
|
||||
"device_id": pumps_from_node[nodeA],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 0.0,
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pumps_from_node[nodeB],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": float(min(volume_left, min_transfer_volume)),
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
|
||||
if not to_vessel.startswith("pump"):
|
||||
# 单泵依次执行阀指令、活塞指令,将最后一台泵液体缓慢加入容器B
|
||||
edge_data = G.get_edge_data(pump_backbone[-1], to_vessel)
|
||||
if edge_data and "port" in edge_data:
|
||||
pump_action_sequence.extend([
|
||||
{
|
||||
"device_id": pump_backbone[-1],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": edge_data["port"][pump_backbone[-1]]
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pump_backbone[-1],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 0.0,
|
||||
"max_velocity": flowrate
|
||||
}
|
||||
pump_action_sequence.extend([
|
||||
{
|
||||
"device_id": valve_from_node[pump_backbone[-1]],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]]
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
else:
|
||||
print(f"Warning: No edge data found between {pump_backbone[-1]} and {to_vessel}")
|
||||
|
||||
},
|
||||
{
|
||||
"device_id": pumps_from_node[pump_backbone[-1]],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 0.0,
|
||||
"max_velocity": flowrate
|
||||
}
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
|
||||
volume_left -= min_transfer_volume
|
||||
return pump_action_sequence
|
||||
|
||||
|
||||
# Pump protocol compilation
|
||||
def generate_pump_protocol_with_rinsing(
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
volume: float,
|
||||
amount: str = "",
|
||||
time: float = 0,
|
||||
viscous: bool = False,
|
||||
rinsing_solvent: str = "air",
|
||||
rinsing_volume: float = 5.0,
|
||||
rinsing_repeats: int = 2,
|
||||
solid: bool = False,
|
||||
flowrate: float = 2.5,
|
||||
transfer_flowrate: float = 0.5,
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
volume: float,
|
||||
amount: str = "",
|
||||
time: float = 0,
|
||||
viscous: bool = False,
|
||||
rinsing_solvent: str = "air",
|
||||
rinsing_volume: float = 5.0,
|
||||
rinsing_repeats: int = 2,
|
||||
solid: bool = False,
|
||||
flowrate: float = 2.5,
|
||||
transfer_flowrate: float = 0.5,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Generates a pump protocol for transferring a specified volume between vessels, including rinsing steps with a chosen solvent. This function constructs a sequence of pump actions based on the provided parameters and the shortest path in a directed graph.
|
||||
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): The directed graph representing the vessels and connections. 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
|
||||
from_vessel (str): The name of the vessel to transfer from.
|
||||
@@ -223,96 +191,64 @@ def generate_pump_protocol_with_rinsing(
|
||||
solid (bool, optional): Indicates if the transfer involves a solid (default is False).
|
||||
flowrate (float, optional): The flow rate for the transfer (default is 2.5). 最终注入容器B时的流速
|
||||
transfer_flowrate (float, optional): The flow rate for the transfer action (default is 0.5). 泵骨架中转移流速(若不指定,默认与注入流速相同)
|
||||
|
||||
|
||||
Returns:
|
||||
list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列.
|
||||
|
||||
|
||||
Raises:
|
||||
AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats.
|
||||
|
||||
|
||||
Examples:
|
||||
pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water")
|
||||
"""
|
||||
# 修复:使用实际存在的节点名称
|
||||
air_vessel = "flask_air" # 这个在你的配置中存在
|
||||
|
||||
# 寻找合适的废料容器,如果没有找到则使用空的容器作为替代
|
||||
waste_vessel = None
|
||||
available_vessels = [node for node in G.nodes if node.startswith("flask_") and node != air_vessel]
|
||||
if available_vessels:
|
||||
# 使用第一个可用的容器作为废料容器
|
||||
waste_vessel = available_vessels[0]
|
||||
print(f"Using {waste_vessel} as waste vessel")
|
||||
else:
|
||||
waste_vessel = "flask_1" # 备用选择
|
||||
|
||||
# 修复:添加路径检查
|
||||
try:
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
|
||||
pump_backbone = shortest_path[1: -1]
|
||||
except (nx.NetworkXNoPath, nx.NodeNotFound) as e:
|
||||
print(f"Warning: Cannot find path from {from_vessel} to {to_vessel}: {e}")
|
||||
return []
|
||||
|
||||
# 修复:正确访问节点数据
|
||||
pump_max_volumes = []
|
||||
for pump in pump_backbone:
|
||||
# 直接使用 G.nodes[pump] 来访问节点数据
|
||||
pump_data = G.nodes[pump] if pump in G.nodes else {}
|
||||
# 尝试多种可能的键名,并提供默认值
|
||||
max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume')
|
||||
if max_vol is None:
|
||||
# 如果是设备节点,尝试从config中获取
|
||||
config = pump_data.get('config', {})
|
||||
max_vol = config.get('max_volume', 25.0)
|
||||
pump_max_volumes.append(float(max_vol))
|
||||
|
||||
if pump_max_volumes:
|
||||
min_transfer_volume = float(min(pump_max_volumes))
|
||||
else:
|
||||
min_transfer_volume = 25.0 # 默认值
|
||||
|
||||
air_vessel = "flask_air"
|
||||
waste_vessel = f"waste_workup"
|
||||
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
|
||||
pump_backbone = shortest_path[1: -1]
|
||||
nodes = G.nodes(data=True)
|
||||
|
||||
pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
|
||||
|
||||
min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
|
||||
if time != 0:
|
||||
flowrate = transfer_flowrate = volume / time
|
||||
|
||||
|
||||
pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate)
|
||||
|
||||
# 修复:只在需要清洗且相关节点存在时才执行清洗步骤
|
||||
if rinsing_solvent != "air" and pump_backbone:
|
||||
if rinsing_solvent != "air" and rinsing_solvent != "":
|
||||
if "," in rinsing_solvent:
|
||||
rinsing_solvents = rinsing_solvent.split(",")
|
||||
assert len(rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats."
|
||||
assert len(
|
||||
rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats."
|
||||
else:
|
||||
rinsing_solvents = [rinsing_solvent] * rinsing_repeats
|
||||
|
||||
|
||||
for rinsing_solvent in rinsing_solvents:
|
||||
solvent_vessel = f"flask_{rinsing_solvent}"
|
||||
|
||||
# 检查溶剂容器是否存在
|
||||
if solvent_vessel not in G.nodes:
|
||||
print(f"Warning: Solvent vessel '{solvent_vessel}' not found in graph. Skipping rinsing step.")
|
||||
continue
|
||||
|
||||
# 清洗泵 - 只有当所有必需的节点都存在且pump_backbone不为空时才执行
|
||||
if pump_backbone and len(pump_backbone) > 0 and waste_vessel in G.nodes:
|
||||
# 清洗泵
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate,
|
||||
transfer_flowrate) +
|
||||
generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate,
|
||||
transfer_flowrate) +
|
||||
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate,
|
||||
transfer_flowrate)
|
||||
)
|
||||
# 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。
|
||||
if rinsing_solvent == rinsing_solvents[0]:
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) +
|
||||
generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) +
|
||||
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate)
|
||||
)
|
||||
|
||||
# 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。
|
||||
if rinsing_solvent == rinsing_solvents[0]:
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
|
||||
# 最后的空气清洗 - 只有当节点存在时才执行
|
||||
if air_vessel in G.nodes:
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
|
||||
|
||||
generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
if rinsing_solvent != "":
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
|
||||
|
||||
return pump_action_sequence
|
||||
# End Protocols
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
io_snrd:
|
||||
description: IO Board with 16 IOs
|
||||
class:
|
||||
module: unilabos.device_comms.SRND_16_IO:SRND_16_IO
|
||||
module: ilabos.device_comms.SRND_16_IO:SRND_16_IO
|
||||
type: python
|
||||
hardware_interface:
|
||||
name: modbus_client
|
||||
|
||||
@@ -71,3 +71,13 @@ solenoid_valve:
|
||||
class:
|
||||
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
valve_position: String
|
||||
action_value_mappings:
|
||||
set_valve_position:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: position
|
||||
feedback: {}
|
||||
result: {}
|
||||
5
unilabos/registry/resources/opentrons/container.yaml
Normal file
5
unilabos/registry/resources/opentrons/container.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
container:
|
||||
description: regular organic container
|
||||
class:
|
||||
module: unilabos.resources.container:RegularContainer
|
||||
type: unilabos
|
||||
67
unilabos/resources/container.py
Normal file
67
unilabos/resources/container.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import json
|
||||
|
||||
from unilabos_msgs.msg import Resource
|
||||
|
||||
from unilabos.ros.msgs.message_converter import convert_from_ros_msg
|
||||
|
||||
|
||||
class RegularContainer(object):
|
||||
# 第一个参数必须是id传入
|
||||
# noinspection PyShadowingBuiltins
|
||||
def __init__(self, id: str):
|
||||
self.id = id
|
||||
self.ulr_resource = Resource()
|
||||
self._data = None
|
||||
|
||||
@property
|
||||
def ulr_resource_data(self):
|
||||
if self._data is None:
|
||||
self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {}
|
||||
return self._data
|
||||
|
||||
@ulr_resource_data.setter
|
||||
def ulr_resource_data(self, value: dict):
|
||||
self._data = value
|
||||
self.ulr_resource.data = json.dumps(self._data)
|
||||
|
||||
@property
|
||||
def liquid_type(self):
|
||||
return self.ulr_resource_data.get("liquid_type", None)
|
||||
|
||||
@liquid_type.setter
|
||||
def liquid_type(self, value: str):
|
||||
if value is not None:
|
||||
self.ulr_resource_data["liquid_type"] = value
|
||||
else:
|
||||
self.ulr_resource_data.pop("liquid_type", None)
|
||||
|
||||
@property
|
||||
def liquid_volume(self):
|
||||
return self.ulr_resource_data.get("liquid_volume", None)
|
||||
|
||||
@liquid_volume.setter
|
||||
def liquid_volume(self, value: float):
|
||||
if value is not None:
|
||||
self.ulr_resource_data["liquid_volume"] = value
|
||||
else:
|
||||
self.ulr_resource_data.pop("liquid_volume", None)
|
||||
|
||||
def get_ulr_resource(self) -> Resource:
|
||||
"""
|
||||
获取UlrResource对象
|
||||
:return: UlrResource对象
|
||||
"""
|
||||
self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新
|
||||
return self.ulr_resource
|
||||
|
||||
def get_ulr_resource_as_dict(self) -> Resource:
|
||||
"""
|
||||
获取UlrResource对象
|
||||
:return: UlrResource对象
|
||||
"""
|
||||
to_dict = convert_from_ros_msg(self.get_ulr_resource())
|
||||
to_dict["type"] = "container"
|
||||
return to_dict
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id}"
|
||||
@@ -4,6 +4,10 @@ import json
|
||||
from typing import Union
|
||||
import numpy as np
|
||||
import networkx as nx
|
||||
from unilabos_msgs.msg import Resource
|
||||
|
||||
from unilabos.resources.container import RegularContainer
|
||||
from unilabos.ros.msgs.message_converter import convert_from_ros_msg_with_mapping, convert_to_ros_msg
|
||||
|
||||
try:
|
||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||
@@ -466,6 +470,10 @@ def initialize_resource(resource_config: dict) -> list[dict]:
|
||||
if resource_config.get("position") is not None:
|
||||
r["position"] = resource_config["position"]
|
||||
r = tree_to_list([r])
|
||||
elif resource_class_config["type"] == "unilabos":
|
||||
res_instance: RegularContainer = RESOURCE(id=resource_config["name"])
|
||||
res_instance.ulr_resource = convert_to_ros_msg(Resource, {k:v for k,v in resource_config.items() if k != "class"})
|
||||
r = [res_instance.get_ulr_resource_as_dict()]
|
||||
elif isinstance(RESOURCE, dict):
|
||||
r = [RESOURCE.copy()]
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from rclpy.service import Service
|
||||
from unilabos_msgs.action import SendCmd
|
||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||
|
||||
from unilabos.resources.container import RegularContainer
|
||||
from unilabos.resources.graphio import (
|
||||
convert_resources_to_type,
|
||||
convert_resources_from_type,
|
||||
@@ -344,6 +345,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
LIQUID_VOLUME = other_calling_param.pop("LIQUID_VOLUME", [])
|
||||
LIQUID_INPUT_SLOT = other_calling_param.pop("LIQUID_INPUT_SLOT", [])
|
||||
slot = other_calling_param.pop("slot", "-1")
|
||||
resource = None
|
||||
if slot != "-1": # slot为负数的时候采用assign方法
|
||||
other_calling_param["slot"] = slot
|
||||
# 本地拿到这个物料,可能需要先做初始化?
|
||||
@@ -362,6 +364,28 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
if initialize_full:
|
||||
resources = initialize_resources([resources])
|
||||
request.resources = [convert_to_ros_msg(Resource, resources)]
|
||||
if len(LIQUID_INPUT_SLOT) and LIQUID_INPUT_SLOT[0] == -1:
|
||||
container_instance = request.resources[0]
|
||||
container_query_dict: dict = resources
|
||||
found_resources = self.resource_tracker.figure_resource({"id": container_query_dict["name"]}, try_mode=True)
|
||||
if not len(found_resources):
|
||||
self.resource_tracker.add_resource(container_instance)
|
||||
logger.info(f"添加物料{container_query_dict['name']}到资源跟踪器")
|
||||
else:
|
||||
assert len(found_resources) == 1, f"找到多个同名物料: {container_query_dict['name']}, 请检查物料系统"
|
||||
resource = found_resources[0]
|
||||
if isinstance(resource, Resource):
|
||||
regular_container = RegularContainer(resource.id)
|
||||
regular_container.ulr_resource = resource
|
||||
regular_container.ulr_resource_data.update(json.loads(container_instance.data))
|
||||
logger.info(f"更新物料{container_query_dict['name']}的数据{resource.data} ULR")
|
||||
elif isinstance(resource, dict):
|
||||
if "data" not in resource:
|
||||
resource["data"] = {}
|
||||
resource["data"].update(json.loads(container_instance.data))
|
||||
logger.info(f"更新物料{container_query_dict['name']}的数据{resource['data']} dict")
|
||||
else:
|
||||
logger.info(f"更新物料{container_query_dict['name']}出现不支持的数据类型{type(resource)} {resource}")
|
||||
response = rclient.call(request)
|
||||
# 应该先add_resource了
|
||||
res.response = "OK"
|
||||
@@ -385,7 +409,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
res.response = serialize_result_info(traceback.format_exc(), False, {})
|
||||
return res
|
||||
# 接下来该根据bind_parent_id进行assign了,目前只有plr可以进行assign,不然没有办法输入到物料系统中
|
||||
resource = self.resource_tracker.figure_resource({"name": bind_parent_id})
|
||||
if bind_parent_id != self.node_name:
|
||||
resource = self.resource_tracker.figure_resource({"name": bind_parent_id}) # 拿到父节点,进行具体assign等操作
|
||||
# request.resources = [convert_to_ros_msg(Resource, resources)]
|
||||
|
||||
try:
|
||||
@@ -435,7 +460,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
"bind_parent_id": bind_parent_id,
|
||||
}
|
||||
)
|
||||
future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4())
|
||||
future = action_client.send_goal_async(goal)
|
||||
|
||||
def done_cb(*args):
|
||||
self.lab_logger().info(f"向meshmanager发送新增resource完成")
|
||||
@@ -601,10 +626,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
goal = goal_handle.request
|
||||
|
||||
# 从目标消息中提取参数, 并调用对应的方法
|
||||
if "sequence" in self._action_value_mappings:
|
||||
if "sequence" in action_value_mapping:
|
||||
# 如果一个指令对应函数的连续调用,如启动和等待结果,默认参数应该属于第一个函数调用
|
||||
def ACTION(**kwargs):
|
||||
for i, action in enumerate(self._action_value_mappings["sequence"]):
|
||||
for i, action in enumerate(action_value_mapping["sequence"]):
|
||||
if i == 0:
|
||||
self.lab_logger().info(f"执行序列动作第一步: {action}")
|
||||
self.get_real_function(self.driver_instance, action)[0](**kwargs)
|
||||
@@ -612,9 +637,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
self.lab_logger().info(f"执行序列动作后续步骤: {action}")
|
||||
self.get_real_function(self.driver_instance, action)[0]()
|
||||
|
||||
action_paramtypes = get_type_hints(
|
||||
self.get_real_function(self.driver_instance, self._action_value_mappings["sequence"][0])
|
||||
)[1]
|
||||
action_paramtypes = self.get_real_function(self.driver_instance, action_value_mapping["sequence"][0])[1]
|
||||
else:
|
||||
ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name)
|
||||
|
||||
@@ -903,9 +926,9 @@ class ROS2DeviceNode:
|
||||
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
|
||||
|
||||
if self._driver_class is ROS2ProtocolNode:
|
||||
self._driver_creator = ProtocolNodeCreator(driver_class, children=children)
|
||||
self._driver_creator = ProtocolNodeCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||
else:
|
||||
self._driver_creator = DeviceClassCreator(driver_class)
|
||||
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||
|
||||
if driver_is_ros:
|
||||
driver_params["device_id"] = device_id
|
||||
|
||||
@@ -383,18 +383,24 @@ class HostNode(BaseROS2DeviceNode):
|
||||
liquid_volume: list[int],
|
||||
slot_on_deck: str,
|
||||
):
|
||||
init_new_res = initialize_resource(
|
||||
{
|
||||
"name": res_id,
|
||||
"class": class_name,
|
||||
"parent": parent,
|
||||
"position": {
|
||||
"x": bind_locations.x,
|
||||
"y": bind_locations.y,
|
||||
"z": bind_locations.z,
|
||||
},
|
||||
}
|
||||
) # flatten的格式
|
||||
res_creation_input = {
|
||||
"name": res_id,
|
||||
"class": class_name,
|
||||
"parent": parent,
|
||||
"position": {
|
||||
"x": bind_locations.x,
|
||||
"y": bind_locations.y,
|
||||
"z": bind_locations.z,
|
||||
},
|
||||
}
|
||||
if len(liquid_input_slot) and liquid_input_slot[0] == -1: # 目前container只逐个创建
|
||||
res_creation_input.update({
|
||||
"data": {
|
||||
"liquid_type": liquid_type[0] if liquid_type else None,
|
||||
"liquid_volume": liquid_volume[0] if liquid_volume else None,
|
||||
}
|
||||
})
|
||||
init_new_res = initialize_resource(res_creation_input) # flatten的格式
|
||||
resources = init_new_res # initialize_resource已经返回list[dict]
|
||||
device_ids = [device_id]
|
||||
bind_parent_id = [parent]
|
||||
|
||||
@@ -256,12 +256,12 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
return write_func(*args, **kwargs)
|
||||
|
||||
if read_method:
|
||||
bound_read = MethodType(_read, device.driver_instance)
|
||||
setattr(device.driver_instance, read_method, bound_read)
|
||||
# bound_read = MethodType(_read, device.driver_instance)
|
||||
setattr(device.driver_instance, read_method, _read)
|
||||
|
||||
if write_method:
|
||||
bound_write = MethodType(_write, device.driver_instance)
|
||||
setattr(device.driver_instance, write_method, bound_write)
|
||||
# bound_write = MethodType(_write, device.driver_instance)
|
||||
setattr(device.driver_instance, write_method, _write)
|
||||
|
||||
|
||||
async def _update_resources(self, goal, protocol_kwargs):
|
||||
|
||||
@@ -25,7 +25,7 @@ class DeviceNodeResourceTracker(object):
|
||||
def clear_resource(self):
|
||||
self.resources = []
|
||||
|
||||
def figure_resource(self, query_resource):
|
||||
def figure_resource(self, query_resource, try_mode=False):
|
||||
if isinstance(query_resource, list):
|
||||
return [self.figure_resource(r) for r in query_resource]
|
||||
res_id = query_resource.id if hasattr(query_resource, "id") else (query_resource.get("id") if isinstance(query_resource, dict) else None)
|
||||
@@ -45,10 +45,14 @@ class DeviceNodeResourceTracker(object):
|
||||
res_list.extend(
|
||||
self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(query_resource, identifier_key))
|
||||
)
|
||||
assert len(res_list) == 1, f"{query_resource} 找到多个资源,请检查资源是否唯一: {res_list}"
|
||||
if not try_mode:
|
||||
assert len(res_list) > 0, f"没有找到资源 {query_resource},请检查资源是否存在"
|
||||
assert len(res_list) == 1, f"{query_resource} 找到多个资源,请检查资源是否唯一: {res_list}"
|
||||
else:
|
||||
return [i[1] for i in res_list]
|
||||
# 后续加入其他对比方式
|
||||
self.resource2parent_resource[id(query_resource)] = res_list[0][0]
|
||||
self.resource2parent_resource[id(res_list[0][1])] = res_list[0][0]
|
||||
# 后续加入其他对比方式
|
||||
return res_list[0][1]
|
||||
|
||||
def loop_find_resource(self, resource, target_resource_cls_type, identifier_key, compare_value, parent_res=None) -> List[Tuple[Any, Any]]:
|
||||
@@ -57,8 +61,12 @@ class DeviceNodeResourceTracker(object):
|
||||
children = getattr(resource, "children", [])
|
||||
for child in children:
|
||||
res_list.extend(self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource))
|
||||
if target_resource_cls_type == type(resource) or target_resource_cls_type == dict:
|
||||
if hasattr(resource, identifier_key):
|
||||
if target_resource_cls_type == type(resource):
|
||||
if target_resource_cls_type == dict:
|
||||
if identifier_key in resource:
|
||||
if resource[identifier_key] == compare_value:
|
||||
res_list.append((parent_res, resource))
|
||||
elif hasattr(resource, identifier_key):
|
||||
if getattr(resource, identifier_key) == compare_value:
|
||||
res_list.append((parent_res, resource))
|
||||
return res_list
|
||||
|
||||
@@ -33,7 +33,7 @@ class DeviceClassCreator(Generic[T]):
|
||||
这个类提供了从任意类创建实例的通用方法。
|
||||
"""
|
||||
|
||||
def __init__(self, cls: Type[T]):
|
||||
def __init__(self, cls: Type[T], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker):
|
||||
"""
|
||||
初始化设备类创建器
|
||||
|
||||
@@ -42,6 +42,18 @@ class DeviceClassCreator(Generic[T]):
|
||||
"""
|
||||
self.device_cls = cls
|
||||
self.device_instance: Optional[T] = None
|
||||
self.children = children
|
||||
self.resource_tracker = resource_tracker
|
||||
|
||||
def attach_resource(self):
|
||||
"""
|
||||
附加资源到设备类实例
|
||||
"""
|
||||
if self.device_instance is not None:
|
||||
for c in self.children.values():
|
||||
if c["type"] == "container":
|
||||
self.resource_tracker.add_resource(c)
|
||||
|
||||
|
||||
def create_instance(self, data: Dict[str, Any]) -> T:
|
||||
"""
|
||||
@@ -60,6 +72,7 @@ class DeviceClassCreator(Generic[T]):
|
||||
}
|
||||
)
|
||||
self.post_create()
|
||||
self.attach_resource()
|
||||
return self.device_instance
|
||||
|
||||
def get_instance(self) -> Optional[T]:
|
||||
@@ -90,14 +103,15 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
|
||||
cls: PyLabRobot设备类
|
||||
children: 子资源字典,用于资源替换
|
||||
"""
|
||||
super().__init__(cls)
|
||||
self.children = children
|
||||
self.resource_tracker = resource_tracker
|
||||
super().__init__(cls, children, resource_tracker)
|
||||
# 检查类是否具有deserialize方法
|
||||
self.has_deserialize = hasattr(cls, "deserialize") and callable(getattr(cls, "deserialize"))
|
||||
if not self.has_deserialize:
|
||||
logger.warning(f"类 {cls.__name__} 没有deserialize方法,将使用标准构造函数")
|
||||
|
||||
def attach_resource(self):
|
||||
pass # 只能增加实例化物料,原来默认物料仅为字典查询
|
||||
|
||||
def _process_resource_mapping(self, resource, source_type):
|
||||
if source_type == dict:
|
||||
from pylabrobot.resources.resource import Resource
|
||||
@@ -260,7 +274,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
|
||||
这个类提供了针对ProtocolNode设备类的实例创建方法,处理children参数。
|
||||
"""
|
||||
|
||||
def __init__(self, cls: Type[T], children: Dict[str, Any]):
|
||||
def __init__(self, cls: Type[T], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker):
|
||||
"""
|
||||
初始化ProtocolNode设备类创建器
|
||||
|
||||
@@ -268,8 +282,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
|
||||
cls: ProtocolNode设备类
|
||||
children: 子资源字典,用于资源替换
|
||||
"""
|
||||
super().__init__(cls)
|
||||
self.children = children
|
||||
super().__init__(cls, children, resource_tracker)
|
||||
|
||||
def create_instance(self, data: Dict[str, Any]) -> T:
|
||||
"""
|
||||
@@ -282,8 +295,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
|
||||
ProtocolNode设备类实例
|
||||
"""
|
||||
try:
|
||||
|
||||
# 创建实例
|
||||
# 创建实例,额外补充一个给protocol node的字段,后面考虑取消
|
||||
data["children"] = self.children
|
||||
self.device_instance = super(ProtocolNodeCreator, self).create_instance(data)
|
||||
self.post_create()
|
||||
|
||||
Reference in New Issue
Block a user