mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-27 15:17:34 +00:00
Compare commits
2 Commits
f7db8d17c5
...
v0.9.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7db3123547 | ||
|
|
6da7a20a7a |
132
.github/workflows/multi-platform-build.yml
vendored
132
.github/workflows/multi-platform-build.yml
vendored
@@ -1,132 +0,0 @@
|
|||||||
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,7 +234,3 @@ CATKIN_IGNORE
|
|||||||
|
|
||||||
*.graphml
|
*.graphml
|
||||||
unilabos/device_mesh/view_robot.rviz
|
unilabos/device_mesh/view_robot.rviz
|
||||||
|
|
||||||
|
|
||||||
# Certs
|
|
||||||
**/.certs
|
|
||||||
@@ -2,10 +2,4 @@
|
|||||||
|
|
||||||
```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,9 +3,7 @@
|
|||||||
{
|
{
|
||||||
"id": "MockChiller1",
|
"id": "MockChiller1",
|
||||||
"name": "模拟冷却器",
|
"name": "模拟冷却器",
|
||||||
"children": [
|
"children": [],
|
||||||
"MockContainerForChiller1"
|
|
||||||
],
|
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "mock_chiller",
|
"class": "mock_chiller",
|
||||||
@@ -27,22 +25,6 @@
|
|||||||
"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": "模拟过滤器",
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
container:
|
|
||||||
description: regular organic container
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.container:RegularContainer
|
|
||||||
type: unilabos
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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,10 +4,6 @@ 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
|
||||||
@@ -470,10 +466,6 @@ 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,7 +19,6 @@ 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,
|
||||||
@@ -345,7 +344,6 @@ 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
|
||||||
# 本地拿到这个物料,可能需要先做初始化?
|
# 本地拿到这个物料,可能需要先做初始化?
|
||||||
@@ -364,28 +362,6 @@ 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"
|
||||||
@@ -409,8 +385,7 @@ 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,不然没有办法输入到物料系统中
|
||||||
if bind_parent_id != self.node_name:
|
resource = self.resource_tracker.figure_resource({"name": bind_parent_id})
|
||||||
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:
|
||||||
@@ -460,7 +435,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)
|
future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4())
|
||||||
|
|
||||||
def done_cb(*args):
|
def done_cb(*args):
|
||||||
self.lab_logger().info(f"向meshmanager发送新增resource完成")
|
self.lab_logger().info(f"向meshmanager发送新增resource完成")
|
||||||
@@ -926,9 +901,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, resource_tracker=self.resource_tracker)
|
self._driver_creator = ProtocolNodeCreator(driver_class, children=children)
|
||||||
else:
|
else:
|
||||||
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
self._driver_creator = DeviceClassCreator(driver_class)
|
||||||
|
|
||||||
if driver_is_ros:
|
if driver_is_ros:
|
||||||
driver_params["device_id"] = device_id
|
driver_params["device_id"] = device_id
|
||||||
|
|||||||
@@ -383,24 +383,18 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
liquid_volume: list[int],
|
liquid_volume: list[int],
|
||||||
slot_on_deck: str,
|
slot_on_deck: str,
|
||||||
):
|
):
|
||||||
res_creation_input = {
|
init_new_res = initialize_resource(
|
||||||
"name": res_id,
|
{
|
||||||
"class": class_name,
|
"name": res_id,
|
||||||
"parent": parent,
|
"class": class_name,
|
||||||
"position": {
|
"parent": parent,
|
||||||
"x": bind_locations.x,
|
"position": {
|
||||||
"y": bind_locations.y,
|
"x": bind_locations.x,
|
||||||
"z": bind_locations.z,
|
"y": bind_locations.y,
|
||||||
},
|
"z": bind_locations.z,
|
||||||
}
|
},
|
||||||
if len(liquid_input_slot) and liquid_input_slot[0] == -1: # 目前container只逐个创建
|
}
|
||||||
res_creation_input.update({
|
) # flatten的格式
|
||||||
"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]
|
||||||
|
|||||||
@@ -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, try_mode=False):
|
def figure_resource(self, query_resource):
|
||||||
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,14 +45,10 @@ 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) == 1, f"{query_resource} 找到多个资源,请检查资源是否唯一: {res_list}"
|
||||||
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(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]]:
|
||||||
@@ -61,12 +57,8 @@ 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):
|
if target_resource_cls_type == type(resource) or target_resource_cls_type == dict:
|
||||||
if target_resource_cls_type == dict:
|
if hasattr(resource, identifier_key):
|
||||||
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], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker):
|
def __init__(self, cls: Type[T]):
|
||||||
"""
|
"""
|
||||||
初始化设备类创建器
|
初始化设备类创建器
|
||||||
|
|
||||||
@@ -42,18 +42,6 @@ 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:
|
||||||
"""
|
"""
|
||||||
@@ -72,7 +60,6 @@ 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]:
|
||||||
@@ -103,15 +90,14 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
|
|||||||
cls: PyLabRobot设备类
|
cls: PyLabRobot设备类
|
||||||
children: 子资源字典,用于资源替换
|
children: 子资源字典,用于资源替换
|
||||||
"""
|
"""
|
||||||
super().__init__(cls, children, resource_tracker)
|
super().__init__(cls)
|
||||||
|
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
|
||||||
@@ -274,7 +260,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
|
|||||||
这个类提供了针对ProtocolNode设备类的实例创建方法,处理children参数。
|
这个类提供了针对ProtocolNode设备类的实例创建方法,处理children参数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cls: Type[T], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker):
|
def __init__(self, cls: Type[T], children: Dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
初始化ProtocolNode设备类创建器
|
初始化ProtocolNode设备类创建器
|
||||||
|
|
||||||
@@ -282,7 +268,8 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
|
|||||||
cls: ProtocolNode设备类
|
cls: ProtocolNode设备类
|
||||||
children: 子资源字典,用于资源替换
|
children: 子资源字典,用于资源替换
|
||||||
"""
|
"""
|
||||||
super().__init__(cls, children, resource_tracker)
|
super().__init__(cls)
|
||||||
|
self.children = children
|
||||||
|
|
||||||
def create_instance(self, data: Dict[str, Any]) -> T:
|
def create_instance(self, data: Dict[str, Any]) -> T:
|
||||||
"""
|
"""
|
||||||
@@ -295,7 +282,8 @@ 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