mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-27 20:23:11 +00:00
Compare commits
81 Commits
v0.10.3
...
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
|
*.graphml
|
||||||
unilabos/device_mesh/view_robot.rviz
|
unilabos/device_mesh/view_robot.rviz
|
||||||
|
|
||||||
|
|
||||||
|
# Certs
|
||||||
|
**/.certs
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
recursive-include unilabos/registry *.yaml
|
recursive-include unilabos/registry *.yaml
|
||||||
recursive-include unilabos/app/web *.html
|
recursive-include unilabos/app/web *.html
|
||||||
recursive-include unilabos/app/web *.css
|
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
|
# Currently, you need to install the `unilabos_msgs` package
|
||||||
# You can download the system-specific package from the Release page
|
# 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
|
# Install PyLabRobot and other prerequisites
|
||||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
|
|||||||
|
|
||||||
# 现阶段,需要安装 `unilabos_msgs` 包
|
# 现阶段,需要安装 `unilabos_msgs` 包
|
||||||
# 可以前往 Release 页面下载系统对应的包进行安装
|
# 可以前往 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等前置
|
# 安装PyLabRobot等前置
|
||||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
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:
|
package:
|
||||||
name: ros-humble-unilabos-msgs
|
name: ros-humble-unilabos-msgs
|
||||||
version: 0.9.4
|
version: 0.9.5
|
||||||
source:
|
source:
|
||||||
path: ../../unilabos_msgs
|
path: ../../unilabos_msgs
|
||||||
folder: ros-humble-unilabos-msgs/src/work
|
folder: ros-humble-unilabos-msgs/src/work
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: unilabos
|
name: unilabos
|
||||||
version: "0.9.4"
|
version: "0.9.5"
|
||||||
|
|
||||||
source:
|
source:
|
||||||
path: ../..
|
path: ../..
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name=package_name,
|
name=package_name,
|
||||||
version='0.9.4',
|
version='0.9.5',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=['setuptools'],
|
install_requires=['setuptools'],
|
||||||
|
|||||||
@@ -3,3 +3,9 @@
|
|||||||
```bash
|
```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: [ '{}' ] }"
|
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",
|
"id": "MockChiller1",
|
||||||
"name": "模拟冷却器",
|
"name": "模拟冷却器",
|
||||||
"children": [],
|
"children": [
|
||||||
|
"MockContainerForChiller1"
|
||||||
|
],
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "mock_chiller",
|
"class": "mock_chiller",
|
||||||
@@ -25,6 +27,22 @@
|
|||||||
"purpose": ""
|
"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",
|
"id": "MockFilter1",
|
||||||
"name": "模拟过滤器",
|
"name": "模拟过滤器",
|
||||||
|
|||||||
@@ -2,6 +2,31 @@ import numpy as np
|
|||||||
import networkx as nx
|
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(
|
def generate_pump_protocol(
|
||||||
G: nx.DiGraph,
|
G: nx.DiGraph,
|
||||||
from_vessel: str,
|
from_vessel: str,
|
||||||
@@ -24,27 +49,10 @@ def generate_pump_protocol(
|
|||||||
|
|
||||||
# 生成泵操作的动作序列
|
# 生成泵操作的动作序列
|
||||||
pump_action_sequence = []
|
pump_action_sequence = []
|
||||||
|
nodes = G.nodes(data=True)
|
||||||
# 检查节点是否存在
|
# 从from_vessel到to_vessel的最短路径
|
||||||
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)
|
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
|
||||||
except nx.NetworkXNoPath:
|
print(shortest_path)
|
||||||
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}")
|
|
||||||
|
|
||||||
pump_backbone = shortest_path
|
pump_backbone = shortest_path
|
||||||
if not from_vessel.startswith("pump"):
|
if not from_vessel.startswith("pump"):
|
||||||
@@ -52,34 +60,12 @@ def generate_pump_protocol(
|
|||||||
if not to_vessel.startswith("pump"):
|
if not to_vessel.startswith("pump"):
|
||||||
pump_backbone = pump_backbone[:-1]
|
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:
|
if transfer_flowrate == 0:
|
||||||
transfer_flowrate = flowrate
|
transfer_flowrate = flowrate
|
||||||
|
|
||||||
# 修复:正确访问节点数据
|
pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
|
||||||
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 # 默认值
|
|
||||||
|
|
||||||
|
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))
|
repeats = int(np.ceil(volume / min_transfer_volume))
|
||||||
if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")):
|
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.")
|
raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.")
|
||||||
@@ -89,20 +75,17 @@ def generate_pump_protocol(
|
|||||||
# 生成泵操作的动作序列
|
# 生成泵操作的动作序列
|
||||||
for i in range(repeats):
|
for i in range(repeats):
|
||||||
# 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
|
# 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
|
||||||
if not from_vessel.startswith("pump") and pump_backbone:
|
if not from_vessel.startswith("pump"):
|
||||||
# 修复:添加边缘数据检查
|
|
||||||
edge_data = G.get_edge_data(pump_backbone[0], from_vessel)
|
|
||||||
if edge_data and "port" in edge_data:
|
|
||||||
pump_action_sequence.extend([
|
pump_action_sequence.extend([
|
||||||
{
|
{
|
||||||
"device_id": pump_backbone[0],
|
"device_id": valve_from_node[pump_backbone[0]],
|
||||||
"action_name": "set_valve_position",
|
"action_name": "set_valve_position",
|
||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"command": edge_data["port"][pump_backbone[0]]
|
"command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"device_id": pump_backbone[0],
|
"device_id": pumps_from_node[pump_backbone[0]],
|
||||||
"action_name": "set_position",
|
"action_name": "set_position",
|
||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"position": float(min(volume_left, min_transfer_volume)),
|
"position": float(min(volume_left, min_transfer_volume)),
|
||||||
@@ -111,37 +94,28 @@ def generate_pump_protocol(
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||||
else:
|
for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]):
|
||||||
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([
|
pump_action_sequence.append([
|
||||||
{
|
{
|
||||||
"device_id": pumpA,
|
"device_id": valve_from_node[nodeA],
|
||||||
"action_name": "set_valve_position",
|
"action_name": "set_valve_position",
|
||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"command": edge_AB["port"][pumpA]
|
"command": G.get_edge_data(nodeA, nodeB)["port"][nodeA]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"device_id": pumpB,
|
"device_id": valve_from_node[nodeB],
|
||||||
"action_name": "set_valve_position",
|
"action_name": "set_valve_position",
|
||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"command": edge_BA["port"][pumpB],
|
"command": G.get_edge_data(nodeB, nodeA)["port"][nodeB],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
# 相邻两泵液体转移:泵A排出液体,泵B吸入液体
|
# 相邻两泵液体转移:泵A排出液体,泵B吸入液体
|
||||||
pump_action_sequence.append([
|
pump_action_sequence.append([
|
||||||
{
|
{
|
||||||
"device_id": pumpA,
|
"device_id": pumps_from_node[nodeA],
|
||||||
"action_name": "set_position",
|
"action_name": "set_position",
|
||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"position": 0.0,
|
"position": 0.0,
|
||||||
@@ -149,7 +123,7 @@ def generate_pump_protocol(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"device_id": pumpB,
|
"device_id": pumps_from_node[nodeB],
|
||||||
"action_name": "set_position",
|
"action_name": "set_position",
|
||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"position": float(min(volume_left, min_transfer_volume)),
|
"position": float(min(volume_left, min_transfer_volume)),
|
||||||
@@ -158,23 +132,19 @@ def generate_pump_protocol(
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
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:
|
if not to_vessel.startswith("pump"):
|
||||||
# 单泵依次执行阀指令、活塞指令,将最后一台泵液体缓慢加入容器B
|
# 单泵依次执行阀指令、活塞指令,将最后一台泵液体缓慢加入容器B
|
||||||
edge_data = G.get_edge_data(pump_backbone[-1], to_vessel)
|
|
||||||
if edge_data and "port" in edge_data:
|
|
||||||
pump_action_sequence.extend([
|
pump_action_sequence.extend([
|
||||||
{
|
{
|
||||||
"device_id": pump_backbone[-1],
|
"device_id": valve_from_node[pump_backbone[-1]],
|
||||||
"action_name": "set_valve_position",
|
"action_name": "set_valve_position",
|
||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"command": edge_data["port"][pump_backbone[-1]]
|
"command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"device_id": pump_backbone[-1],
|
"device_id": pumps_from_node[pump_backbone[-1]],
|
||||||
"action_name": "set_position",
|
"action_name": "set_position",
|
||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"position": 0.0,
|
"position": 0.0,
|
||||||
@@ -183,8 +153,6 @@ def generate_pump_protocol(
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
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}")
|
|
||||||
|
|
||||||
volume_left -= min_transfer_volume
|
volume_left -= min_transfer_volume
|
||||||
return pump_action_sequence
|
return pump_action_sequence
|
||||||
@@ -233,86 +201,54 @@ def generate_pump_protocol_with_rinsing(
|
|||||||
Examples:
|
Examples:
|
||||||
pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water")
|
pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water")
|
||||||
"""
|
"""
|
||||||
# 修复:使用实际存在的节点名称
|
air_vessel = "flask_air"
|
||||||
air_vessel = "flask_air" # 这个在你的配置中存在
|
waste_vessel = f"waste_workup"
|
||||||
|
|
||||||
# 寻找合适的废料容器,如果没有找到则使用空的容器作为替代
|
|
||||||
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)
|
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
|
||||||
pump_backbone = shortest_path[1: -1]
|
pump_backbone = shortest_path[1: -1]
|
||||||
except (nx.NetworkXNoPath, nx.NodeNotFound) as e:
|
nodes = G.nodes(data=True)
|
||||||
print(f"Warning: Cannot find path from {from_vessel} to {to_vessel}: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 修复:正确访问节点数据
|
pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
|
||||||
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 # 默认值
|
|
||||||
|
|
||||||
|
min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
|
||||||
if time != 0:
|
if time != 0:
|
||||||
flowrate = transfer_flowrate = volume / time
|
flowrate = transfer_flowrate = volume / time
|
||||||
|
|
||||||
pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate)
|
pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate)
|
||||||
|
if rinsing_solvent != "air" and rinsing_solvent != "":
|
||||||
# 修复:只在需要清洗且相关节点存在时才执行清洗步骤
|
|
||||||
if rinsing_solvent != "air" and pump_backbone:
|
|
||||||
if "," in rinsing_solvent:
|
if "," in rinsing_solvent:
|
||||||
rinsing_solvents = rinsing_solvent.split(",")
|
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:
|
else:
|
||||||
rinsing_solvents = [rinsing_solvent] * rinsing_repeats
|
rinsing_solvents = [rinsing_solvent] * rinsing_repeats
|
||||||
|
|
||||||
for rinsing_solvent in rinsing_solvents:
|
for rinsing_solvent in rinsing_solvents:
|
||||||
solvent_vessel = f"flask_{rinsing_solvent}"
|
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(
|
pump_action_sequence.extend(
|
||||||
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) +
|
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate,
|
||||||
generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) +
|
transfer_flowrate) +
|
||||||
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, 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]:
|
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(
|
||||||
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||||
|
pump_action_sequence.extend(
|
||||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_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(
|
||||||
if air_vessel in G.nodes:
|
generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
|
if rinsing_solvent != "":
|
||||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
|
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
|
return pump_action_sequence
|
||||||
# End Protocols
|
# End Protocols
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
io_snrd:
|
io_snrd:
|
||||||
description: IO Board with 16 IOs
|
description: IO Board with 16 IOs
|
||||||
class:
|
class:
|
||||||
module: unilabos.device_comms.SRND_16_IO:SRND_16_IO
|
module: ilabos.device_comms.SRND_16_IO:SRND_16_IO
|
||||||
type: python
|
type: python
|
||||||
hardware_interface:
|
hardware_interface:
|
||||||
name: modbus_client
|
name: modbus_client
|
||||||
|
|||||||
@@ -71,3 +71,13 @@ solenoid_valve:
|
|||||||
class:
|
class:
|
||||||
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
|
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
|
||||||
type: python
|
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
|
from typing import Union
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import networkx as nx
|
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:
|
try:
|
||||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
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:
|
if resource_config.get("position") is not None:
|
||||||
r["position"] = resource_config["position"]
|
r["position"] = resource_config["position"]
|
||||||
r = tree_to_list([r])
|
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):
|
elif isinstance(RESOURCE, dict):
|
||||||
r = [RESOURCE.copy()]
|
r = [RESOURCE.copy()]
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from rclpy.service import Service
|
|||||||
from unilabos_msgs.action import SendCmd
|
from unilabos_msgs.action import SendCmd
|
||||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||||
|
|
||||||
|
from unilabos.resources.container import RegularContainer
|
||||||
from unilabos.resources.graphio import (
|
from unilabos.resources.graphio import (
|
||||||
convert_resources_to_type,
|
convert_resources_to_type,
|
||||||
convert_resources_from_type,
|
convert_resources_from_type,
|
||||||
@@ -344,6 +345,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
LIQUID_VOLUME = other_calling_param.pop("LIQUID_VOLUME", [])
|
LIQUID_VOLUME = other_calling_param.pop("LIQUID_VOLUME", [])
|
||||||
LIQUID_INPUT_SLOT = other_calling_param.pop("LIQUID_INPUT_SLOT", [])
|
LIQUID_INPUT_SLOT = other_calling_param.pop("LIQUID_INPUT_SLOT", [])
|
||||||
slot = other_calling_param.pop("slot", "-1")
|
slot = other_calling_param.pop("slot", "-1")
|
||||||
|
resource = None
|
||||||
if slot != "-1": # slot为负数的时候采用assign方法
|
if slot != "-1": # slot为负数的时候采用assign方法
|
||||||
other_calling_param["slot"] = slot
|
other_calling_param["slot"] = slot
|
||||||
# 本地拿到这个物料,可能需要先做初始化?
|
# 本地拿到这个物料,可能需要先做初始化?
|
||||||
@@ -362,6 +364,28 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
if initialize_full:
|
if initialize_full:
|
||||||
resources = initialize_resources([resources])
|
resources = initialize_resources([resources])
|
||||||
request.resources = [convert_to_ros_msg(Resource, 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)
|
response = rclient.call(request)
|
||||||
# 应该先add_resource了
|
# 应该先add_resource了
|
||||||
res.response = "OK"
|
res.response = "OK"
|
||||||
@@ -385,7 +409,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
res.response = serialize_result_info(traceback.format_exc(), False, {})
|
res.response = serialize_result_info(traceback.format_exc(), False, {})
|
||||||
return res
|
return res
|
||||||
# 接下来该根据bind_parent_id进行assign了,目前只有plr可以进行assign,不然没有办法输入到物料系统中
|
# 接下来该根据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)]
|
# request.resources = [convert_to_ros_msg(Resource, resources)]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -435,7 +460,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
"bind_parent_id": bind_parent_id,
|
"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):
|
def done_cb(*args):
|
||||||
self.lab_logger().info(f"向meshmanager发送新增resource完成")
|
self.lab_logger().info(f"向meshmanager发送新增resource完成")
|
||||||
@@ -601,10 +626,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
goal = goal_handle.request
|
goal = goal_handle.request
|
||||||
|
|
||||||
# 从目标消息中提取参数, 并调用对应的方法
|
# 从目标消息中提取参数, 并调用对应的方法
|
||||||
if "sequence" in self._action_value_mappings:
|
if "sequence" in action_value_mapping:
|
||||||
# 如果一个指令对应函数的连续调用,如启动和等待结果,默认参数应该属于第一个函数调用
|
# 如果一个指令对应函数的连续调用,如启动和等待结果,默认参数应该属于第一个函数调用
|
||||||
def ACTION(**kwargs):
|
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:
|
if i == 0:
|
||||||
self.lab_logger().info(f"执行序列动作第一步: {action}")
|
self.lab_logger().info(f"执行序列动作第一步: {action}")
|
||||||
self.get_real_function(self.driver_instance, action)[0](**kwargs)
|
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.lab_logger().info(f"执行序列动作后续步骤: {action}")
|
||||||
self.get_real_function(self.driver_instance, action)[0]()
|
self.get_real_function(self.driver_instance, action)[0]()
|
||||||
|
|
||||||
action_paramtypes = get_type_hints(
|
action_paramtypes = self.get_real_function(self.driver_instance, action_value_mapping["sequence"][0])[1]
|
||||||
self.get_real_function(self.driver_instance, self._action_value_mappings["sequence"][0])
|
|
||||||
)[1]
|
|
||||||
else:
|
else:
|
||||||
ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name)
|
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
|
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
|
||||||
|
|
||||||
if self._driver_class is 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:
|
else:
|
||||||
self._driver_creator = DeviceClassCreator(driver_class)
|
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||||
|
|
||||||
if driver_is_ros:
|
if driver_is_ros:
|
||||||
driver_params["device_id"] = device_id
|
driver_params["device_id"] = device_id
|
||||||
|
|||||||
@@ -383,8 +383,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
liquid_volume: list[int],
|
liquid_volume: list[int],
|
||||||
slot_on_deck: str,
|
slot_on_deck: str,
|
||||||
):
|
):
|
||||||
init_new_res = initialize_resource(
|
res_creation_input = {
|
||||||
{
|
|
||||||
"name": res_id,
|
"name": res_id,
|
||||||
"class": class_name,
|
"class": class_name,
|
||||||
"parent": parent,
|
"parent": parent,
|
||||||
@@ -394,7 +393,14 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
"z": bind_locations.z,
|
"z": bind_locations.z,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
) # flatten的格式
|
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]
|
resources = init_new_res # initialize_resource已经返回list[dict]
|
||||||
device_ids = [device_id]
|
device_ids = [device_id]
|
||||||
bind_parent_id = [parent]
|
bind_parent_id = [parent]
|
||||||
|
|||||||
@@ -256,12 +256,12 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
|||||||
return write_func(*args, **kwargs)
|
return write_func(*args, **kwargs)
|
||||||
|
|
||||||
if read_method:
|
if read_method:
|
||||||
bound_read = MethodType(_read, device.driver_instance)
|
# bound_read = MethodType(_read, device.driver_instance)
|
||||||
setattr(device.driver_instance, read_method, bound_read)
|
setattr(device.driver_instance, read_method, _read)
|
||||||
|
|
||||||
if write_method:
|
if write_method:
|
||||||
bound_write = MethodType(_write, device.driver_instance)
|
# bound_write = MethodType(_write, device.driver_instance)
|
||||||
setattr(device.driver_instance, write_method, bound_write)
|
setattr(device.driver_instance, write_method, _write)
|
||||||
|
|
||||||
|
|
||||||
async def _update_resources(self, goal, protocol_kwargs):
|
async def _update_resources(self, goal, protocol_kwargs):
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class DeviceNodeResourceTracker(object):
|
|||||||
def clear_resource(self):
|
def clear_resource(self):
|
||||||
self.resources = []
|
self.resources = []
|
||||||
|
|
||||||
def figure_resource(self, query_resource):
|
def figure_resource(self, query_resource, try_mode=False):
|
||||||
if isinstance(query_resource, list):
|
if isinstance(query_resource, list):
|
||||||
return [self.figure_resource(r) for r in query_resource]
|
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)
|
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(
|
res_list.extend(
|
||||||
self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(query_resource, identifier_key))
|
self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(query_resource, identifier_key))
|
||||||
)
|
)
|
||||||
|
if not try_mode:
|
||||||
|
assert len(res_list) > 0, f"没有找到资源 {query_resource},请检查资源是否存在"
|
||||||
assert len(res_list) == 1, f"{query_resource} 找到多个资源,请检查资源是否唯一: {res_list}"
|
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(query_resource)] = res_list[0][0]
|
||||||
self.resource2parent_resource[id(res_list[0][1])] = res_list[0][0]
|
self.resource2parent_resource[id(res_list[0][1])] = res_list[0][0]
|
||||||
# 后续加入其他对比方式
|
|
||||||
return res_list[0][1]
|
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]]:
|
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", [])
|
children = getattr(resource, "children", [])
|
||||||
for child in children:
|
for child in children:
|
||||||
res_list.extend(self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource))
|
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 target_resource_cls_type == type(resource):
|
||||||
if hasattr(resource, identifier_key):
|
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:
|
if getattr(resource, identifier_key) == compare_value:
|
||||||
res_list.append((parent_res, resource))
|
res_list.append((parent_res, resource))
|
||||||
return res_list
|
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_cls = cls
|
||||||
self.device_instance: Optional[T] = None
|
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:
|
def create_instance(self, data: Dict[str, Any]) -> T:
|
||||||
"""
|
"""
|
||||||
@@ -60,6 +72,7 @@ class DeviceClassCreator(Generic[T]):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.post_create()
|
self.post_create()
|
||||||
|
self.attach_resource()
|
||||||
return self.device_instance
|
return self.device_instance
|
||||||
|
|
||||||
def get_instance(self) -> Optional[T]:
|
def get_instance(self) -> Optional[T]:
|
||||||
@@ -90,14 +103,15 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
|
|||||||
cls: PyLabRobot设备类
|
cls: PyLabRobot设备类
|
||||||
children: 子资源字典,用于资源替换
|
children: 子资源字典,用于资源替换
|
||||||
"""
|
"""
|
||||||
super().__init__(cls)
|
super().__init__(cls, children, resource_tracker)
|
||||||
self.children = children
|
|
||||||
self.resource_tracker = resource_tracker
|
|
||||||
# 检查类是否具有deserialize方法
|
# 检查类是否具有deserialize方法
|
||||||
self.has_deserialize = hasattr(cls, "deserialize") and callable(getattr(cls, "deserialize"))
|
self.has_deserialize = hasattr(cls, "deserialize") and callable(getattr(cls, "deserialize"))
|
||||||
if not self.has_deserialize:
|
if not self.has_deserialize:
|
||||||
logger.warning(f"类 {cls.__name__} 没有deserialize方法,将使用标准构造函数")
|
logger.warning(f"类 {cls.__name__} 没有deserialize方法,将使用标准构造函数")
|
||||||
|
|
||||||
|
def attach_resource(self):
|
||||||
|
pass # 只能增加实例化物料,原来默认物料仅为字典查询
|
||||||
|
|
||||||
def _process_resource_mapping(self, resource, source_type):
|
def _process_resource_mapping(self, resource, source_type):
|
||||||
if source_type == dict:
|
if source_type == dict:
|
||||||
from pylabrobot.resources.resource import Resource
|
from pylabrobot.resources.resource import Resource
|
||||||
@@ -260,7 +274,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
|
|||||||
这个类提供了针对ProtocolNode设备类的实例创建方法,处理children参数。
|
这个类提供了针对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设备类创建器
|
初始化ProtocolNode设备类创建器
|
||||||
|
|
||||||
@@ -268,8 +282,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
|
|||||||
cls: ProtocolNode设备类
|
cls: ProtocolNode设备类
|
||||||
children: 子资源字典,用于资源替换
|
children: 子资源字典,用于资源替换
|
||||||
"""
|
"""
|
||||||
super().__init__(cls)
|
super().__init__(cls, children, resource_tracker)
|
||||||
self.children = children
|
|
||||||
|
|
||||||
def create_instance(self, data: Dict[str, Any]) -> T:
|
def create_instance(self, data: Dict[str, Any]) -> T:
|
||||||
"""
|
"""
|
||||||
@@ -282,8 +295,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
|
|||||||
ProtocolNode设备类实例
|
ProtocolNode设备类实例
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# 创建实例,额外补充一个给protocol node的字段,后面考虑取消
|
||||||
# 创建实例
|
|
||||||
data["children"] = self.children
|
data["children"] = self.children
|
||||||
self.device_instance = super(ProtocolNodeCreator, self).create_instance(data)
|
self.device_instance = super(ProtocolNodeCreator, self).create_instance(data)
|
||||||
self.post_create()
|
self.post_create()
|
||||||
|
|||||||
Reference in New Issue
Block a user