mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-14 21:24:40 +00:00
Fix/resource UUID and doc fix (#109)
* Fix ResourceTreeSet load error * Raise error when using unsupported type to create ResourceTreeSet * Fix children key error * Fix children key error * Fix workstation resource not tracking * Fix workstation deck & children resource dupe * Fix workstation deck & children resource dupe * Fix multiple resource error * Fix resource tree update * Fix resource tree update * Force confirm uuid * Tip more error log * Refactor Bioyond workstation and experiment workflow (#105) Refactored the Bioyond workstation classes to improve parameter handling and workflow management. Updated experiment.py to use BioyondReactionStation with deck and material mappings, and enhanced workflow step parameter mapping and execution logic. Adjusted JSON experiment configs, improved workflow sequence handling, and added UUID assignment to PLR materials. Removed unused station_config and material cache logic, and added detailed docstrings and debug output for workflow methods. * Fix resource get. Fix resource parent not found. Mapping uuid for all resources. * mount parent uuid * Add logging configuration based on BasicConfig in main function * fix workstation node error * fix workstation node error * Update boot example * temp fix for resource get * temp fix for resource get * provide error info when cant find plr type * pack repo info * fix to plr type error * fix to plr type error * Update regular container method * support no size init * fix comprehensive_station.json * fix comprehensive_station.json * fix type conversion * fix state loading for regular container * Update deploy-docs.yml * Update deploy-docs.yml --------- Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
This commit is contained in:
8
.github/workflows/conda-pack-build.yml
vendored
8
.github/workflows/conda-pack-build.yml
vendored
@@ -242,6 +242,10 @@ jobs:
|
|||||||
echo Adding: verify_installation.py
|
echo Adding: verify_installation.py
|
||||||
copy scripts\verify_installation.py dist-package\
|
copy scripts\verify_installation.py dist-package\
|
||||||
|
|
||||||
|
rem Copy source code repository (including .git)
|
||||||
|
echo Adding: Uni-Lab-OS source repository
|
||||||
|
robocopy . dist-package\Uni-Lab-OS /E /XD dist-package /NFL /NDL /NJH /NJS /NC /NS || if %ERRORLEVEL% LSS 8 exit /b 0
|
||||||
|
|
||||||
rem Create README using Python script
|
rem Create README using Python script
|
||||||
echo Creating: README.txt
|
echo Creating: README.txt
|
||||||
python scripts\create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package\README.txt
|
python scripts\create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package\README.txt
|
||||||
@@ -274,6 +278,10 @@ jobs:
|
|||||||
echo "Adding: verify_installation.py"
|
echo "Adding: verify_installation.py"
|
||||||
cp scripts/verify_installation.py dist-package/
|
cp scripts/verify_installation.py dist-package/
|
||||||
|
|
||||||
|
# Copy source code repository (including .git)
|
||||||
|
echo "Adding: Uni-Lab-OS source repository"
|
||||||
|
rsync -a --exclude='dist-package' . dist-package/Uni-Lab-OS
|
||||||
|
|
||||||
# Create README using Python script
|
# Create README using Python script
|
||||||
echo "Creating: README.txt"
|
echo "Creating: README.txt"
|
||||||
python scripts/create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package/README.txt
|
python scripts/create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package/README.txt
|
||||||
|
|||||||
43
.github/workflows/deploy-docs.yml
vendored
43
.github/workflows/deploy-docs.yml
vendored
@@ -39,24 +39,39 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.branch || github.ref }}
|
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Python environment
|
- name: Setup Miniforge (with mamba)
|
||||||
uses: actions/setup-python@v5
|
uses: conda-incubator/setup-miniconda@v3
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
miniforge-version: latest
|
||||||
|
use-mamba: true
|
||||||
|
python-version: '3.11.11'
|
||||||
|
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||||
|
channel-priority: flexible
|
||||||
|
activate-environment: unilab
|
||||||
|
auto-update-conda: false
|
||||||
|
show-channel-urls: true
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Install unilabos and dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
echo "Installing unilabos and dependencies to unilab environment..."
|
||||||
sudo apt-get install -y pandoc
|
echo "Using mamba for faster and more reliable dependency resolution..."
|
||||||
|
mamba install -n unilab uni-lab::unilabos -c uni-lab -c robostack-staging -c conda-forge -y
|
||||||
|
|
||||||
- name: Install Python dependencies
|
- name: Install latest unilabos from source
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
echo "Uninstalling existing unilabos..."
|
||||||
# Install package in development mode to get version info
|
mamba run -n unilab pip uninstall unilabos -y || echo "unilabos not installed via pip"
|
||||||
pip install -e .
|
echo "Installing unilabos from source..."
|
||||||
# Install documentation dependencies
|
mamba run -n unilab pip install .
|
||||||
pip install -r docs/requirements.txt
|
echo "Verifying installation..."
|
||||||
|
mamba run -n unilab pip show unilabos
|
||||||
|
|
||||||
|
- name: Install documentation dependencies
|
||||||
|
run: |
|
||||||
|
echo "Installing documentation build dependencies..."
|
||||||
|
mamba run -n unilab pip install -r docs/requirements.txt
|
||||||
|
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
id: pages
|
id: pages
|
||||||
@@ -68,8 +83,8 @@ jobs:
|
|||||||
cd docs
|
cd docs
|
||||||
# Clean previous builds
|
# Clean previous builds
|
||||||
rm -rf _build
|
rm -rf _build
|
||||||
# Build HTML documentation
|
# Build HTML documentation in conda environment
|
||||||
python -m sphinx -b html . _build/html -v
|
mamba run -n unilab python -m sphinx -b html . _build/html -v
|
||||||
|
|
||||||
- name: Check build results
|
- name: Check build results
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
使用以下命令启动模拟反应器:
|
使用以下命令启动模拟反应器:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
unilab -g test/experiments/mock_reactor.json --app_bridges ""
|
unilab -g test/experiments/mock_reactor.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 执行抽真空和充气操作
|
### 2. 执行抽真空和充气操作
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ extensions = [
|
|||||||
"myst_parser",
|
"myst_parser",
|
||||||
"sphinx.ext.autodoc",
|
"sphinx.ext.autodoc",
|
||||||
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
|
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
|
||||||
"sphinx_rtd_theme"
|
"sphinx_rtd_theme",
|
||||||
]
|
]
|
||||||
|
|
||||||
source_suffix = {
|
source_suffix = {
|
||||||
|
|||||||
@@ -170,15 +170,16 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 1000.0
|
"max_volume": 1000.0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
|
"size_x": 200,
|
||||||
|
"size_y": 150,
|
||||||
|
"size_z": 0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [
|
"liquids": [["DMF", 500.0]],
|
||||||
{
|
"pending_liquids": [["DMF", 500.0]]
|
||||||
"liquid_type": "DMF",
|
|
||||||
"liquid_volume": 1000.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -194,15 +195,16 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 1000.0
|
"max_volume": 1000.0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
|
"size_x": 200,
|
||||||
|
"size_y": 150,
|
||||||
|
"size_z": 0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [
|
"liquids": [["ethyl_acetate", 1000.0]],
|
||||||
{
|
"pending_liquids": [["ethyl_acetate", 1000.0]]
|
||||||
"liquid_type": "ethyl_acetate",
|
|
||||||
"liquid_volume": 1000.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -218,15 +220,16 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 1000.0
|
"max_volume": 1000.0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
|
"size_x": 300,
|
||||||
|
"size_y": 150,
|
||||||
|
"size_z": 0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [
|
"liquids": [["hexane", 1000.0]],
|
||||||
{
|
"pending_liquids": [["hexane", 1000.0]]
|
||||||
"liquid_type": "hexane",
|
|
||||||
"liquid_volume": 1000.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -242,15 +245,16 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 1000.0
|
"max_volume": 1000.0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
|
"size_x": 900,
|
||||||
|
"size_y": 150,
|
||||||
|
"size_z": 0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [
|
"liquids": [["methanol", 1000.0]],
|
||||||
{
|
"pending_liquids": [["methanol", 1000.0]]
|
||||||
"liquid_type": "methanol",
|
|
||||||
"liquid_volume": 1000.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -266,15 +270,16 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 1000.0
|
"max_volume": 1000.0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
|
"size_x": 950,
|
||||||
|
"size_y": 150,
|
||||||
|
"size_z": 0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [
|
"liquids": [["water", 1000.0]],
|
||||||
{
|
"pending_liquids": [["water", 1000.0]]
|
||||||
"liquid_type": "water",
|
|
||||||
"liquid_volume": 1000.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -335,14 +340,16 @@
|
|||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 500.0,
|
"max_volume": 500.0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
"max_temp": 200.0,
|
"max_temp": 200.0,
|
||||||
"min_temp": -20.0,
|
"min_temp": -20.0,
|
||||||
"has_stirrer": true,
|
"has_stirrer": true,
|
||||||
"has_heater": true
|
"has_heater": true
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [
|
"liquids": [],
|
||||||
]
|
"pending_liquids": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -419,11 +426,16 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 2000.0
|
"max_volume": 2000.0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
|
"size_x": 500,
|
||||||
|
"size_y": 400,
|
||||||
|
"size_z": 0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [
|
"liquids": [],
|
||||||
]
|
"pending_liquids": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -439,11 +451,16 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 2000.0
|
"max_volume": 2000.0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
|
"size_x": 1100,
|
||||||
|
"size_y": 500,
|
||||||
|
"size_z": 0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [
|
"liquids": [],
|
||||||
]
|
"pending_liquids": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -649,11 +666,16 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 250.0
|
"max_volume": 250.0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
|
"size_x": 900,
|
||||||
|
"size_y": 500,
|
||||||
|
"size_z": 0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [
|
"liquids": [],
|
||||||
]
|
"pending_liquids": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -669,11 +691,16 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 250.0
|
"max_volume": 250.0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
|
"size_x": 950,
|
||||||
|
"size_y": 500,
|
||||||
|
"size_z": 0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [
|
"liquids": [],
|
||||||
]
|
"pending_liquids": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -689,11 +716,16 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 250.0
|
"max_volume": 250.0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
|
"size_x": 1050,
|
||||||
|
"size_y": 500,
|
||||||
|
"size_z": 0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [
|
"liquids": [],
|
||||||
]
|
"pending_liquids": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -733,6 +765,11 @@
|
|||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max_volume": 500.0,
|
"max_volume": 500.0,
|
||||||
|
"size_x": 550,
|
||||||
|
"size_y": 250,
|
||||||
|
"size_z": 0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
"reagent": "sodium_chloride",
|
"reagent": "sodium_chloride",
|
||||||
"physical_state": "solid"
|
"physical_state": "solid"
|
||||||
},
|
},
|
||||||
@@ -756,6 +793,11 @@
|
|||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 500.0,
|
"volume": 500.0,
|
||||||
|
"size_x": 600,
|
||||||
|
"size_y": 250,
|
||||||
|
"size_z": 0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
"reagent": "sodium_carbonate",
|
"reagent": "sodium_carbonate",
|
||||||
"physical_state": "solid"
|
"physical_state": "solid"
|
||||||
},
|
},
|
||||||
@@ -779,6 +821,11 @@
|
|||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 500.0,
|
"volume": 500.0,
|
||||||
|
"size_x": 650,
|
||||||
|
"size_y": 250,
|
||||||
|
"size_z": 0,
|
||||||
|
"type": "RegularContainer",
|
||||||
|
"category": "container",
|
||||||
"reagent": "magnesium_chloride",
|
"reagent": "magnesium_chloride",
|
||||||
"physical_state": "solid"
|
"physical_state": "solid"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
],
|
],
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "dispensing_station.bioyond",
|
"class": "workstation.bioyond_dispensing_station",
|
||||||
"config": {
|
"config": {
|
||||||
"config": {
|
"config": {
|
||||||
"api_key": "DE9BDDA0",
|
"api_key": "DE9BDDA0",
|
||||||
@@ -20,13 +20,6 @@
|
|||||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck"
|
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"station_config": {
|
|
||||||
"station_type": "dispensing_station",
|
|
||||||
"enable_dispensing_station": true,
|
|
||||||
"enable_reaction_station": false,
|
|
||||||
"station_name": "DispensingStation_001",
|
|
||||||
"description": "Bioyond配液工作站"
|
|
||||||
},
|
|
||||||
"protocol_type": []
|
"protocol_type": []
|
||||||
},
|
},
|
||||||
"data": {}
|
"data": {}
|
||||||
|
|||||||
@@ -24,9 +24,13 @@
|
|||||||
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
||||||
},
|
},
|
||||||
"material_type_mappings": {
|
"material_type_mappings": {
|
||||||
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
"烧杯": ["BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"],
|
||||||
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
"试剂瓶": ["BIOYOND_PolymerStation_1BottleCarrier", ""],
|
||||||
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
|
"样品板": ["BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"],
|
||||||
|
"分装板": ["BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"],
|
||||||
|
"样品瓶": ["BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"],
|
||||||
|
"90%分装小瓶": ["BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"],
|
||||||
|
"10%分装小瓶": ["BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deck": {
|
"deck": {
|
||||||
@@ -42,7 +46,6 @@
|
|||||||
{
|
{
|
||||||
"id": "Bioyond_Deck",
|
"id": "Bioyond_Deck",
|
||||||
"name": "Bioyond_Deck",
|
"name": "Bioyond_Deck",
|
||||||
"sample_id": null,
|
|
||||||
"children": [
|
"children": [
|
||||||
],
|
],
|
||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ def main():
|
|||||||
working_dir = os.path.abspath(os.getcwd())
|
working_dir = os.path.abspath(os.getcwd())
|
||||||
else:
|
else:
|
||||||
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||||
|
|
||||||
if args_dict.get("working_dir"):
|
if args_dict.get("working_dir"):
|
||||||
working_dir = args_dict.get("working_dir", "")
|
working_dir = args_dict.get("working_dir", "")
|
||||||
if config_path and not os.path.exists(config_path):
|
if config_path and not os.path.exists(config_path):
|
||||||
@@ -211,6 +212,14 @@ def main():
|
|||||||
# 加载配置文件
|
# 加载配置文件
|
||||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||||
load_config_from_file(config_path)
|
load_config_from_file(config_path)
|
||||||
|
|
||||||
|
# 根据配置重新设置日志级别
|
||||||
|
from unilabos.utils.log import configure_logger, logger
|
||||||
|
|
||||||
|
if hasattr(BasicConfig, "log_level"):
|
||||||
|
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
|
||||||
|
configure_logger(loglevel=BasicConfig.log_level)
|
||||||
|
|
||||||
if args_dict["addr"] == "test":
|
if args_dict["addr"] == "test":
|
||||||
print_status("使用测试环境地址", "info")
|
print_status("使用测试环境地址", "info")
|
||||||
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ class HTTPClient:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
|
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
|
||||||
"""
|
"""
|
||||||
|
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps({"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}, indent=4))
|
||||||
# 从序列化数据中提取所有节点的UUID(保存旧UUID)
|
# 从序列化数据中提取所有节点的UUID(保存旧UUID)
|
||||||
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
|
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
|
||||||
if not self.initialized or first_add:
|
if not self.initialized or first_add:
|
||||||
@@ -92,6 +94,8 @@ class HTTPClient:
|
|||||||
timeout=100,
|
timeout=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_add.json"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"{response.status_code}" + "\n" + response.text)
|
||||||
# 处理响应,构建UUID映射
|
# 处理响应,构建UUID映射
|
||||||
uuid_mapping = {}
|
uuid_mapping = {}
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import base64
|
|||||||
import traceback
|
import traceback
|
||||||
import os
|
import os
|
||||||
import importlib.util
|
import importlib.util
|
||||||
from typing import Optional
|
from typing import Optional, Literal
|
||||||
from unilabos.utils import logger
|
from unilabos.utils import logger
|
||||||
|
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ class BasicConfig:
|
|||||||
vis_2d_enable = False
|
vis_2d_enable = False
|
||||||
enable_resource_load = True
|
enable_resource_load = True
|
||||||
communication_protocol = "websocket"
|
communication_protocol = "websocket"
|
||||||
|
log_level: Literal['TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = "DEBUG" # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def auth_secret(cls):
|
def auth_secret(cls):
|
||||||
|
|||||||
@@ -6,8 +6,15 @@ from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstati
|
|||||||
|
|
||||||
|
|
||||||
class BioyondDispensingStation(BioyondWorkstation):
|
class BioyondDispensingStation(BioyondWorkstation):
|
||||||
def __init__(self, config):
|
def __init__(
|
||||||
super().__init__(config)
|
self,
|
||||||
|
config,
|
||||||
|
# 桌子
|
||||||
|
deck,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(config, deck, *args, **kwargs)
|
||||||
# self.config = config
|
# self.config = config
|
||||||
# self.api_key = config["api_key"]
|
# self.api_key = config["api_key"]
|
||||||
# self.host = config["api_host"]
|
# self.host = config["api_host"]
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
# experiment_workflow.py
|
|
||||||
"""
|
"""
|
||||||
实验流程主程序
|
实验流程主程序
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from bioyond_rpc import BioyondV1RPC
|
from reaction_station import BioyondReactionStation
|
||||||
from config import API_CONFIG, WORKFLOW_MAPPINGS
|
from config import API_CONFIG, WORKFLOW_MAPPINGS, DECK_CONFIG, MATERIAL_TYPE_MAPPINGS
|
||||||
|
|
||||||
|
|
||||||
def run_experiment():
|
def run_experiment():
|
||||||
@@ -14,15 +13,20 @@ def run_experiment():
|
|||||||
# 初始化Bioyond客户端
|
# 初始化Bioyond客户端
|
||||||
config = {
|
config = {
|
||||||
**API_CONFIG,
|
**API_CONFIG,
|
||||||
"workflow_mappings": WORKFLOW_MAPPINGS
|
"workflow_mappings": WORKFLOW_MAPPINGS,
|
||||||
|
"material_type_mappings": MATERIAL_TYPE_MAPPINGS
|
||||||
}
|
}
|
||||||
|
|
||||||
Bioyond = BioyondV1RPC(config)
|
# 创建BioyondReactionStation实例,传入deck配置
|
||||||
|
Bioyond = BioyondReactionStation(
|
||||||
|
config=config,
|
||||||
|
deck=DECK_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
print("\n============= 多工作流参数测试(简化接口+材料缓存)=============")
|
print("\n============= 多工作流参数测试(简化接口+材料缓存)=============")
|
||||||
|
|
||||||
# 显示可用的材料名称(前20个)
|
# 显示可用的材料名称(前20个)
|
||||||
available_materials = Bioyond.get_available_materials()
|
available_materials = Bioyond.hardware_interface.get_available_materials()
|
||||||
print(f"可用材料名称(前20个): {available_materials[:20]}")
|
print(f"可用材料名称(前20个): {available_materials[:20]}")
|
||||||
print(f"总共有 {len(available_materials)} 个材料可用\n")
|
print(f"总共有 {len(available_materials)} 个材料可用\n")
|
||||||
|
|
||||||
@@ -84,32 +88,32 @@ def run_experiment():
|
|||||||
material_id="3",
|
material_id="3",
|
||||||
time="180",
|
time="180",
|
||||||
torque_variation="2",
|
torque_variation="2",
|
||||||
assign_material_name="BTDA-1",
|
assign_material_name="BTDA1",
|
||||||
temperature=-10.00
|
temperature=-10.00
|
||||||
)
|
)
|
||||||
|
#二杆,样品版90
|
||||||
print("7. 添加固体进料小瓶,带参数...")
|
print("7. 添加固体进料小瓶,带参数...")
|
||||||
Bioyond.solid_feeding_vials(
|
Bioyond.solid_feeding_vials(
|
||||||
material_id="3",
|
material_id="3",
|
||||||
time="180",
|
time="180",
|
||||||
torque_variation="2",
|
torque_variation="2",
|
||||||
assign_material_name="BTDA-2",
|
assign_material_name="BTDA2",
|
||||||
temperature=25.00
|
temperature=25.00
|
||||||
)
|
)
|
||||||
|
#二杆,样品版90
|
||||||
print("8. 添加固体进料小瓶,带参数...")
|
print("8. 添加固体进料小瓶,带参数...")
|
||||||
Bioyond.solid_feeding_vials(
|
Bioyond.solid_feeding_vials(
|
||||||
material_id="3",
|
material_id="3",
|
||||||
time="480",
|
time="480",
|
||||||
torque_variation="2",
|
torque_variation="2",
|
||||||
assign_material_name="BTDA-3",
|
assign_material_name="BTDA3",
|
||||||
temperature=25.00
|
temperature=25.00
|
||||||
)
|
)
|
||||||
|
|
||||||
# 液体投料滴定(第一个)
|
# 液体投料滴定(第一个)
|
||||||
print("9. 添加液体投料滴定,带参数...") # ODPA
|
print("9. 添加液体投料滴定,带参数...") # ODPA
|
||||||
Bioyond.liquid_feeding_titration(
|
Bioyond.liquid_feeding_titration(
|
||||||
volume_formula="1000",
|
volume_formula="{{6-0-5}}+{{7-0-5}}+{{8-0-5}}",
|
||||||
assign_material_name="BTDA-DD",
|
assign_material_name="BTDA-DD",
|
||||||
titration_type="1",
|
titration_type="1",
|
||||||
time="360",
|
time="360",
|
||||||
@@ -169,8 +173,6 @@ def run_experiment():
|
|||||||
temperature="25.00"
|
temperature="25.00"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
print("15. 添加液体投料溶剂,带参数...")
|
print("15. 添加液体投料溶剂,带参数...")
|
||||||
Bioyond.liquid_feeding_solvents(
|
Bioyond.liquid_feeding_solvents(
|
||||||
assign_material_name="PGME",
|
assign_material_name="PGME",
|
||||||
@@ -194,8 +196,8 @@ def run_experiment():
|
|||||||
print("\n4. 执行process_and_execute_workflow...")
|
print("\n4. 执行process_and_execute_workflow...")
|
||||||
|
|
||||||
result = Bioyond.process_and_execute_workflow(
|
result = Bioyond.process_and_execute_workflow(
|
||||||
workflow_name="test3_86",
|
workflow_name="test3_8",
|
||||||
task_name="实验3_86"
|
task_name="实验3_8"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 显示执行结果
|
# 显示执行结果
|
||||||
|
|||||||
@@ -1,30 +1,67 @@
|
|||||||
import json
|
import json
|
||||||
|
from typing import List, Dict, Any
|
||||||
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
||||||
from unilabos.devices.workstation.bioyond_studio.config import (
|
from unilabos.devices.workstation.bioyond_studio.config import (
|
||||||
API_CONFIG, WORKFLOW_MAPPINGS, WORKFLOW_STEP_IDS, MATERIAL_TYPE_MAPPINGS,
|
WORKFLOW_STEP_IDS,
|
||||||
STATION_TYPES, DEFAULT_STATION_CONFIG
|
WORKFLOW_TO_SECTION_MAP
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BioyondReactionStation(BioyondWorkstation):
|
class BioyondReactionStation(BioyondWorkstation):
|
||||||
def __init__(self, config: dict = None):
|
"""Bioyond反应站类
|
||||||
super().__init__(config)
|
|
||||||
|
继承自BioyondWorkstation,提供反应站特定的业务方法
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: dict = None, deck=None):
|
||||||
|
"""初始化反应站
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: 配置字典,应包含workflow_mappings等配置
|
||||||
|
deck: Deck对象
|
||||||
|
"""
|
||||||
|
# 如果 deck 作为独立参数传入,使用它;否则尝试从 config 中提取
|
||||||
|
if deck is None and config:
|
||||||
|
deck = config.get('deck')
|
||||||
|
|
||||||
|
# 调试信息:检查传入的config
|
||||||
|
print(f"BioyondReactionStation初始化 - config包含workflow_mappings: {'workflow_mappings' in (config or {})}")
|
||||||
|
if config and 'workflow_mappings' in config:
|
||||||
|
print(f"workflow_mappings内容: {config['workflow_mappings']}")
|
||||||
|
|
||||||
|
# 将 config 作为 bioyond_config 传递给父类
|
||||||
|
super().__init__(bioyond_config=config, deck=deck)
|
||||||
|
|
||||||
|
# 调试信息:检查初始化后的workflow_mappings
|
||||||
|
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
|
||||||
|
print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
|
||||||
|
|
||||||
|
# ==================== 工作流方法 ====================
|
||||||
|
|
||||||
# 工作流方法
|
|
||||||
def reactor_taken_out(self):
|
def reactor_taken_out(self):
|
||||||
"""反应器取出"""
|
"""反应器取出"""
|
||||||
self.hardware_interface.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_out"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_out"}')
|
||||||
reactor_taken_out_params = {"param_values": {}}
|
reactor_taken_out_params = {"param_values": {}}
|
||||||
self.hardware_interface.pending_task_params.append(reactor_taken_out_params)
|
self.pending_task_params.append(reactor_taken_out_params)
|
||||||
print(f"成功添加反应器取出工作流")
|
print(f"成功添加反应器取出工作流")
|
||||||
print(f"当前队列长度: {len(self.hardware_interface.pending_task_params)}")
|
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def reactor_taken_in(self, assign_material_name: str, cutoff: str = "900000", temperature: float = -10.00):
|
def reactor_taken_in(
|
||||||
"""反应器放入"""
|
self,
|
||||||
|
assign_material_name: str,
|
||||||
|
cutoff: str = "900000",
|
||||||
|
temperature: float = -10.00
|
||||||
|
):
|
||||||
|
"""反应器放入
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assign_material_name: 物料名称
|
||||||
|
cutoff: 截止参数
|
||||||
|
temperature: 温度
|
||||||
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}')
|
||||||
material_id = self._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
|
|
||||||
if isinstance(temperature, str):
|
if isinstance(temperature, str):
|
||||||
temperature = float(temperature)
|
temperature = float(temperature)
|
||||||
@@ -45,11 +82,25 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def solid_feeding_vials(self, material_id: str, time: str = "0", torque_variation: str = "1",
|
def solid_feeding_vials(
|
||||||
assign_material_name: str = None, temperature: float = 25.00):
|
self,
|
||||||
"""固体进料小瓶"""
|
material_id: str,
|
||||||
|
time: str = "0",
|
||||||
|
torque_variation: str = "1",
|
||||||
|
assign_material_name: str = None,
|
||||||
|
temperature: float = 25.00
|
||||||
|
):
|
||||||
|
"""固体进料小瓶
|
||||||
|
|
||||||
|
Args:
|
||||||
|
material_id: 物料ID
|
||||||
|
time: 时间
|
||||||
|
torque_variation: 扭矩变化
|
||||||
|
assign_material_name: 物料名称
|
||||||
|
temperature: 温度
|
||||||
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}')
|
||||||
material_id_m = self._get_material_id_by_name(assign_material_name)
|
material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
|
|
||||||
if isinstance(temperature, str):
|
if isinstance(temperature, str):
|
||||||
temperature = float(temperature)
|
temperature = float(temperature)
|
||||||
@@ -76,12 +127,27 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def liquid_feeding_vials_non_titration(self, volumeFormula: str, assign_material_name: str,
|
def liquid_feeding_vials_non_titration(
|
||||||
titration_type: str = "1", time: str = "0",
|
self,
|
||||||
torque_variation: str = "1", temperature: float = 25.00):
|
volumeFormula: str,
|
||||||
"""液体进料小瓶(非滴定)"""
|
assign_material_name: str,
|
||||||
|
titration_type: str = "1",
|
||||||
|
time: str = "0",
|
||||||
|
torque_variation: str = "1",
|
||||||
|
temperature: float = 25.00
|
||||||
|
):
|
||||||
|
"""液体进料小瓶(非滴定)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
volumeFormula: 体积公式
|
||||||
|
assign_material_name: 物料名称
|
||||||
|
titration_type: 滴定类型
|
||||||
|
time: 时间
|
||||||
|
torque_variation: 扭矩变化
|
||||||
|
temperature: 温度
|
||||||
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}')
|
||||||
material_id = self._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
|
|
||||||
if isinstance(temperature, str):
|
if isinstance(temperature, str):
|
||||||
temperature = float(temperature)
|
temperature = float(temperature)
|
||||||
@@ -109,11 +175,27 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def liquid_feeding_solvents(self, assign_material_name: str, volume: str, titration_type: str = "1",
|
def liquid_feeding_solvents(
|
||||||
time: str = "360", torque_variation: str = "2", temperature: float = 25.00):
|
self,
|
||||||
"""液体进料溶剂"""
|
assign_material_name: str,
|
||||||
|
volume: str,
|
||||||
|
titration_type: str = "1",
|
||||||
|
time: str = "360",
|
||||||
|
torque_variation: str = "2",
|
||||||
|
temperature: float = 25.00
|
||||||
|
):
|
||||||
|
"""液体进料溶剂
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assign_material_name: 物料名称
|
||||||
|
volume: 体积
|
||||||
|
titration_type: 滴定类型
|
||||||
|
time: 时间
|
||||||
|
torque_variation: 扭矩变化
|
||||||
|
temperature: 温度
|
||||||
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}')
|
||||||
material_id = self._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
|
|
||||||
if isinstance(temperature, str):
|
if isinstance(temperature, str):
|
||||||
temperature = float(temperature)
|
temperature = float(temperature)
|
||||||
@@ -141,11 +223,27 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def liquid_feeding_titration(self, volume_formula: str, assign_material_name: str, titration_type: str = "1",
|
def liquid_feeding_titration(
|
||||||
time: str = "90", torque_variation: int = 2, temperature: float = 25.00):
|
self,
|
||||||
"""液体进料(滴定)"""
|
volume_formula: str,
|
||||||
|
assign_material_name: str,
|
||||||
|
titration_type: str = "1",
|
||||||
|
time: str = "90",
|
||||||
|
torque_variation: int = 2,
|
||||||
|
temperature: float = 25.00
|
||||||
|
):
|
||||||
|
"""液体进料(滴定)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
volume_formula: 体积公式
|
||||||
|
assign_material_name: 物料名称
|
||||||
|
titration_type: 滴定类型
|
||||||
|
time: 时间
|
||||||
|
torque_variation: 扭矩变化
|
||||||
|
temperature: 温度
|
||||||
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
||||||
material_id = self._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
|
|
||||||
if isinstance(temperature, str):
|
if isinstance(temperature, str):
|
||||||
temperature = float(temperature)
|
temperature = float(temperature)
|
||||||
@@ -173,12 +271,27 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def liquid_feeding_beaker(self, volume: str = "35000", assign_material_name: str = "BAPP",
|
def liquid_feeding_beaker(
|
||||||
time: str = "0", torque_variation: str = "1", titrationType: str = "1",
|
self,
|
||||||
temperature: float = 25.00):
|
volume: str = "35000",
|
||||||
"""液体进料烧杯"""
|
assign_material_name: str = "BAPP",
|
||||||
|
time: str = "0",
|
||||||
|
torque_variation: str = "1",
|
||||||
|
titrationType: str = "1",
|
||||||
|
temperature: float = 25.00
|
||||||
|
):
|
||||||
|
"""液体进料烧杯
|
||||||
|
|
||||||
|
Args:
|
||||||
|
volume: 体积
|
||||||
|
assign_material_name: 物料名称
|
||||||
|
time: 时间
|
||||||
|
torque_variation: 扭矩变化
|
||||||
|
titrationType: 滴定类型
|
||||||
|
temperature: 温度
|
||||||
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}')
|
||||||
material_id = self._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
|
|
||||||
if isinstance(temperature, str):
|
if isinstance(temperature, str):
|
||||||
temperature = float(temperature)
|
temperature = float(temperature)
|
||||||
@@ -205,3 +318,322 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}")
|
print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}")
|
||||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
|
# ==================== 工作流管理方法 ====================
|
||||||
|
|
||||||
|
def get_workflow_sequence(self) -> List[str]:
|
||||||
|
"""获取当前工作流执行顺序
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
工作流名称列表
|
||||||
|
"""
|
||||||
|
id_to_name = {workflow_id: name for name, workflow_id in self.workflow_mappings.items()}
|
||||||
|
workflow_names = []
|
||||||
|
for workflow_id in self.workflow_sequence:
|
||||||
|
workflow_names.append(id_to_name.get(workflow_id, workflow_id))
|
||||||
|
return workflow_names
|
||||||
|
|
||||||
|
def workflow_step_query(self, workflow_id: str) -> dict:
|
||||||
|
"""查询工作流步骤参数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workflow_id: 工作流ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
工作流步骤参数字典
|
||||||
|
"""
|
||||||
|
return self.hardware_interface.workflow_step_query(workflow_id)
|
||||||
|
|
||||||
|
def create_order(self, json_str: str) -> dict:
|
||||||
|
"""创建订单
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_str: 订单参数的JSON字符串
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
创建结果
|
||||||
|
"""
|
||||||
|
return self.hardware_interface.create_order(json_str)
|
||||||
|
|
||||||
|
# ==================== 工作流执行核心方法 ====================
|
||||||
|
|
||||||
|
# 发布任务
|
||||||
|
def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict:
|
||||||
|
web_workflow_list = self.get_workflow_sequence()
|
||||||
|
workflow_name = workflow_name
|
||||||
|
|
||||||
|
pending_params_backup = self.pending_task_params.copy()
|
||||||
|
print(f"保存pending_task_params副本,共{len(pending_params_backup)}个参数")
|
||||||
|
|
||||||
|
# 1. 处理网页工作流列表
|
||||||
|
print(f"处理网页工作流列表: {web_workflow_list}")
|
||||||
|
web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list})
|
||||||
|
workflows_result = self.process_web_workflows(web_workflow_json)
|
||||||
|
|
||||||
|
if not workflows_result:
|
||||||
|
error_msg = "处理网页工作流列表失败"
|
||||||
|
print(error_msg)
|
||||||
|
result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "process_web_workflows"})
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 2. 合并工作流序列
|
||||||
|
print(f"合并工作流序列,名称: {workflow_name}")
|
||||||
|
merge_json = json.dumps({"name": workflow_name})
|
||||||
|
merged_workflow = self.merge_sequence_workflow(merge_json)
|
||||||
|
print(f"合并工作流序列结果: {merged_workflow}")
|
||||||
|
|
||||||
|
if not merged_workflow:
|
||||||
|
error_msg = "合并工作流序列失败"
|
||||||
|
print(error_msg)
|
||||||
|
result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "merge_sequence_workflow"})
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 3. 合并所有参数并创建任务
|
||||||
|
# 新API只返回状态信息,需要适配处理
|
||||||
|
if isinstance(merged_workflow, dict) and merged_workflow.get("code") == 1:
|
||||||
|
# 新API返回格式:{code: 1, message: "", timestamp: 0}
|
||||||
|
# 使用传入的工作流名称和生成的临时ID
|
||||||
|
final_workflow_name = workflow_name
|
||||||
|
workflow_id = f"merged_{workflow_name}_{self.hardware_interface.get_current_time_iso8601().replace('-', '').replace('T', '').replace(':', '').replace('.', '')[:14]}"
|
||||||
|
print(f"新API合并成功,使用工作流创建任务: {final_workflow_name} (临时ID: {workflow_id})")
|
||||||
|
else:
|
||||||
|
# 旧API返回格式:包含详细工作流信息
|
||||||
|
final_workflow_name = merged_workflow.get("name", workflow_name)
|
||||||
|
workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "")
|
||||||
|
print(f"旧API格式,使用工作流创建任务: {final_workflow_name} (ID: {workflow_id})")
|
||||||
|
|
||||||
|
if not workflow_id:
|
||||||
|
error_msg = "无法获取工作流ID"
|
||||||
|
print(error_msg)
|
||||||
|
result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "get_workflow_id"})
|
||||||
|
return result
|
||||||
|
|
||||||
|
workflow_query_json = json.dumps({"workflow_id": workflow_id})
|
||||||
|
workflow_params_structure = self.workflow_step_query(workflow_query_json)
|
||||||
|
|
||||||
|
self.pending_task_params = pending_params_backup
|
||||||
|
print(f"恢复pending_task_params,共{len(self.pending_task_params)}个参数")
|
||||||
|
|
||||||
|
param_values = self.generate_task_param_values(workflow_params_structure)
|
||||||
|
|
||||||
|
task_params = [{
|
||||||
|
"orderCode": f"BSO{self.hardware_interface.get_current_time_iso8601().replace('-', '').replace('T', '').replace(':', '').replace('.', '')[:14]}",
|
||||||
|
"orderName": f"实验-{self.hardware_interface.get_current_time_iso8601()[:10].replace('-', '')}",
|
||||||
|
"workFlowId": workflow_id,
|
||||||
|
"borderNumber": 1,
|
||||||
|
"paramValues": param_values,
|
||||||
|
"extendProperties": ""
|
||||||
|
}]
|
||||||
|
|
||||||
|
task_json = json.dumps(task_params)
|
||||||
|
print(f"创建任务参数: {type(task_json)}")
|
||||||
|
result = self.create_order(task_json)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
error_msg = "创建任务失败"
|
||||||
|
print(error_msg)
|
||||||
|
result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "create_order"})
|
||||||
|
return result
|
||||||
|
|
||||||
|
print(f"任务创建成功: {result}")
|
||||||
|
self.pending_task_params.clear()
|
||||||
|
print("已清空pending_task_params")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"workflow": {"name": final_workflow_name, "id": workflow_id},
|
||||||
|
"task": result,
|
||||||
|
"method": "process_and_execute_workflow"
|
||||||
|
}
|
||||||
|
|
||||||
|
def merge_sequence_workflow(self, json_str: str) -> dict:
|
||||||
|
"""合并当前工作流序列
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_str: 包含name等参数的JSON字符串
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
合并结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = json.loads(json_str)
|
||||||
|
name = data.get("name", "合并工作流")
|
||||||
|
step_parameters = data.get("stepParameters", {})
|
||||||
|
variables = data.get("variables", {})
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if not self.workflow_sequence:
|
||||||
|
print("工作流序列为空,无法合并")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# 将工作流ID列表转换为新API要求的格式
|
||||||
|
workflows = [{"id": workflow_id} for workflow_id in self.workflow_sequence]
|
||||||
|
|
||||||
|
# 构建新的API参数格式
|
||||||
|
params = {
|
||||||
|
"name": name,
|
||||||
|
"workflows": workflows,
|
||||||
|
"stepParameters": step_parameters,
|
||||||
|
"variables": variables
|
||||||
|
}
|
||||||
|
|
||||||
|
# 使用新的API接口
|
||||||
|
response = self.hardware_interface.post(
|
||||||
|
url=f'{self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters',
|
||||||
|
params={
|
||||||
|
"apiKey": self.hardware_interface.api_key,
|
||||||
|
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
||||||
|
"data": params,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return {}
|
||||||
|
return response.get("data", {})
|
||||||
|
|
||||||
|
def generate_task_param_values(self, workflow_params_structure: dict) -> dict:
|
||||||
|
"""生成任务参数值
|
||||||
|
|
||||||
|
根据工作流参数结构和待处理的任务参数,生成最终的任务参数值
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workflow_params_structure: 工作流参数结构
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
任务参数值字典
|
||||||
|
"""
|
||||||
|
if not workflow_params_structure:
|
||||||
|
print("workflow_params_structure为空")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
data = workflow_params_structure
|
||||||
|
|
||||||
|
# 从pending_task_params中提取实际参数值,按DisplaySectionName和Key组织
|
||||||
|
pending_params_by_section = {}
|
||||||
|
print(f"开始处理pending_task_params,共{len(self.pending_task_params)}个任务参数组")
|
||||||
|
|
||||||
|
# 获取工作流执行顺序,用于按顺序匹配参数
|
||||||
|
workflow_sequence = self.get_workflow_sequence()
|
||||||
|
print(f"工作流执行顺序: {workflow_sequence}")
|
||||||
|
|
||||||
|
workflow_index = 0
|
||||||
|
|
||||||
|
# 遍历所有待处理的任务参数
|
||||||
|
for i, task_param in enumerate(self.pending_task_params):
|
||||||
|
if 'param_values' in task_param:
|
||||||
|
print(f"处理第{i+1}个任务参数组,包含{len(task_param['param_values'])}个步骤")
|
||||||
|
|
||||||
|
if workflow_index < len(workflow_sequence):
|
||||||
|
current_workflow = workflow_sequence[workflow_index]
|
||||||
|
section_name = WORKFLOW_TO_SECTION_MAP.get(current_workflow)
|
||||||
|
print(f" 匹配到工作流: {current_workflow} -> {section_name}")
|
||||||
|
workflow_index += 1
|
||||||
|
else:
|
||||||
|
print(f" 警告: 参数组{i+1}超出了工作流序列范围")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not section_name:
|
||||||
|
print(f" 警告: 工作流{current_workflow}没有对应的DisplaySectionName")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if section_name not in pending_params_by_section:
|
||||||
|
pending_params_by_section[section_name] = {}
|
||||||
|
|
||||||
|
# 处理每个步骤的参数
|
||||||
|
for step_id, param_list in task_param['param_values'].items():
|
||||||
|
print(f" 步骤ID: {step_id},参数数量: {len(param_list)}")
|
||||||
|
|
||||||
|
for param_item in param_list:
|
||||||
|
key = param_item.get('Key', '')
|
||||||
|
value = param_item.get('Value', '')
|
||||||
|
m = param_item.get('m', 0)
|
||||||
|
n = param_item.get('n', 0)
|
||||||
|
print(f" 参数: {key} = {value} (m={m}, n={n}) -> 分组到{section_name}")
|
||||||
|
|
||||||
|
param_key = f"{section_name}.{key}"
|
||||||
|
if param_key not in pending_params_by_section[section_name]:
|
||||||
|
pending_params_by_section[section_name][param_key] = []
|
||||||
|
|
||||||
|
pending_params_by_section[section_name][param_key].append({
|
||||||
|
'value': value,
|
||||||
|
'm': m,
|
||||||
|
'n': n
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f"pending_params_by_section构建完成,包含{len(pending_params_by_section)}个分组")
|
||||||
|
|
||||||
|
# 收集所有参数,过滤TaskDisplayable为0的项
|
||||||
|
filtered_params = []
|
||||||
|
|
||||||
|
for step_id, step_info in data.items():
|
||||||
|
if isinstance(step_info, list):
|
||||||
|
for step_item in step_info:
|
||||||
|
param_list = step_item.get("parameterList", [])
|
||||||
|
for param in param_list:
|
||||||
|
if param.get("TaskDisplayable") == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
param_with_step = param.copy()
|
||||||
|
param_with_step['step_id'] = step_id
|
||||||
|
param_with_step['step_name'] = step_item.get("name", "")
|
||||||
|
param_with_step['step_m'] = step_item.get("m", 0)
|
||||||
|
param_with_step['step_n'] = step_item.get("n", 0)
|
||||||
|
filtered_params.append(param_with_step)
|
||||||
|
|
||||||
|
# 按DisplaySectionIndex排序
|
||||||
|
filtered_params.sort(key=lambda x: x.get('DisplaySectionIndex', 0))
|
||||||
|
|
||||||
|
# 生成参数映射
|
||||||
|
param_mapping = {}
|
||||||
|
step_params = {}
|
||||||
|
for param in filtered_params:
|
||||||
|
step_id = param['step_id']
|
||||||
|
if step_id not in step_params:
|
||||||
|
step_params[step_id] = []
|
||||||
|
step_params[step_id].append(param)
|
||||||
|
|
||||||
|
# 为每个步骤生成参数
|
||||||
|
for step_id, params in step_params.items():
|
||||||
|
param_list = []
|
||||||
|
for param in params:
|
||||||
|
key = param.get('Key', '')
|
||||||
|
display_section_index = param.get('DisplaySectionIndex', 0)
|
||||||
|
step_m = param.get('step_m', 0)
|
||||||
|
step_n = param.get('step_n', 0)
|
||||||
|
|
||||||
|
section_name = param.get('DisplaySectionName', '')
|
||||||
|
param_key = f"{section_name}.{key}"
|
||||||
|
|
||||||
|
if section_name in pending_params_by_section and param_key in pending_params_by_section[section_name]:
|
||||||
|
pending_param_list = pending_params_by_section[section_name][param_key]
|
||||||
|
if pending_param_list:
|
||||||
|
pending_param = pending_param_list[0]
|
||||||
|
value = pending_param['value']
|
||||||
|
m = step_m
|
||||||
|
n = step_n
|
||||||
|
print(f" 匹配成功: {section_name}.{key} = {value} (m={m}, n={n})")
|
||||||
|
pending_param_list.pop(0)
|
||||||
|
else:
|
||||||
|
value = "1"
|
||||||
|
m = step_m
|
||||||
|
n = step_n
|
||||||
|
print(f" 匹配失败: {section_name}.{key},参数列表为空,使用默认值 = {value}")
|
||||||
|
else:
|
||||||
|
value = "1"
|
||||||
|
m = display_section_index
|
||||||
|
n = step_n
|
||||||
|
print(f" 匹配失败: {section_name}.{key},使用默认值 = {value} (m={m}, n={n})")
|
||||||
|
|
||||||
|
param_item = {
|
||||||
|
"m": m,
|
||||||
|
"n": n,
|
||||||
|
"key": key,
|
||||||
|
"value": str(value).strip()
|
||||||
|
}
|
||||||
|
param_list.append(param_item)
|
||||||
|
|
||||||
|
if param_list:
|
||||||
|
param_mapping[step_id] = param_list
|
||||||
|
|
||||||
|
print(f"生成任务参数值,包含 {len(param_mapping)} 个步骤")
|
||||||
|
return param_mapping
|
||||||
@@ -129,7 +129,6 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
self,
|
self,
|
||||||
bioyond_config: Optional[Dict[str, Any]] = None,
|
bioyond_config: Optional[Dict[str, Any]] = None,
|
||||||
deck: Optional[Any] = None,
|
deck: Optional[Any] = None,
|
||||||
station_config: Optional[Dict[str, Any]] = None,
|
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -152,9 +151,6 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
if isinstance(resource, WareHouse):
|
if isinstance(resource, WareHouse):
|
||||||
self.deck.warehouses[resource.name] = resource
|
self.deck.warehouses[resource.name] = resource
|
||||||
|
|
||||||
# 配置站点类型
|
|
||||||
self._configure_station_type(station_config)
|
|
||||||
|
|
||||||
# 创建通信模块
|
# 创建通信模块
|
||||||
self._create_communication_module(bioyond_config)
|
self._create_communication_module(bioyond_config)
|
||||||
self.resource_synchronizer = BioyondResourceSynchronizer(self)
|
self.resource_synchronizer = BioyondResourceSynchronizer(self)
|
||||||
@@ -167,8 +163,6 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
self.workflow_mappings = {}
|
self.workflow_mappings = {}
|
||||||
self.workflow_sequence = []
|
self.workflow_sequence = []
|
||||||
self.pending_task_params = []
|
self.pending_task_params = []
|
||||||
self.material_cache = {}
|
|
||||||
self._load_material_cache()
|
|
||||||
|
|
||||||
if "workflow_mappings" in bioyond_config:
|
if "workflow_mappings" in bioyond_config:
|
||||||
self._set_workflow_mappings(bioyond_config["workflow_mappings"])
|
self._set_workflow_mappings(bioyond_config["workflow_mappings"])
|
||||||
@@ -325,10 +319,22 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def append_to_workflow_sequence(self, web_workflow_name: str) -> bool:
|
def append_to_workflow_sequence(self, web_workflow_name: str) -> bool:
|
||||||
workflow_id = self._get_workflow(web_workflow_name)
|
# 检查是否为JSON格式的字符串
|
||||||
|
actual_workflow_name = web_workflow_name
|
||||||
|
if web_workflow_name.startswith('{') and web_workflow_name.endswith('}'):
|
||||||
|
try:
|
||||||
|
data = json.loads(web_workflow_name)
|
||||||
|
actual_workflow_name = data.get("web_workflow_name", web_workflow_name)
|
||||||
|
print(f"解析JSON格式工作流名称: {web_workflow_name} -> {actual_workflow_name}")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"JSON解析失败,使用原始字符串: {web_workflow_name}")
|
||||||
|
|
||||||
|
workflow_id = self._get_workflow(actual_workflow_name)
|
||||||
if workflow_id:
|
if workflow_id:
|
||||||
self.workflow_sequence.append(workflow_id)
|
self.workflow_sequence.append(workflow_id)
|
||||||
print(f"添加工作流到执行顺序: {web_workflow_name} -> {workflow_id}")
|
print(f"添加工作流到执行顺序: {actual_workflow_name} -> {workflow_id}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def set_workflow_sequence(self, json_str: str) -> List[str]:
|
def set_workflow_sequence(self, json_str: str) -> List[str]:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ class WorkstationBase(ABC):
|
|||||||
def post_init(self, ros_node: ROS2WorkstationNode) -> None:
|
def post_init(self, ros_node: ROS2WorkstationNode) -> None:
|
||||||
# 初始化物料系统
|
# 初始化物料系统
|
||||||
self._ros_node = ros_node
|
self._ros_node = ros_node
|
||||||
self._ros_node.update_resource([self.deck])
|
|
||||||
|
|
||||||
def _build_resource_mappings(self, deck: Deck):
|
def _build_resource_mappings(self, deck: Deck):
|
||||||
"""递归构建资源映射"""
|
"""递归构建资源映射"""
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ container:
|
|||||||
- container
|
- container
|
||||||
class:
|
class:
|
||||||
module: unilabos.resources.container:RegularContainer
|
module: unilabos.resources.container:RegularContainer
|
||||||
type: unilabos
|
type: pylabrobot
|
||||||
description: regular organic container
|
description: regular organic container
|
||||||
handles:
|
handles:
|
||||||
- data_key: fluid_in
|
- data_key: fluid_in
|
||||||
|
|||||||
@@ -1,67 +1,84 @@
|
|||||||
import json
|
import json
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from pylabrobot.resources import Container
|
||||||
from unilabos_msgs.msg import Resource
|
from unilabos_msgs.msg import Resource
|
||||||
|
|
||||||
from unilabos.ros.msgs.message_converter import convert_from_ros_msg
|
from unilabos.ros.msgs.message_converter import convert_from_ros_msg
|
||||||
|
|
||||||
|
|
||||||
class RegularContainer(object):
|
class RegularContainer(Container):
|
||||||
# 第一个参数必须是id传入
|
def __init__(self, *args, **kwargs):
|
||||||
# noinspection PyShadowingBuiltins
|
if "size_x" not in kwargs:
|
||||||
def __init__(self, id: str):
|
kwargs["size_x"] = 0
|
||||||
self.id = id
|
if "size_y" not in kwargs:
|
||||||
self.ulr_resource = Resource()
|
kwargs["size_y"] = 0
|
||||||
self._data = None
|
if "size_z" not in kwargs:
|
||||||
|
kwargs["size_z"] = 0
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self.state = {}
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
def load_state(self, state: Dict[str, Any]):
|
||||||
def ulr_resource_data(self):
|
self.state = state
|
||||||
if self._data is None:
|
#
|
||||||
self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {}
|
# class RegularContainer(object):
|
||||||
return self._data
|
# # 第一个参数必须是id传入
|
||||||
|
# # noinspection PyShadowingBuiltins
|
||||||
@ulr_resource_data.setter
|
# def __init__(self, id: str):
|
||||||
def ulr_resource_data(self, value: dict):
|
# self.id = id
|
||||||
self._data = value
|
# self.ulr_resource = Resource()
|
||||||
self.ulr_resource.data = json.dumps(self._data)
|
# self._data = None
|
||||||
|
#
|
||||||
@property
|
# @property
|
||||||
def liquid_type(self):
|
# def ulr_resource_data(self):
|
||||||
return self.ulr_resource_data.get("liquid_type", None)
|
# if self._data is None:
|
||||||
|
# self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {}
|
||||||
@liquid_type.setter
|
# return self._data
|
||||||
def liquid_type(self, value: str):
|
#
|
||||||
if value is not None:
|
# @ulr_resource_data.setter
|
||||||
self.ulr_resource_data["liquid_type"] = value
|
# def ulr_resource_data(self, value: dict):
|
||||||
else:
|
# self._data = value
|
||||||
self.ulr_resource_data.pop("liquid_type", None)
|
# self.ulr_resource.data = json.dumps(self._data)
|
||||||
|
#
|
||||||
@property
|
# @property
|
||||||
def liquid_volume(self):
|
# def liquid_type(self):
|
||||||
return self.ulr_resource_data.get("liquid_volume", None)
|
# return self.ulr_resource_data.get("liquid_type", None)
|
||||||
|
#
|
||||||
@liquid_volume.setter
|
# @liquid_type.setter
|
||||||
def liquid_volume(self, value: float):
|
# def liquid_type(self, value: str):
|
||||||
if value is not None:
|
# if value is not None:
|
||||||
self.ulr_resource_data["liquid_volume"] = value
|
# self.ulr_resource_data["liquid_type"] = value
|
||||||
else:
|
# else:
|
||||||
self.ulr_resource_data.pop("liquid_volume", None)
|
# self.ulr_resource_data.pop("liquid_type", None)
|
||||||
|
#
|
||||||
def get_ulr_resource(self) -> Resource:
|
# @property
|
||||||
"""
|
# def liquid_volume(self):
|
||||||
获取UlrResource对象
|
# return self.ulr_resource_data.get("liquid_volume", None)
|
||||||
:return: UlrResource对象
|
#
|
||||||
"""
|
# @liquid_volume.setter
|
||||||
self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新
|
# def liquid_volume(self, value: float):
|
||||||
return self.ulr_resource
|
# if value is not None:
|
||||||
|
# self.ulr_resource_data["liquid_volume"] = value
|
||||||
def get_ulr_resource_as_dict(self) -> Resource:
|
# else:
|
||||||
"""
|
# self.ulr_resource_data.pop("liquid_volume", None)
|
||||||
获取UlrResource对象
|
#
|
||||||
:return: UlrResource对象
|
# def get_ulr_resource(self) -> Resource:
|
||||||
"""
|
# """
|
||||||
to_dict = convert_from_ros_msg(self.get_ulr_resource())
|
# 获取UlrResource对象
|
||||||
to_dict["type"] = "container"
|
# :return: UlrResource对象
|
||||||
return to_dict
|
# """
|
||||||
|
# self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新
|
||||||
def __str__(self):
|
# return self.ulr_resource
|
||||||
return f"{self.id}"
|
#
|
||||||
|
# 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,7 @@ import json
|
|||||||
import os.path
|
import os.path
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Union, Any, Dict, List, Tuple
|
from typing import Union, Any, Dict, List, Tuple
|
||||||
|
import uuid
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
from pylabrobot.resources import ResourceHolder
|
from pylabrobot.resources import ResourceHolder
|
||||||
from unilabos_msgs.msg import Resource
|
from unilabos_msgs.msg import Resource
|
||||||
@@ -16,6 +17,7 @@ from unilabos.ros.nodes.resource_tracker import (
|
|||||||
ResourceDictInstance,
|
ResourceDictInstance,
|
||||||
ResourceTreeSet,
|
ResourceTreeSet,
|
||||||
)
|
)
|
||||||
|
from unilabos.utils import logger
|
||||||
from unilabos.utils.banner_print import print_status
|
from unilabos.utils.banner_print import print_status
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -52,7 +54,7 @@ def canonicalize_nodes_data(
|
|||||||
if not node.get("type"):
|
if not node.get("type"):
|
||||||
node["type"] = "device"
|
node["type"] = "device"
|
||||||
print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'type', defaulting to 'device'", "warning")
|
print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'type', defaulting to 'device'", "warning")
|
||||||
if not node.get("name"):
|
if node.get("name", None) is None:
|
||||||
node["name"] = node.get("id")
|
node["name"] = node.get("id")
|
||||||
print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'name', defaulting to {node['name']}", "warning")
|
print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'name', defaulting to {node['name']}", "warning")
|
||||||
if not isinstance(node.get("position"), dict):
|
if not isinstance(node.get("position"), dict):
|
||||||
@@ -66,8 +68,12 @@ def canonicalize_nodes_data(
|
|||||||
z = node.pop("z", None)
|
z = node.pop("z", None)
|
||||||
if z is not None:
|
if z is not None:
|
||||||
node["position"]["position"]["z"] = z
|
node["position"]["position"]["z"] = z
|
||||||
|
if "sample_id" in node:
|
||||||
|
sample_id = node.pop("sample_id")
|
||||||
|
if sample_id:
|
||||||
|
logger.error(f"{node}的sample_id参数已弃用,sample_id: {sample_id}")
|
||||||
for k in list(node.keys()):
|
for k in list(node.keys()):
|
||||||
if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data"]:
|
if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children"]:
|
||||||
v = node.pop(k)
|
v = node.pop(k)
|
||||||
node["config"][k] = v
|
node["config"][k] = v
|
||||||
|
|
||||||
@@ -629,6 +635,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
{"name": material["name"], "class": className}, resource_type=ResourcePLR
|
{"name": material["name"], "class": className}, resource_type=ResourcePLR
|
||||||
)
|
)
|
||||||
plr_material.code = material.get("code", "") and material.get("barCode", "") or ""
|
plr_material.code = material.get("code", "") and material.get("barCode", "") or ""
|
||||||
|
plr_material.unilabos_uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
# 处理子物料(detail)
|
# 处理子物料(detail)
|
||||||
if material.get("detail") and len(material["detail"]) > 0:
|
if material.get("detail") and len(material["detail"]) > 0:
|
||||||
@@ -774,6 +781,7 @@ def initialize_resource(resource_config: dict, resource_type: Any = None) -> Uni
|
|||||||
else:
|
else:
|
||||||
r = resource_plr
|
r = resource_plr
|
||||||
elif resource_class_config["type"] == "unilabos":
|
elif resource_class_config["type"] == "unilabos":
|
||||||
|
raise ValueError(f"No more support for unilabos Resource class {resource_class_config}")
|
||||||
res_instance: RegularContainer = RESOURCE(id=resource_config["name"])
|
res_instance: RegularContainer = RESOURCE(id=resource_config["name"])
|
||||||
res_instance.ulr_resource = convert_to_ros_msg(
|
res_instance.ulr_resource = convert_to_ros_msg(
|
||||||
Resource, {k: v for k, v in resource_config.items() if k != "class"}
|
Resource, {k: v for k, v in resource_config.items() if k != "class"}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device
|
|||||||
d = None
|
d = None
|
||||||
original_device_config = copy.deepcopy(device_config)
|
original_device_config = copy.deepcopy(device_config)
|
||||||
device_class_config = device_config["class"]
|
device_class_config = device_config["class"]
|
||||||
|
uid = device_config["uuid"]
|
||||||
if isinstance(device_class_config, str): # 如果是字符串,则直接去lab_registry中查找,获取class
|
if isinstance(device_class_config, str): # 如果是字符串,则直接去lab_registry中查找,获取class
|
||||||
if len(device_class_config) == 0:
|
if len(device_class_config) == 0:
|
||||||
raise DeviceClassInvalid(f"Device [{device_id}] class cannot be an empty string. {device_config}")
|
raise DeviceClassInvalid(f"Device [{device_id}] class cannot be an empty string. {device_config}")
|
||||||
@@ -50,7 +51,7 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
d = DEVICE(
|
d = DEVICE(
|
||||||
device_id=device_id, driver_is_ros=device_class_config["type"] == "ros2", driver_params=device_config.get("config", {})
|
device_id=device_id, device_uuid=uid, driver_is_ros=device_class_config["type"] == "ros2", driver_params=device_config.get("config", {})
|
||||||
)
|
)
|
||||||
except DeviceInitError as ex:
|
except DeviceInitError as ex:
|
||||||
return d
|
return d
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
from typing import get_type_hints, TypeVar, Generic, Dict, Any, Type, TypedDict, Optional, List, TYPE_CHECKING
|
from typing import get_type_hints, TypeVar, Generic, Dict, Any, Type, TypedDict, Optional, List, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -132,6 +132,7 @@ class ROSLoggerAdapter:
|
|||||||
def init_wrapper(
|
def init_wrapper(
|
||||||
self,
|
self,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
|
device_uuid: str,
|
||||||
driver_class: type[T],
|
driver_class: type[T],
|
||||||
device_config: Dict[str, Any],
|
device_config: Dict[str, Any],
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
@@ -150,6 +151,7 @@ def init_wrapper(
|
|||||||
if children is None:
|
if children is None:
|
||||||
children = []
|
children = []
|
||||||
kwargs["device_id"] = device_id
|
kwargs["device_id"] = device_id
|
||||||
|
kwargs["device_uuid"] = device_uuid
|
||||||
kwargs["driver_class"] = driver_class
|
kwargs["driver_class"] = driver_class
|
||||||
kwargs["device_config"] = device_config
|
kwargs["device_config"] = device_config
|
||||||
kwargs["driver_params"] = driver_params
|
kwargs["driver_params"] = driver_params
|
||||||
@@ -266,6 +268,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
self,
|
self,
|
||||||
driver_instance: T,
|
driver_instance: T,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
|
device_uuid: str,
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
action_value_mappings: Dict[str, Any],
|
action_value_mappings: Dict[str, Any],
|
||||||
hardware_interface: Dict[str, Any],
|
hardware_interface: Dict[str, Any],
|
||||||
@@ -278,6 +281,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
Args:
|
Args:
|
||||||
driver_instance: 设备实例
|
driver_instance: 设备实例
|
||||||
device_id: 设备标识符
|
device_id: 设备标识符
|
||||||
|
device_uuid: 设备标识符
|
||||||
status_types: 需要发布的状态和传感器信息
|
status_types: 需要发布的状态和传感器信息
|
||||||
action_value_mappings: 设备动作
|
action_value_mappings: 设备动作
|
||||||
hardware_interface: 硬件接口配置
|
hardware_interface: 硬件接口配置
|
||||||
@@ -285,7 +289,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
"""
|
"""
|
||||||
self.driver_instance = driver_instance
|
self.driver_instance = driver_instance
|
||||||
self.device_id = device_id
|
self.device_id = device_id
|
||||||
self.uuid = str(uuid.uuid4())
|
self.uuid = device_uuid
|
||||||
self.publish_high_frequency = False
|
self.publish_high_frequency = False
|
||||||
self.callback_group = ReentrantCallbackGroup()
|
self.callback_group = ReentrantCallbackGroup()
|
||||||
self.resource_tracker = resource_tracker
|
self.resource_tracker = resource_tracker
|
||||||
@@ -554,6 +558,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
async def update_resource(self, resources: List["ResourcePLR"]):
|
async def update_resource(self, resources: List["ResourcePLR"]):
|
||||||
r = SerialCommand.Request()
|
r = SerialCommand.Request()
|
||||||
tree_set = ResourceTreeSet.from_plr_resources(resources)
|
tree_set = ResourceTreeSet.from_plr_resources(resources)
|
||||||
|
for tree in tree_set.trees:
|
||||||
|
root_node = tree.root_node
|
||||||
|
if not root_node.res_content.uuid_parent:
|
||||||
|
logger.warning(f"更新无父节点物料{root_node},自动以当前设备作为根节点")
|
||||||
|
root_node.res_content.parent_uuid = self.uuid
|
||||||
r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"})
|
r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"})
|
||||||
response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore
|
response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore
|
||||||
try:
|
try:
|
||||||
@@ -648,15 +657,27 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
results.append({"success": True, "action": "update"})
|
results.append({"success": True, "action": "update"})
|
||||||
elif action == "remove":
|
elif action == "remove":
|
||||||
# 移除资源
|
# 移除资源
|
||||||
plr_resources: List[ResourcePLR] = [
|
found_resources: List[List[Union[ResourcePLR, dict]]] = self.resource_tracker.figure_resource(
|
||||||
self.resource_tracker.uuid_to_resources[i] for i in resources_uuid
|
[{"uuid": uid} for uid in resources_uuid], try_mode=True
|
||||||
]
|
)
|
||||||
|
found_plr_resources = []
|
||||||
|
other_plr_resources = []
|
||||||
|
for res_list in found_resources:
|
||||||
|
for res in res_list:
|
||||||
|
if issubclass(res.__class__, ResourcePLR):
|
||||||
|
found_plr_resources.append(res)
|
||||||
|
else:
|
||||||
|
other_plr_resources.append(res)
|
||||||
func = getattr(self.driver_instance, "resource_tree_remove", None)
|
func = getattr(self.driver_instance, "resource_tree_remove", None)
|
||||||
if callable(func):
|
if callable(func):
|
||||||
func(plr_resources)
|
func(found_plr_resources)
|
||||||
for plr_resource in plr_resources:
|
for plr_resource in found_plr_resources:
|
||||||
plr_resource.parent.unassign_child_resource(plr_resource)
|
plr_resource.parent.unassign_child_resource(plr_resource)
|
||||||
self.resource_tracker.remove_resource(plr_resource)
|
self.resource_tracker.remove_resource(plr_resource)
|
||||||
|
self.lab_logger().info(f"移除物料 {plr_resource} 及其子节点")
|
||||||
|
for res in other_plr_resources:
|
||||||
|
self.resource_tracker.remove_resource(res)
|
||||||
|
self.lab_logger().info(f"移除物料 {res} 及其子节点")
|
||||||
results.append({"success": True, "action": "remove"})
|
results.append({"success": True, "action": "remove"})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Error processing {action} operation: {str(e)}"
|
error_msg = f"Error processing {action} operation: {str(e)}"
|
||||||
@@ -936,7 +957,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
|
|
||||||
# 通过资源跟踪器获取本地实例
|
# 通过资源跟踪器获取本地实例
|
||||||
final_resources = queried_resources if is_sequence else queried_resources[0]
|
final_resources = queried_resources if is_sequence else queried_resources[0]
|
||||||
action_kwargs[k] = self.resource_tracker.figure_resource(final_resources, try_mode=False)
|
final_resources = self.resource_tracker.figure_resource({"name": final_resources.name}, try_mode=False) if not is_sequence else [
|
||||||
|
self.resource_tracker.figure_resource({"name": res.name}, try_mode=False) for res in queried_resources
|
||||||
|
]
|
||||||
|
action_kwargs[k] = final_resources
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.lab_logger().error(f"{action_name} 物料实例获取失败: {e}\n{traceback.format_exc()}")
|
self.lab_logger().error(f"{action_name} 物料实例获取失败: {e}\n{traceback.format_exc()}")
|
||||||
@@ -1347,6 +1371,7 @@ class ROS2DeviceNode:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
|
device_uuid: str,
|
||||||
driver_class: Type[T],
|
driver_class: Type[T],
|
||||||
device_config: Dict[str, Any],
|
device_config: Dict[str, Any],
|
||||||
driver_params: Dict[str, Any],
|
driver_params: Dict[str, Any],
|
||||||
@@ -1362,6 +1387,7 @@ class ROS2DeviceNode:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
device_id: 设备标识符
|
device_id: 设备标识符
|
||||||
|
device_uuid: 设备uuid
|
||||||
driver_class: 设备类
|
driver_class: 设备类
|
||||||
device_config: 原始初始化的json
|
device_config: 原始初始化的json
|
||||||
driver_params: driver初始化的参数
|
driver_params: driver初始化的参数
|
||||||
@@ -1436,6 +1462,7 @@ class ROS2DeviceNode:
|
|||||||
children=children,
|
children=children,
|
||||||
driver_instance=self._driver_instance, # type: ignore
|
driver_instance=self._driver_instance, # type: ignore
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings=action_value_mappings,
|
action_value_mappings=action_value_mappings,
|
||||||
hardware_interface=hardware_interface,
|
hardware_interface=hardware_interface,
|
||||||
@@ -1446,6 +1473,7 @@ class ROS2DeviceNode:
|
|||||||
self._ros_node = BaseROS2DeviceNode(
|
self._ros_node = BaseROS2DeviceNode(
|
||||||
driver_instance=self._driver_instance,
|
driver_instance=self._driver_instance,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings=action_value_mappings,
|
action_value_mappings=action_value_mappings,
|
||||||
hardware_interface=hardware_interface,
|
hardware_interface=hardware_interface,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from unilabos_msgs.srv import (
|
|||||||
ResourceDelete,
|
ResourceDelete,
|
||||||
ResourceUpdate,
|
ResourceUpdate,
|
||||||
ResourceList,
|
ResourceList,
|
||||||
SerialCommand,
|
SerialCommand, ResourceGet,
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||||
from unique_identifier_msgs.msg import UUID
|
from unique_identifier_msgs.msg import UUID
|
||||||
@@ -41,6 +41,7 @@ from unilabos.ros.nodes.resource_tracker import (
|
|||||||
ResourceTreeSet,
|
ResourceTreeSet,
|
||||||
ResourceTreeInstance,
|
ResourceTreeInstance,
|
||||||
)
|
)
|
||||||
|
from unilabos.utils import logger
|
||||||
from unilabos.utils.exception import DeviceClassInvalid
|
from unilabos.utils.exception import DeviceClassInvalid
|
||||||
from unilabos.utils.type_check import serialize_result_info
|
from unilabos.utils.type_check import serialize_result_info
|
||||||
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
||||||
@@ -99,17 +100,6 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
"""
|
"""
|
||||||
if self._instance is not None:
|
if self._instance is not None:
|
||||||
self._instance.lab_logger().critical("[Host Node] HostNode instance already exists.")
|
self._instance.lab_logger().critical("[Host Node] HostNode instance already exists.")
|
||||||
# 初始化Node基类,传递空参数覆盖列表
|
|
||||||
BaseROS2DeviceNode.__init__(
|
|
||||||
self,
|
|
||||||
driver_instance=self,
|
|
||||||
device_id=device_id,
|
|
||||||
status_types={},
|
|
||||||
action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
|
|
||||||
hardware_interface={},
|
|
||||||
print_publish=False,
|
|
||||||
resource_tracker=self._resource_tracker, # host node并不是通过initialize 包一层传进来的
|
|
||||||
)
|
|
||||||
|
|
||||||
# 设置单例实例
|
# 设置单例实例
|
||||||
self.__class__._instance = self
|
self.__class__._instance = self
|
||||||
@@ -127,6 +117,91 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
bridges = []
|
bridges = []
|
||||||
self.bridges = bridges
|
self.bridges = bridges
|
||||||
|
|
||||||
|
# 创建 host_node 作为一个单独的 ResourceTree
|
||||||
|
host_node_dict = {
|
||||||
|
"id": "host_node",
|
||||||
|
"uuid": str(uuid.uuid4()),
|
||||||
|
"parent_uuid": "",
|
||||||
|
"name": "host_node",
|
||||||
|
"type": "device",
|
||||||
|
"class": "host_node",
|
||||||
|
"config": {},
|
||||||
|
"data": {},
|
||||||
|
"children": [],
|
||||||
|
"description": "",
|
||||||
|
"schema": {},
|
||||||
|
"model": {},
|
||||||
|
"icon": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建 host_node 的 ResourceTree
|
||||||
|
host_node_instance = ResourceDictInstance.get_resource_instance_from_dict(host_node_dict)
|
||||||
|
host_node_tree = ResourceTreeInstance(host_node_instance)
|
||||||
|
resources_config.trees.insert(0, host_node_tree)
|
||||||
|
try:
|
||||||
|
for bridge in self.bridges:
|
||||||
|
if hasattr(bridge, "resource_tree_add") and resources_config:
|
||||||
|
from unilabos.app.web.client import HTTPClient
|
||||||
|
|
||||||
|
client: HTTPClient = bridge
|
||||||
|
resource_start_time = time.time()
|
||||||
|
# 传递 ResourceTreeSet 对象,在 client 中转换为字典并获取 UUID 映射
|
||||||
|
uuid_mapping = client.resource_tree_add(resources_config, "", True)
|
||||||
|
device_uuid = resources_config.root_nodes[0].res_content.uuid
|
||||||
|
resource_end_time = time.time()
|
||||||
|
logger.info(
|
||||||
|
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
|
||||||
|
)
|
||||||
|
for edge in self.resources_edge_config:
|
||||||
|
edge["source_uuid"] = uuid_mapping.get(edge["source_uuid"], edge["source_uuid"])
|
||||||
|
edge["target_uuid"] = uuid_mapping.get(edge["target_uuid"], edge["target_uuid"])
|
||||||
|
resource_add_res = client.resource_edge_add(self.resources_edge_config)
|
||||||
|
resource_edge_end_time = time.time()
|
||||||
|
logger.info(
|
||||||
|
f"[Host Node-Resource] 物料关系上传 {round(resource_edge_end_time - resource_end_time, 5) * 1000} ms"
|
||||||
|
)
|
||||||
|
# resources_config 通过各个设备的 resource_tracker 进行uuid更新,利用uuid_mapping
|
||||||
|
# resources_config 的 root node 是
|
||||||
|
# # 创建反向映射:new_uuid -> old_uuid
|
||||||
|
# reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()}
|
||||||
|
# for tree in resources_config.trees:
|
||||||
|
# node = tree.root_node
|
||||||
|
# if node.res_content.type == "device":
|
||||||
|
# if node.res_content.id == "host_node":
|
||||||
|
# continue
|
||||||
|
# # slave节点走c2s更新接口,拿到add自行update uuid
|
||||||
|
# device_tracker = self.devices_instances[node.res_content.id].resource_tracker
|
||||||
|
# old_uuid = reverse_uuid_mapping.get(node.res_content.uuid)
|
||||||
|
# if old_uuid:
|
||||||
|
# # 找到旧UUID,使用UUID查找
|
||||||
|
# resource_instance = device_tracker.uuid_to_resources.get(old_uuid)
|
||||||
|
# else:
|
||||||
|
# # 未找到旧UUID,使用name查找
|
||||||
|
# resource_instance = device_tracker.figure_resource(
|
||||||
|
# {"name": node.res_content.name}
|
||||||
|
# )
|
||||||
|
# device_tracker.loop_update_uuid(resource_instance, uuid_mapping)
|
||||||
|
# else:
|
||||||
|
# try:
|
||||||
|
# for plr_resource in ResourceTreeSet([tree]).to_plr_resources():
|
||||||
|
# self.resource_tracker.add_resource(plr_resource)
|
||||||
|
# except Exception as ex:
|
||||||
|
# self.lab_logger().warning("[Host Node-Resource] 根节点物料序列化失败!")
|
||||||
|
except Exception as ex:
|
||||||
|
logger.error(f"[Host Node-Resource] 添加物料出错!\n{traceback.format_exc()}")
|
||||||
|
# 初始化Node基类,传递空参数覆盖列表
|
||||||
|
BaseROS2DeviceNode.__init__(
|
||||||
|
self,
|
||||||
|
driver_instance=self,
|
||||||
|
device_id=device_id,
|
||||||
|
device_uuid=host_node_dict["uuid"],
|
||||||
|
status_types={},
|
||||||
|
action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
|
||||||
|
hardware_interface={},
|
||||||
|
print_publish=False,
|
||||||
|
resource_tracker=self._resource_tracker, # host node并不是通过initialize 包一层传进来的
|
||||||
|
)
|
||||||
|
|
||||||
# 创建设备、动作客户端和目标存储
|
# 创建设备、动作客户端和目标存储
|
||||||
self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射
|
self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射
|
||||||
self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例
|
self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例
|
||||||
@@ -207,81 +282,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
].items():
|
].items():
|
||||||
controller_config["update_rate"] = update_rate
|
controller_config["update_rate"] = update_rate
|
||||||
self.initialize_controller(controller_id, controller_config)
|
self.initialize_controller(controller_id, controller_config)
|
||||||
# 创建 host_node 作为一个单独的 ResourceTree
|
|
||||||
|
|
||||||
host_node_dict = {
|
|
||||||
"id": "host_node",
|
|
||||||
"uuid": str(uuid.uuid4()),
|
|
||||||
"parent_uuid": "",
|
|
||||||
"name": "host_node",
|
|
||||||
"type": "device",
|
|
||||||
"class": "host_node",
|
|
||||||
"config": {},
|
|
||||||
"data": {},
|
|
||||||
"children": [],
|
|
||||||
"description": "",
|
|
||||||
"schema": {},
|
|
||||||
"model": {},
|
|
||||||
"icon": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建 host_node 的 ResourceTree
|
|
||||||
host_node_instance = ResourceDictInstance.get_resource_instance_from_dict(host_node_dict)
|
|
||||||
host_node_tree = ResourceTreeInstance(host_node_instance)
|
|
||||||
resources_config.trees.insert(0, host_node_tree)
|
|
||||||
try:
|
|
||||||
for bridge in self.bridges:
|
|
||||||
if hasattr(bridge, "resource_tree_add") and resources_config:
|
|
||||||
from unilabos.app.web.client import HTTPClient
|
|
||||||
|
|
||||||
client: HTTPClient = bridge
|
|
||||||
resource_start_time = time.time()
|
|
||||||
# 传递 ResourceTreeSet 对象,在 client 中转换为字典并获取 UUID 映射
|
|
||||||
uuid_mapping = client.resource_tree_add(resources_config, "", True)
|
|
||||||
resource_end_time = time.time()
|
|
||||||
self.lab_logger().info(
|
|
||||||
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
|
|
||||||
)
|
|
||||||
for edge in self.resources_edge_config:
|
|
||||||
edge["source_uuid"] = uuid_mapping.get(edge["source_uuid"], edge["source_uuid"])
|
|
||||||
edge["target_uuid"] = uuid_mapping.get(edge["target_uuid"], edge["target_uuid"])
|
|
||||||
resource_add_res = client.resource_edge_add(self.resources_edge_config)
|
|
||||||
resource_edge_end_time = time.time()
|
|
||||||
self.lab_logger().info(
|
|
||||||
f"[Host Node-Resource] 物料关系上传 {round(resource_edge_end_time - resource_end_time, 5) * 1000} ms"
|
|
||||||
)
|
|
||||||
# resources_config 通过各个设备的 resource_tracker 进行uuid更新,利用uuid_mapping
|
|
||||||
# resources_config 的 root node 是
|
|
||||||
# 创建反向映射:new_uuid -> old_uuid
|
|
||||||
reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()}
|
|
||||||
for tree in resources_config.trees:
|
|
||||||
node = tree.root_node
|
|
||||||
if node.res_content.type == "device":
|
|
||||||
for sub_node in node.children:
|
|
||||||
# 只有二级子设备
|
|
||||||
if sub_node.res_content.type != "device":
|
|
||||||
# slave节点走c2s更新接口,拿到add自行update uuid
|
|
||||||
device_tracker = self.devices_instances[node.res_content.id].resource_tracker
|
|
||||||
# sub_node.res_content.uuid 已经是新UUID,需要用旧UUID去查找
|
|
||||||
old_uuid = reverse_uuid_mapping.get(sub_node.res_content.uuid)
|
|
||||||
if old_uuid:
|
|
||||||
# 找到旧UUID,使用UUID查找
|
|
||||||
resource_instance = device_tracker.figure_resource({"uuid": old_uuid})
|
|
||||||
else:
|
|
||||||
# 未找到旧UUID,使用name查找
|
|
||||||
resource_instance = device_tracker.figure_resource(
|
|
||||||
{"name": sub_node.res_content.name}
|
|
||||||
)
|
|
||||||
device_tracker.loop_update_uuid(resource_instance, uuid_mapping)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
for plr_resource in ResourceTreeSet([tree]).to_plr_resources():
|
|
||||||
self.resource_tracker.add_resource(plr_resource)
|
|
||||||
except Exception as ex:
|
|
||||||
self.lab_logger().warning("[Host Node-Resource] 根节点物料序列化失败!")
|
|
||||||
except Exception as ex:
|
|
||||||
self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
|
|
||||||
self.lab_logger().error(traceback.format_exc())
|
|
||||||
# 创建定时器,定期发现设备
|
# 创建定时器,定期发现设备
|
||||||
self._discovery_timer = self.create_timer(
|
self._discovery_timer = self.create_timer(
|
||||||
discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup()
|
discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup()
|
||||||
@@ -862,7 +863,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _resource_tree_action_add_callback(self, data: dict, response: SerialCommand_Response): # OK
|
async def _resource_tree_action_add_callback(self, data: dict, response: SerialCommand_Response): # OK
|
||||||
resource_tree_set = ResourceTreeSet.load(data["data"])
|
resource_tree_set = ResourceTreeSet.load(data["data"])
|
||||||
mount_uuid = data["mount_uuid"]
|
mount_uuid = data["mount_uuid"]
|
||||||
first_add = data["first_add"]
|
first_add = data["first_add"]
|
||||||
@@ -903,7 +904,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
response.response = json.dumps(uuid_mapping) if success else "FAILED"
|
response.response = json.dumps(uuid_mapping) if success else "FAILED"
|
||||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
|
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
|
||||||
|
|
||||||
def _resource_tree_action_get_callback(self, data: dict, response: SerialCommand_Response): # OK
|
async def _resource_tree_action_get_callback(self, data: dict, response: SerialCommand_Response): # OK
|
||||||
uuid_list: List[str] = data["data"]
|
uuid_list: List[str] = data["data"]
|
||||||
with_children: bool = data["with_children"]
|
with_children: bool = data["with_children"]
|
||||||
from unilabos.app.web.client import http_client
|
from unilabos.app.web.client import http_client
|
||||||
@@ -911,7 +912,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
resource_response = http_client.resource_tree_get(uuid_list, with_children)
|
resource_response = http_client.resource_tree_get(uuid_list, with_children)
|
||||||
response.response = json.dumps(resource_response)
|
response.response = json.dumps(resource_response)
|
||||||
|
|
||||||
def _resource_tree_action_remove_callback(self, data: dict, response: SerialCommand_Response):
|
async def _resource_tree_action_remove_callback(self, data: dict, response: SerialCommand_Response):
|
||||||
"""
|
"""
|
||||||
子节点通知Host物料树删除
|
子节点通知Host物料树删除
|
||||||
"""
|
"""
|
||||||
@@ -919,7 +920,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
response.response = "OK"
|
response.response = "OK"
|
||||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree remove completed")
|
self.lab_logger().info(f"[Host Node-Resource] Resource tree remove completed")
|
||||||
|
|
||||||
def _resource_tree_action_update_callback(self, data: dict, response: SerialCommand_Response):
|
async def _resource_tree_action_update_callback(self, data: dict, response: SerialCommand_Response):
|
||||||
"""
|
"""
|
||||||
子节点通知Host物料树更新
|
子节点通知Host物料树更新
|
||||||
"""
|
"""
|
||||||
@@ -932,8 +933,17 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
from unilabos.app.web.client import http_client
|
from unilabos.app.web.client import http_client
|
||||||
|
|
||||||
|
uuid_to_trees: Dict[str, List[ResourceTreeInstance]] = collections.defaultdict(list)
|
||||||
|
for tree in resource_tree_set.trees:
|
||||||
|
uuid_to_trees[tree.root_node.res_content.parent_uuid].append(tree)
|
||||||
|
|
||||||
|
for uid, trees in uuid_to_trees.items():
|
||||||
|
new_tree_set = ResourceTreeSet(trees)
|
||||||
resource_start_time = time.time()
|
resource_start_time = time.time()
|
||||||
uuid_mapping = http_client.resource_tree_update(resource_tree_set, "", False)
|
self.lab_logger().info(
|
||||||
|
f"[Host Node-Resource] 物料 {[root_node.res_content.id for root_node in new_tree_set.root_nodes]} {uid} 挂载 {trees[0].root_node.res_content.parent_uuid} 请求更新上传"
|
||||||
|
)
|
||||||
|
uuid_mapping = http_client.resource_tree_add(new_tree_set, uid, False)
|
||||||
success = bool(uuid_mapping)
|
success = bool(uuid_mapping)
|
||||||
resource_end_time = time.time()
|
resource_end_time = time.time()
|
||||||
self.lab_logger().info(
|
self.lab_logger().info(
|
||||||
@@ -945,7 +955,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
response.response = json.dumps(uuid_mapping)
|
response.response = json.dumps(uuid_mapping)
|
||||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
|
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
|
||||||
|
|
||||||
def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
|
async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
|
||||||
"""
|
"""
|
||||||
子节点通知Host物料树更新
|
子节点通知Host物料树更新
|
||||||
|
|
||||||
@@ -958,13 +968,13 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
action = data["action"]
|
action = data["action"]
|
||||||
data = data["data"]
|
data = data["data"]
|
||||||
if action == "add":
|
if action == "add":
|
||||||
self._resource_tree_action_add_callback(data, response)
|
await self._resource_tree_action_add_callback(data, response)
|
||||||
elif action == "get":
|
elif action == "get":
|
||||||
self._resource_tree_action_get_callback(data, response)
|
await self._resource_tree_action_get_callback(data, response)
|
||||||
elif action == "update":
|
elif action == "update":
|
||||||
self._resource_tree_action_update_callback(data, response)
|
await self._resource_tree_action_update_callback(data, response)
|
||||||
elif action == "remove":
|
elif action == "remove":
|
||||||
self._resource_tree_action_remove_callback(data, response)
|
await self._resource_tree_action_remove_callback(data, response)
|
||||||
else:
|
else:
|
||||||
self.lab_logger().error(f"[Host Node-Resource] Invalid action: {action}")
|
self.lab_logger().error(f"[Host Node-Resource] Invalid action: {action}")
|
||||||
response.response = "ERROR"
|
response.response = "ERROR"
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ from typing import List, Dict, Any, Optional, TYPE_CHECKING
|
|||||||
|
|
||||||
import rclpy
|
import rclpy
|
||||||
from rosidl_runtime_py import message_to_ordereddict
|
from rosidl_runtime_py import message_to_ordereddict
|
||||||
|
from unilabos_msgs.msg import Resource
|
||||||
|
from unilabos_msgs.srv import ResourceUpdate
|
||||||
|
|
||||||
from unilabos.messages import * # type: ignore # protocol names
|
from unilabos.messages import * # type: ignore # protocol names
|
||||||
from rclpy.action import ActionServer, ActionClient
|
from rclpy.action import ActionServer, ActionClient
|
||||||
from rclpy.action.server import ServerGoalHandle
|
from rclpy.action.server import ServerGoalHandle
|
||||||
from rclpy.callback_groups import ReentrantCallbackGroup
|
from rclpy.callback_groups import ReentrantCallbackGroup
|
||||||
from unilabos_msgs.msg import Resource # type: ignore
|
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||||
from unilabos_msgs.srv import ResourceGet, ResourceUpdate # type: ignore
|
|
||||||
|
|
||||||
from unilabos.compile import action_protocol_generators
|
from unilabos.compile import action_protocol_generators
|
||||||
from unilabos.resources.graphio import list_to_nested_dict, nested_dict_to_list
|
from unilabos.resources.graphio import list_to_nested_dict, nested_dict_to_list
|
||||||
@@ -20,11 +21,11 @@ from unilabos.ros.initialize_device import initialize_device_from_dict
|
|||||||
from unilabos.ros.msgs.message_converter import (
|
from unilabos.ros.msgs.message_converter import (
|
||||||
get_action_type,
|
get_action_type,
|
||||||
convert_to_ros_msg,
|
convert_to_ros_msg,
|
||||||
convert_from_ros_msg,
|
|
||||||
convert_from_ros_msg_with_mapping,
|
convert_from_ros_msg_with_mapping,
|
||||||
)
|
)
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode
|
||||||
from unilabos.utils.type_check import serialize_result_info, get_result_info_str
|
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
|
||||||
|
from unilabos.utils.type_check import get_result_info_str
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||||
@@ -50,6 +51,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
*,
|
*,
|
||||||
driver_instance: "WorkstationBase",
|
driver_instance: "WorkstationBase",
|
||||||
device_id: str,
|
device_id: str,
|
||||||
|
device_uuid: str,
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
action_value_mappings: Dict[str, Any],
|
action_value_mappings: Dict[str, Any],
|
||||||
hardware_interface: Dict[str, Any],
|
hardware_interface: Dict[str, Any],
|
||||||
@@ -64,6 +66,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=driver_instance,
|
driver_instance=driver_instance,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
|
action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
|
||||||
hardware_interface=hardware_interface,
|
hardware_interface=hardware_interface,
|
||||||
@@ -222,16 +225,28 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
# 向Host查询物料当前状态
|
# 向Host查询物料当前状态
|
||||||
for k, v in goal.get_fields_and_field_types().items():
|
for k, v in goal.get_fields_and_field_types().items():
|
||||||
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||||
r = ResourceGet.Request()
|
self.lab_logger().info(f"{protocol_name} 查询资源状态: Key: {k} Type: {v}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 统一处理单个或多个资源
|
||||||
resource_id = (
|
resource_id = (
|
||||||
protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"]
|
protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"]
|
||||||
)
|
)
|
||||||
r.id = resource_id
|
r = SerialCommand_Request()
|
||||||
r.with_children = True
|
r.command = json.dumps({"id": resource_id, "with_children": True})
|
||||||
response = await self._resource_clients["resource_get"].call_async(r)
|
# 发送请求并等待响应
|
||||||
protocol_kwargs[k] = list_to_nested_dict(
|
response: SerialCommand_Response = await self._resource_clients[
|
||||||
[convert_from_ros_msg(rs) for rs in response.resources]
|
"resource_get"
|
||||||
)
|
].call_async(
|
||||||
|
r
|
||||||
|
) # type: ignore
|
||||||
|
raw_data = json.loads(response.response)
|
||||||
|
tree_set = ResourceTreeSet.from_raw_list(raw_data)
|
||||||
|
target = tree_set.dump()
|
||||||
|
protocol_kwargs[k] = target[0][0] if v == "unilabos_msgs/Resource" else target
|
||||||
|
except Exception as ex:
|
||||||
|
self.lab_logger().error(f"查询资源失败: {k}, 错误: {ex}\n{traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
self.lab_logger().info(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}")
|
self.lab_logger().info(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
from pydantic import BaseModel, field_serializer, field_validator
|
from pydantic import BaseModel, field_serializer, field_validator
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
@@ -140,7 +141,7 @@ class ResourceDictInstance(object):
|
|||||||
def get_nested_dict(self) -> Dict[str, Any]:
|
def get_nested_dict(self) -> Dict[str, Any]:
|
||||||
"""获取资源实例的嵌套字典表示"""
|
"""获取资源实例的嵌套字典表示"""
|
||||||
res_dict = self.res_content.model_dump(by_alias=True)
|
res_dict = self.res_content.model_dump(by_alias=True)
|
||||||
res_dict["children"] = {child.res_content.name: child.get_nested_dict() for child in self.children}
|
res_dict["children"] = {child.res_content.id: child.get_nested_dict() for child in self.children}
|
||||||
res_dict["parent"] = self.res_content.parent_instance_name
|
res_dict["parent"] = self.res_content.parent_instance_name
|
||||||
res_dict["position"] = self.res_content.position.position.model_dump()
|
res_dict["position"] = self.res_content.position.position.model_dump()
|
||||||
return res_dict
|
return res_dict
|
||||||
@@ -213,7 +214,7 @@ class ResourceTreeInstance(object):
|
|||||||
if node.res_content.uuid:
|
if node.res_content.uuid:
|
||||||
known_uuids.add(node.res_content.uuid)
|
known_uuids.add(node.res_content.uuid)
|
||||||
else:
|
else:
|
||||||
print(f"警告: 资源 {node.res_content.id} 没有uuid")
|
logger.warning(f"警告: 资源 {node.res_content.id} 没有uuid")
|
||||||
|
|
||||||
# 验证并递归处理子节点
|
# 验证并递归处理子节点
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
@@ -289,8 +290,6 @@ class ResourceTreeSet(object):
|
|||||||
elif isinstance(resource_list[0], ResourceTreeInstance):
|
elif isinstance(resource_list[0], ResourceTreeInstance):
|
||||||
# 已经是ResourceTree列表
|
# 已经是ResourceTree列表
|
||||||
self.trees = cast(List[ResourceTreeInstance], resource_list)
|
self.trees = cast(List[ResourceTreeInstance], resource_list)
|
||||||
elif isinstance(resource_list[0], list):
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"不支持的类型: {type(resource_list[0])}。"
|
f"不支持的类型: {type(resource_list[0])}。"
|
||||||
@@ -307,10 +306,7 @@ class ResourceTreeSet(object):
|
|||||||
replace_info = {
|
replace_info = {
|
||||||
"plate": "plate",
|
"plate": "plate",
|
||||||
"well": "well",
|
"well": "well",
|
||||||
"tip_spot": "container",
|
|
||||||
"trash": "container",
|
|
||||||
"deck": "deck",
|
"deck": "deck",
|
||||||
"tip_rack": "container",
|
|
||||||
}
|
}
|
||||||
if source in replace_info:
|
if source in replace_info:
|
||||||
return replace_info[source]
|
return replace_info[source]
|
||||||
@@ -320,7 +316,12 @@ class ResourceTreeSet(object):
|
|||||||
|
|
||||||
def build_uuid_mapping(res: "PLRResource", uuid_list: list):
|
def build_uuid_mapping(res: "PLRResource", uuid_list: list):
|
||||||
"""递归构建uuid映射字典"""
|
"""递归构建uuid映射字典"""
|
||||||
uuid_list.append(getattr(res, "unilabos_uuid", ""))
|
uid = getattr(res, "unilabos_uuid", "")
|
||||||
|
if not uid:
|
||||||
|
uid = str(uuid.uuid4())
|
||||||
|
res.unilabos_uuid = uid
|
||||||
|
logger.warning(f"{res}没有uuid,请设置后再传入,默认填充{uid}!\n{traceback.format_exc()}")
|
||||||
|
uuid_list.append(uid)
|
||||||
for child in res.children:
|
for child in res.children:
|
||||||
build_uuid_mapping(child, uuid_list)
|
build_uuid_mapping(child, uuid_list)
|
||||||
|
|
||||||
@@ -384,7 +385,7 @@ class ResourceTreeSet(object):
|
|||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
# 类型映射
|
# 类型映射
|
||||||
TYPE_MAP = {"plate": "plate", "well": "well", "container": "tip_spot", "deck": "deck", "tip_rack": "tip_rack"}
|
TYPE_MAP = {"plate": "Plate", "well": "Well", "deck": "Deck"}
|
||||||
|
|
||||||
def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict):
|
def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict):
|
||||||
"""一次遍历收集 name_to_uuid 和 all_states"""
|
"""一次遍历收集 name_to_uuid 和 all_states"""
|
||||||
@@ -396,13 +397,13 @@ class ResourceTreeSet(object):
|
|||||||
def node_to_plr_dict(node: ResourceDictInstance, has_model: bool):
|
def node_to_plr_dict(node: ResourceDictInstance, has_model: bool):
|
||||||
"""转换节点为 PLR 字典格式"""
|
"""转换节点为 PLR 字典格式"""
|
||||||
res = node.res_content
|
res = node.res_content
|
||||||
plr_type = TYPE_MAP.get(res.type, "tip_spot")
|
plr_type = TYPE_MAP.get(res.type, res.type)
|
||||||
if res.type not in TYPE_MAP:
|
if res.type not in TYPE_MAP:
|
||||||
logger.warning(f"未知类型 {res.type},使用默认类型 tip_spot")
|
logger.warning(f"未知类型 {res.type}")
|
||||||
|
|
||||||
d = {
|
d = {
|
||||||
"name": res.name,
|
"name": res.name,
|
||||||
"type": plr_type,
|
"type": res.config.get("type", plr_type),
|
||||||
"size_x": res.config.get("size_x", 0),
|
"size_x": res.config.get("size_x", 0),
|
||||||
"size_y": res.config.get("size_y", 0),
|
"size_y": res.config.get("size_y", 0),
|
||||||
"size_z": res.config.get("size_z", 0),
|
"size_z": res.config.get("size_z", 0),
|
||||||
@@ -413,7 +414,7 @@ class ResourceTreeSet(object):
|
|||||||
"type": "Coordinate",
|
"type": "Coordinate",
|
||||||
},
|
},
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"},
|
"rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"},
|
||||||
"category": plr_type,
|
"category": res.config.get("category", plr_type),
|
||||||
"children": [node_to_plr_dict(child, has_model) for child in node.children],
|
"children": [node_to_plr_dict(child, has_model) for child in node.children],
|
||||||
"parent_name": res.parent_instance_name,
|
"parent_name": res.parent_instance_name,
|
||||||
**res.config,
|
**res.config,
|
||||||
@@ -435,7 +436,7 @@ class ResourceTreeSet(object):
|
|||||||
try:
|
try:
|
||||||
sub_cls = find_subclass(plr_dict["type"], PLRResource)
|
sub_cls = find_subclass(plr_dict["type"], PLRResource)
|
||||||
if sub_cls is None:
|
if sub_cls is None:
|
||||||
raise ValueError(f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类")
|
raise ValueError(f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}")
|
||||||
spec = inspect.signature(sub_cls)
|
spec = inspect.signature(sub_cls)
|
||||||
if "category" not in spec.parameters:
|
if "category" not in spec.parameters:
|
||||||
plr_dict.pop("category", None)
|
plr_dict.pop("category", None)
|
||||||
@@ -715,16 +716,9 @@ class ResourceTreeSet(object):
|
|||||||
Returns:
|
Returns:
|
||||||
ResourceTreeSet: 反序列化后的资源树集合
|
ResourceTreeSet: 反序列化后的资源树集合
|
||||||
"""
|
"""
|
||||||
# 将每个字典转换为 ResourceInstanceDict
|
|
||||||
# FIXME: 需要重新确定parent关系
|
|
||||||
nested_lists = []
|
nested_lists = []
|
||||||
for tree_data in data:
|
for tree_data in data:
|
||||||
flatten_instances = [
|
nested_lists.extend(ResourceTreeSet.from_raw_list(tree_data).trees)
|
||||||
ResourceDictInstance.get_resource_instance_from_dict(node_dict) for node_dict in tree_data
|
|
||||||
]
|
|
||||||
nested_lists.append(flatten_instances)
|
|
||||||
|
|
||||||
# 使用现有的构造函数创建 ResourceTreeSet
|
|
||||||
return cls(nested_lists)
|
return cls(nested_lists)
|
||||||
|
|
||||||
|
|
||||||
@@ -777,7 +771,8 @@ class DeviceNodeResourceTracker(object):
|
|||||||
else:
|
else:
|
||||||
return getattr(resource, uuid_attr, None)
|
return getattr(resource, uuid_attr, None)
|
||||||
|
|
||||||
def _set_resource_uuid(self, resource, new_uuid: str):
|
@classmethod
|
||||||
|
def set_resource_uuid(cls, resource, new_uuid: str):
|
||||||
"""
|
"""
|
||||||
设置资源的 uuid,统一处理 dict 和 instance 两种类型
|
设置资源的 uuid,统一处理 dict 和 instance 两种类型
|
||||||
|
|
||||||
@@ -830,7 +825,7 @@ class DeviceNodeResourceTracker(object):
|
|||||||
resource_name = self._get_resource_attr(res, "name")
|
resource_name = self._get_resource_attr(res, "name")
|
||||||
if resource_name and resource_name in name_to_uuid_map:
|
if resource_name and resource_name in name_to_uuid_map:
|
||||||
new_uuid = name_to_uuid_map[resource_name]
|
new_uuid = name_to_uuid_map[resource_name]
|
||||||
self._set_resource_uuid(res, new_uuid)
|
self.set_resource_uuid(res, new_uuid)
|
||||||
self.uuid_to_resources[new_uuid] = res
|
self.uuid_to_resources[new_uuid] = res
|
||||||
logger.debug(f"设置资源UUID: {resource_name} -> {new_uuid}")
|
logger.debug(f"设置资源UUID: {resource_name} -> {new_uuid}")
|
||||||
return 1
|
return 1
|
||||||
@@ -842,7 +837,7 @@ class DeviceNodeResourceTracker(object):
|
|||||||
"""
|
"""
|
||||||
递归遍历资源树,更新所有节点的uuid
|
递归遍历资源树,更新所有节点的uuid
|
||||||
|
|
||||||
Args:
|
Args:0
|
||||||
resource: 资源对象(可以是dict或实例)
|
resource: 资源对象(可以是dict或实例)
|
||||||
uuid_map: uuid映射字典,{old_uuid: new_uuid}
|
uuid_map: uuid映射字典,{old_uuid: new_uuid}
|
||||||
|
|
||||||
@@ -852,17 +847,18 @@ class DeviceNodeResourceTracker(object):
|
|||||||
|
|
||||||
def process(res):
|
def process(res):
|
||||||
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
|
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
|
||||||
|
replaced = 0
|
||||||
if current_uuid and current_uuid in uuid_map:
|
if current_uuid and current_uuid in uuid_map:
|
||||||
new_uuid = uuid_map[current_uuid]
|
new_uuid = uuid_map[current_uuid]
|
||||||
if current_uuid != new_uuid:
|
if current_uuid != new_uuid:
|
||||||
self._set_resource_uuid(res, new_uuid)
|
self.set_resource_uuid(res, new_uuid)
|
||||||
# 更新uuid_to_resources映射
|
# 更新uuid_to_resources映射
|
||||||
if current_uuid in self.uuid_to_resources:
|
if current_uuid in self.uuid_to_resources:
|
||||||
self.uuid_to_resources.pop(current_uuid)
|
self.uuid_to_resources.pop(current_uuid)
|
||||||
self.uuid_to_resources[new_uuid] = res
|
self.uuid_to_resources[new_uuid] = res
|
||||||
logger.debug(f"更新uuid: {current_uuid} -> {new_uuid}")
|
logger.debug(f"更新uuid: {current_uuid} -> {new_uuid}")
|
||||||
return 1
|
replaced = 1
|
||||||
return 0
|
return replaced
|
||||||
|
|
||||||
return self._traverse_and_process(resource, process)
|
return self._traverse_and_process(resource, process)
|
||||||
|
|
||||||
@@ -877,8 +873,9 @@ class DeviceNodeResourceTracker(object):
|
|||||||
def process(res):
|
def process(res):
|
||||||
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
|
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
|
||||||
if current_uuid:
|
if current_uuid:
|
||||||
|
old = self.uuid_to_resources.get(current_uuid)
|
||||||
self.uuid_to_resources[current_uuid] = res
|
self.uuid_to_resources[current_uuid] = res
|
||||||
logger.debug(f"收集资源UUID映射: {current_uuid} -> {res}")
|
logger.debug(f"收集资源UUID映射: {current_uuid} -> {res} {'' if old is None else f'(覆盖旧值: {old})'}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
self._traverse_and_process(resource, process)
|
self._traverse_and_process(resource, process)
|
||||||
@@ -913,9 +910,23 @@ class DeviceNodeResourceTracker(object):
|
|||||||
Args:
|
Args:
|
||||||
resource: 资源对象(可以是dict或实例)
|
resource: 资源对象(可以是dict或实例)
|
||||||
"""
|
"""
|
||||||
|
root_uuids = {}
|
||||||
for r in self.resources:
|
for r in self.resources:
|
||||||
|
res_uuid = r.get("uuid") if isinstance(r, dict) else getattr(r, "unilabos_uuid", None)
|
||||||
|
if res_uuid:
|
||||||
|
root_uuids[res_uuid] = r
|
||||||
if id(r) == id(resource):
|
if id(r) == id(resource):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 这里只做uuid的根节点比较
|
||||||
|
if isinstance(resource, dict):
|
||||||
|
res_uuid = resource.get("uuid")
|
||||||
|
else:
|
||||||
|
res_uuid = getattr(resource, "unilabos_uuid", None)
|
||||||
|
if res_uuid in root_uuids:
|
||||||
|
old_res = root_uuids[res_uuid]
|
||||||
|
# self.remove_resource(old_res)
|
||||||
|
logger.warning(f"资源{resource}已存在,旧资源: {old_res}")
|
||||||
self.resources.append(resource)
|
self.resources.append(resource)
|
||||||
# 递归收集uuid映射
|
# 递归收集uuid映射
|
||||||
self._collect_uuid_mapping(resource)
|
self._collect_uuid_mapping(resource)
|
||||||
@@ -1046,13 +1057,19 @@ class DeviceNodeResourceTracker(object):
|
|||||||
) -> List[Tuple[Any, Any]]:
|
) -> List[Tuple[Any, Any]]:
|
||||||
res_list = []
|
res_list = []
|
||||||
# print(resource, target_resource_cls_type, identifier_key, compare_value)
|
# print(resource, target_resource_cls_type, identifier_key, compare_value)
|
||||||
|
children = []
|
||||||
|
if not isinstance(resource, dict):
|
||||||
children = getattr(resource, "children", [])
|
children = getattr(resource, "children", [])
|
||||||
|
else:
|
||||||
|
children = resource.get("children")
|
||||||
|
if children is not None:
|
||||||
|
children = list(children.values()) if isinstance(children, dict) else children
|
||||||
for child in children:
|
for child in children:
|
||||||
res_list.extend(
|
res_list.extend(
|
||||||
self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource)
|
self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource)
|
||||||
)
|
)
|
||||||
if issubclass(type(resource), target_resource_cls_type):
|
if issubclass(type(resource), target_resource_cls_type):
|
||||||
if target_resource_cls_type == dict:
|
if type(resource) == dict:
|
||||||
# 对于字典类型,直接检查 identifier_key
|
# 对于字典类型,直接检查 identifier_key
|
||||||
if identifier_key in resource:
|
if identifier_key in resource:
|
||||||
if resource[identifier_key] == compare_value:
|
if resource[identifier_key] == compare_value:
|
||||||
|
|||||||
@@ -336,6 +336,9 @@ class WorkstationNodeCreator(DeviceClassCreator[T]):
|
|||||||
try:
|
try:
|
||||||
# 创建实例,额外补充一个给protocol node的字段,后面考虑取消
|
# 创建实例,额外补充一个给protocol node的字段,后面考虑取消
|
||||||
data["children"] = self.children
|
data["children"] = self.children
|
||||||
|
for material_id, child in self.children.items():
|
||||||
|
if child["type"] != "device":
|
||||||
|
self.resource_tracker.add_resource(self.children[material_id])
|
||||||
deck_dict = data.get("deck")
|
deck_dict = data.get("deck")
|
||||||
if deck_dict:
|
if deck_dict:
|
||||||
from pylabrobot.resources import Deck, Resource
|
from pylabrobot.resources import Deck, Resource
|
||||||
|
|||||||
Reference in New Issue
Block a user