mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-06 15:05:13 +00:00
Compare commits
225 Commits
v0.10.7
...
62c4135938
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62c4135938 | ||
|
|
027b4269c4 | ||
|
|
3757bd9c58 | ||
|
|
c75b7d5aae | ||
|
|
dfc635189c | ||
|
|
d8f3ebac15 | ||
|
|
4a1e703a3a | ||
|
|
55d22a7c29 | ||
|
|
03a4e4ecba | ||
|
|
2316c34cb5 | ||
|
|
a8887161d3 | ||
|
|
25834f5ba0 | ||
|
|
a1e9332b51 | ||
|
|
357fc038ef | ||
|
|
fd58ef07f3 | ||
|
|
93dee2c1dc | ||
|
|
70fbf19009 | ||
|
|
9149155232 | ||
|
|
1ca1792e3c | ||
|
|
485e7e8dd2 | ||
|
|
4ddabdcb65 | ||
|
|
a5b0325301 | ||
|
|
50b44938c7 | ||
|
|
df0d2235b0 | ||
|
|
4e434eeb97 | ||
|
|
ca027bf0eb | ||
|
|
635a332b4e | ||
|
|
edf7a117ca | ||
|
|
70b2715996 | ||
|
|
7e8dfc2dc5 | ||
|
|
9b626489a8 | ||
|
|
03fe208743 | ||
|
|
e913e540a3 | ||
|
|
aed39b648d | ||
|
|
8c8359fab3 | ||
|
|
5d20be0762 | ||
|
|
09f745d300 | ||
|
|
bbcbcde9a4 | ||
|
|
42b437cdea | ||
|
|
ffd0f2d26a | ||
|
|
32422c0b3d | ||
|
|
c44e597dc0 | ||
|
|
4eef012a8e | ||
|
|
ac69452f3c | ||
|
|
57b30f627b | ||
|
|
2d2a4ca067 | ||
|
|
a2613aad4c | ||
|
|
54f75183ff | ||
|
|
735be067dc | ||
|
|
0fe62d64f0 | ||
|
|
2d4ecec1e1 | ||
|
|
0f976a1874 | ||
|
|
b263a7e679 | ||
|
|
7c7f1b31c5 | ||
|
|
00e668e140 | ||
|
|
4989f65a0b | ||
|
|
9fa3688196 | ||
|
|
40fb1ea49c | ||
|
|
18b0bb397e | ||
|
|
65abc5dbf7 | ||
|
|
2455ca15ba | ||
|
|
05a3ff607a | ||
|
|
ec882df36d | ||
|
|
43b992e3eb | ||
|
|
6422fa5a9a | ||
|
|
434b9e98e0 | ||
|
|
040073f430 | ||
|
|
3d95c9896a | ||
|
|
9aa97ed01e | ||
|
|
0b8bdf5e0a | ||
|
|
299f010754 | ||
|
|
15ce0d6883 | ||
|
|
dec474e1a7 | ||
|
|
5f187899fc | ||
|
|
c8d16c7024 | ||
|
|
25d46dc9d5 | ||
|
|
88c4d1a9d1 | ||
|
|
81fd8291c5 | ||
|
|
3a11eb90d4 | ||
|
|
387866b9c9 | ||
|
|
7f40f141f6 | ||
|
|
6fc7ed1b88 | ||
|
|
93f0e08d75 | ||
|
|
4b43734b55 | ||
|
|
174b1914d4 | ||
|
|
704e13f030 | ||
|
|
0c42d60cf2 | ||
|
|
df33e1a214 | ||
|
|
1f49924966 | ||
|
|
609b6006e8 | ||
|
|
67c01271b7 | ||
|
|
a1783f489e | ||
|
|
a8f6527de9 | ||
|
|
54cfaf15f3 | ||
|
|
5610c28b67 | ||
|
|
cfc1ee6e79 | ||
|
|
1c9d2ee98a | ||
|
|
3fe8f4ca44 | ||
|
|
2476821dcc | ||
|
|
7b426ed5ae | ||
|
|
9bbae96447 | ||
|
|
10aabb7592 | ||
|
|
709eb0d91c | ||
|
|
14b7d52825 | ||
|
|
a5397ffe12 | ||
|
|
c6c2da69ba | ||
|
|
622e579063 | ||
|
|
196e0f7e2b | ||
|
|
a632fd495e | ||
|
|
a8cc02a126 | ||
|
|
ad2e1432c6 | ||
|
|
c3b9583eac | ||
|
|
5c47cd0c8a | ||
|
|
63ab1af45d | ||
|
|
a8419dc0c3 | ||
|
|
34f05f2e25 | ||
|
|
0dc2488f02 | ||
|
|
f13156e792 | ||
|
|
13fd1ac572 | ||
|
|
f8ef6e0686 | ||
|
|
94a7b8aaca | ||
|
|
301bea639e | ||
|
|
4b5a83efa4 | ||
|
|
2889e9be2c | ||
|
|
304aebbba7 | ||
|
|
091c9fa247 | ||
|
|
67ca45a240 | ||
|
|
7aab2ea493 | ||
|
|
62f3a6d696 | ||
|
|
eb70ad0e18 | ||
|
|
768f43880e | ||
|
|
762c3c737c | ||
|
|
ace98a4472 | ||
|
|
41eaa88c6f | ||
|
|
a1a55a2c0a | ||
|
|
2eaa0ca729 | ||
|
|
6f8f070f40 | ||
|
|
da4bd927e0 | ||
|
|
01f8816597 | ||
|
|
e5006285df | ||
|
|
573c724a5c | ||
|
|
09549d2839 | ||
|
|
50c7777cea | ||
|
|
4888f02c09 | ||
|
|
779c9693d9 | ||
|
|
ffa841a41a | ||
|
|
fc669f09f8 | ||
|
|
2ca0311de6 | ||
|
|
94cdcbf24e | ||
|
|
1cd07915e7 | ||
|
|
b600fc666d | ||
|
|
9e214c56c1 | ||
|
|
bdf27a7e82 | ||
|
|
2493fb9f94 | ||
|
|
c7a0ff67a9 | ||
|
|
711a7c65fa | ||
|
|
cde7956896 | ||
|
|
95b6fd0451 | ||
|
|
513e848d89 | ||
|
|
58d1cc4720 | ||
|
|
5676dd6589 | ||
|
|
1ae274a833 | ||
|
|
22b88c8441 | ||
|
|
81bcc1907d | ||
|
|
8cffd3dc21 | ||
|
|
a722636938 | ||
|
|
f68340d932 | ||
|
|
361eae2f6d | ||
|
|
c25283ae04 | ||
|
|
961752fb0d | ||
|
|
55165024dd | ||
|
|
6ddceb8393 | ||
|
|
4e52c7d2f4 | ||
|
|
0b56efc89d | ||
|
|
a27b93396a | ||
|
|
2a60a6c27e | ||
|
|
5dda94044d | ||
|
|
0cfc6f45e3 | ||
|
|
831f4549f9 | ||
|
|
f4d4eb06d3 | ||
|
|
e3b8164f6b | ||
|
|
78c04acc2e | ||
|
|
cd0428ea78 | ||
|
|
bdddbd57ba | ||
|
|
a312de08a5 | ||
|
|
68513b5745 | ||
|
|
19027350fb | ||
|
|
bbbdb06bbc | ||
|
|
cd84e26126 | ||
|
|
ce5bab3af1 | ||
|
|
82d9ef6bf7 | ||
|
|
332b33c6f4 | ||
|
|
1ec642ee3a | ||
|
|
7d8e6d029b | ||
|
|
5ec8a57a1f | ||
|
|
ae3c1100ae | ||
|
|
14bc2e6cda | ||
|
|
9f823a4198 | ||
|
|
02c79363c1 | ||
|
|
227ff1284a | ||
|
|
4b7bde6be5 | ||
|
|
8a669ac35a | ||
|
|
a1538da39e | ||
|
|
0063df4cf3 | ||
|
|
e570ba4976 | ||
|
|
e8c1f76dbb | ||
|
|
f791c1a342 | ||
|
|
ea60cbe891 | ||
|
|
eac9b8ab3d | ||
|
|
573bcf1a6c | ||
|
|
50e93cb1af | ||
|
|
fe1a029a9b | ||
|
|
662c063f50 | ||
|
|
01cbbba0b3 | ||
|
|
e6c556cf19 | ||
|
|
0605f305ed | ||
|
|
37d8108ec4 | ||
|
|
6081dac561 | ||
|
|
5b2d066127 | ||
|
|
06e66765e7 | ||
|
|
98ce360088 | ||
|
|
5cd0f72fbd | ||
|
|
343f394203 | ||
|
|
46aa7a7bd2 | ||
|
|
a66369e2c3 |
114
.github/workflows/conda-pack-build.yml
vendored
114
.github/workflows/conda-pack-build.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
# Windows uses cmd for better conda/mamba compatibility, Unix uses bash
|
||||
shell: ${{ matrix.platform == 'win-64' && 'cmd /C CALL {0}' || 'bash -el {0}' }}
|
||||
shell: ${{ matrix.platform == 'win-64' && 'cmd' || 'bash' }}
|
||||
|
||||
steps:
|
||||
- name: Check if platform should be built
|
||||
@@ -73,7 +73,6 @@ jobs:
|
||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||
channel-priority: flexible
|
||||
activate-environment: unilab
|
||||
auto-activate-base: true
|
||||
auto-update-conda: false
|
||||
show-channel-urls: true
|
||||
|
||||
@@ -82,7 +81,7 @@ jobs:
|
||||
run: |
|
||||
echo Installing unilabos and dependencies to unilab environment...
|
||||
echo Using mamba for faster and more reliable dependency resolution...
|
||||
mamba install uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
|
||||
- name: Install conda-pack, unilabos and dependencies (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
@@ -90,15 +89,15 @@ jobs:
|
||||
run: |
|
||||
echo "Installing unilabos and dependencies to unilab environment..."
|
||||
echo "Using mamba for faster and more reliable dependency resolution..."
|
||||
mamba install uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
|
||||
- name: Get latest ros-humble-unilabos-msgs version (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
id: msgs_version_win
|
||||
run: |
|
||||
echo Checking installed ros-humble-unilabos-msgs version...
|
||||
conda list ros-humble-unilabos-msgs
|
||||
for /f "tokens=2" %%i in ('conda list ros-humble-unilabos-msgs --json ^| python -c "import sys, json; pkgs=json.load(sys.stdin); print(pkgs[0]['version'] if pkgs else 'not-found')"') do set VERSION=%%i
|
||||
conda list -n unilab ros-humble-unilabos-msgs
|
||||
for /f "tokens=2" %%i in ('conda list -n unilab ros-humble-unilabos-msgs --json ^| python -c "import sys, json; pkgs=json.load(sys.stdin); print(pkgs[0]['version'] if pkgs else 'not-found')"') do set VERSION=%%i
|
||||
echo installed_version=%VERSION% >> %GITHUB_OUTPUT%
|
||||
echo Installed ros-humble-unilabos-msgs version: %VERSION%
|
||||
|
||||
@@ -108,7 +107,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Checking installed ros-humble-unilabos-msgs version..."
|
||||
VERSION=$(conda list ros-humble-unilabos-msgs --json | python -c "import sys, json; pkgs=json.load(sys.stdin); print(pkgs[0]['version'] if pkgs else 'not-found')")
|
||||
VERSION=$(conda list -n unilab ros-humble-unilabos-msgs --json | python -c "import sys, json; pkgs=json.load(sys.stdin); print(pkgs[0]['version'] if pkgs else 'not-found')")
|
||||
echo "installed_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Installed ros-humble-unilabos-msgs version: $VERSION"
|
||||
|
||||
@@ -119,7 +118,7 @@ jobs:
|
||||
mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo Search completed
|
||||
echo.
|
||||
echo Updating ros-humble-unilabos-msgs to latest version...
|
||||
mamba update ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo Already at latest version
|
||||
mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo Already at latest version
|
||||
|
||||
- name: Check for newer ros-humble-unilabos-msgs (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
@@ -129,65 +128,65 @@ jobs:
|
||||
mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo "Search completed"
|
||||
echo ""
|
||||
echo "Updating ros-humble-unilabos-msgs to latest version..."
|
||||
mamba update ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo "Already at latest version"
|
||||
mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo "Already at latest version"
|
||||
|
||||
- name: Install latest unilabos from source (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo Uninstalling existing unilabos...
|
||||
pip uninstall unilabos -y || echo unilabos not installed via pip
|
||||
mamba run -n unilab pip uninstall unilabos -y || echo unilabos not installed via pip
|
||||
echo Installing unilabos from source (branch: ${{ github.event.inputs.branch }})...
|
||||
pip install .
|
||||
mamba run -n unilab pip install .
|
||||
echo Verifying installation...
|
||||
pip show unilabos
|
||||
mamba run -n unilab pip show unilabos
|
||||
|
||||
- name: Install latest unilabos from source (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Uninstalling existing unilabos..."
|
||||
pip uninstall unilabos -y || echo "unilabos not installed via pip"
|
||||
mamba run -n unilab pip uninstall unilabos -y || echo "unilabos not installed via pip"
|
||||
echo "Installing unilabos from source (branch: ${{ github.event.inputs.branch }})..."
|
||||
pip install .
|
||||
mamba run -n unilab pip install .
|
||||
echo "Verifying installation..."
|
||||
pip show unilabos
|
||||
mamba run -n unilab pip show unilabos
|
||||
|
||||
- name: Display environment info (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo === Environment Information ===
|
||||
conda env list
|
||||
mamba env list
|
||||
echo.
|
||||
echo === Installed Packages ===
|
||||
conda list | findstr /C:"unilabos" /C:"ros-humble-unilabos-msgs" || conda list
|
||||
mamba list -n unilab | findstr /C:"unilabos" /C:"ros-humble-unilabos-msgs" || mamba list -n unilab
|
||||
echo.
|
||||
echo === Python Packages ===
|
||||
pip list | findstr unilabos || pip list
|
||||
mamba run -n unilab pip list | findstr unilabos || mamba run -n unilab pip list
|
||||
|
||||
- name: Display environment info (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "=== Environment Information ==="
|
||||
conda env list
|
||||
mamba env list
|
||||
echo ""
|
||||
echo "=== Installed Packages ==="
|
||||
conda list | grep -E "(unilabos|ros-humble-unilabos-msgs)" || conda list
|
||||
mamba list -n unilab | grep -E "(unilabos|ros-humble-unilabos-msgs)" || mamba list -n unilab
|
||||
echo ""
|
||||
echo "=== Python Packages ==="
|
||||
pip list | grep unilabos || pip list
|
||||
mamba run -n unilab pip list | grep unilabos || mamba run -n unilab pip list
|
||||
|
||||
- name: Verify environment integrity (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo Verifying Python version...
|
||||
python -c "import sys; print(f'Python version: {sys.version}')"
|
||||
mamba run -n unilab python -c "import sys; print(f'Python version: {sys.version}')"
|
||||
echo Verifying unilabos import...
|
||||
python -c "import unilabos; print(f'UniLabOS version: {unilabos.__version__}')" || echo Warning: Could not import unilabos
|
||||
mamba run -n unilab python -c "import unilabos; print(f'UniLabOS version: {unilabos.__version__}')" || echo Warning: Could not import unilabos
|
||||
echo Checking critical packages...
|
||||
python -c "import rclpy; print('ROS2 rclpy: OK')"
|
||||
mamba run -n unilab python -c "import rclpy; print('ROS2 rclpy: OK')"
|
||||
echo Running comprehensive verification script...
|
||||
python scripts\verify_installation.py || echo Warning: Verification script reported issues
|
||||
mamba run -n unilab python scripts\verify_installation.py --auto-install || echo Warning: Verification script reported issues
|
||||
echo Environment verification complete!
|
||||
|
||||
- name: Verify environment integrity (Unix)
|
||||
@@ -195,20 +194,20 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Verifying Python version..."
|
||||
python -c "import sys; print(f'Python version: {sys.version}')"
|
||||
mamba run -n unilab python -c "import sys; print(f'Python version: {sys.version}')"
|
||||
echo "Verifying unilabos import..."
|
||||
python -c "import unilabos; print(f'UniLabOS version: {unilabos.__version__}')" || echo "Warning: Could not import unilabos"
|
||||
mamba run -n unilab python -c "import unilabos; print(f'UniLabOS version: {unilabos.__version__}')" || echo "Warning: Could not import unilabos"
|
||||
echo "Checking critical packages..."
|
||||
python -c "import rclpy; print('ROS2 rclpy: OK')"
|
||||
mamba run -n unilab python -c "import rclpy; print('ROS2 rclpy: OK')"
|
||||
echo "Running comprehensive verification script..."
|
||||
python scripts/verify_installation.py || echo "Warning: Verification script reported issues"
|
||||
mamba run -n unilab python scripts/verify_installation.py --auto-install || echo "Warning: Verification script reported issues"
|
||||
echo "Environment verification complete!"
|
||||
|
||||
- name: Pack conda environment (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo Packing unilab environment with conda-pack...
|
||||
conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
|
||||
mamba activate unilab && conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
|
||||
echo Pack file created:
|
||||
dir unilab-env-${{ matrix.platform }}.tar.gz
|
||||
|
||||
@@ -217,6 +216,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Packing unilab environment with conda-pack..."
|
||||
mamba install conda-pack -c conda-forge -y
|
||||
conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
|
||||
echo "Pack file created:"
|
||||
ls -lh unilab-env-${{ matrix.platform }}.tar.gz
|
||||
@@ -242,6 +242,10 @@ jobs:
|
||||
echo Adding: verify_installation.py
|
||||
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
|
||||
echo Creating: 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"
|
||||
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
|
||||
echo "Creating: README.txt"
|
||||
python scripts/create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package/README.txt
|
||||
@@ -283,46 +291,6 @@ jobs:
|
||||
ls -lh dist-package/
|
||||
echo ""
|
||||
|
||||
- name: Finalize Windows distribution package
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo ==========================================
|
||||
echo Windows distribution package ready
|
||||
echo.
|
||||
echo Package will be uploaded as artifact
|
||||
echo GitHub Actions will automatically create ZIP
|
||||
echo.
|
||||
echo Contents:
|
||||
dir /b dist-package
|
||||
echo.
|
||||
echo Users will download a ZIP containing:
|
||||
echo - install_unilab.bat
|
||||
echo - unilab-env-${{ matrix.platform }}.tar.gz
|
||||
echo - verify_installation.py
|
||||
echo - README.txt
|
||||
echo ==========================================
|
||||
|
||||
- name: Create Unix/Linux TAR.GZ archive
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo "Creating Unix/Linux TAR.GZ archive..."
|
||||
echo "Archive: unilab-pack-${{ matrix.platform }}.tar.gz"
|
||||
echo "Contents: install_unilab.sh + unilab-env-${{ matrix.platform }}.tar.gz + extras"
|
||||
tar -czf unilab-pack-${{ matrix.platform }}.tar.gz -C dist-package .
|
||||
echo "=========================================="
|
||||
|
||||
echo ""
|
||||
echo "Final package created:"
|
||||
ls -lh unilab-pack-*
|
||||
echo ""
|
||||
echo "Users can now:"
|
||||
echo " 1. Download unilab-pack-${{ matrix.platform }}.tar.gz"
|
||||
echo " 2. Extract it: tar -xzf unilab-pack-${{ matrix.platform }}.tar.gz"
|
||||
echo " 3. Run: bash install_unilab.sh"
|
||||
echo ""
|
||||
|
||||
- name: Upload distribution package
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -365,12 +333,8 @@ jobs:
|
||||
echo "Distribution package contents:"
|
||||
ls -lh dist-package/
|
||||
echo ""
|
||||
echo "Package size (tar.gz):"
|
||||
ls -lh unilab-pack-*.tar.gz
|
||||
echo ""
|
||||
echo "Artifact name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}"
|
||||
echo ""
|
||||
echo "After download:"
|
||||
echo " - Windows/macOS: Extract ZIP, then: tar -xzf unilab-pack-${{ matrix.platform }}.tar.gz"
|
||||
echo " - Linux: Extract ZIP (or download tar.gz directly), run install_unilab.sh"
|
||||
echo " install_unilab.sh"
|
||||
echo "=========================================="
|
||||
|
||||
43
.github/workflows/deploy-docs.yml
vendored
43
.github/workflows/deploy-docs.yml
vendored
@@ -39,24 +39,39 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python environment
|
||||
uses: actions/setup-python@v5
|
||||
- name: Setup Miniforge (with mamba)
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
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: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pandoc
|
||||
echo "Installing unilabos and dependencies to unilab environment..."
|
||||
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: |
|
||||
python -m pip install --upgrade pip
|
||||
# Install package in development mode to get version info
|
||||
pip install -e .
|
||||
# Install documentation dependencies
|
||||
pip install -r docs/requirements.txt
|
||||
echo "Uninstalling existing unilabos..."
|
||||
mamba run -n unilab pip uninstall unilabos -y || echo "unilabos not installed via pip"
|
||||
echo "Installing unilabos from source..."
|
||||
mamba run -n unilab pip install .
|
||||
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
|
||||
id: pages
|
||||
@@ -68,8 +83,8 @@ jobs:
|
||||
cd docs
|
||||
# Clean previous builds
|
||||
rm -rf _build
|
||||
# Build HTML documentation
|
||||
python -m sphinx -b html . _build/html -v
|
||||
# Build HTML documentation in conda environment
|
||||
mamba run -n unilab python -m sphinx -b html . _build/html -v
|
||||
|
||||
- name: Check build results
|
||||
run: |
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
使用以下命令启动模拟反应器:
|
||||
|
||||
```bash
|
||||
unilab -g test/experiments/mock_reactor.json --app_bridges ""
|
||||
unilab -g test/experiments/mock_reactor.json
|
||||
```
|
||||
|
||||
### 2. 执行抽真空和充气操作
|
||||
|
||||
@@ -23,7 +23,7 @@ extensions = [
|
||||
"myst_parser",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
|
||||
"sphinx_rtd_theme"
|
||||
"sphinx_rtd_theme",
|
||||
]
|
||||
|
||||
source_suffix = {
|
||||
|
||||
@@ -172,7 +172,7 @@ Examples:
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(readme_content)
|
||||
|
||||
print(f"✓ README.txt created: {output_path}")
|
||||
print(f" README.txt created: {output_path}")
|
||||
print(f" Platform: {args.platform}")
|
||||
print(f" Branch: {args.branch}")
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@ This script verifies that UniLabOS and its dependencies are correctly installed.
|
||||
Run this script after installing the conda-pack environment to ensure everything works.
|
||||
|
||||
Usage:
|
||||
python verify_installation.py
|
||||
python verify_installation.py [--auto-install]
|
||||
|
||||
Options:
|
||||
--auto-install Automatically install missing packages
|
||||
|
||||
Or in the conda environment:
|
||||
conda activate unilab
|
||||
@@ -17,14 +20,15 @@ Usage:
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
|
||||
# IMPORTANT: Set UTF-8 encoding BEFORE any other imports
|
||||
# This ensures all subsequent imports (including unilabos) can output UTF-8 characters
|
||||
if sys.platform == "win32":
|
||||
# Method 1: Reconfigure stdout/stderr to use UTF-8 with error handling
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore
|
||||
sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
@@ -49,7 +53,7 @@ CHECK_MARK = "[OK]"
|
||||
CROSS_MARK = "[FAIL]"
|
||||
|
||||
|
||||
def check_package(package_name: str, display_name: str = None) -> bool:
|
||||
def check_package(package_name: str, display_name: str | None = None) -> bool:
|
||||
"""
|
||||
Check if a package can be imported.
|
||||
|
||||
@@ -87,9 +91,25 @@ def check_python_version() -> bool:
|
||||
|
||||
def main():
|
||||
"""Run all verification checks."""
|
||||
# Parse command line arguments
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Verify UniLabOS installation",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--auto-install",
|
||||
action="store_true",
|
||||
help="Automatically install missing packages",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 60)
|
||||
print("UniLabOS Installation Verification")
|
||||
print("=" * 60)
|
||||
if args.auto_install:
|
||||
print("Mode: Auto-install missing packages")
|
||||
else:
|
||||
print("Mode: Verification only")
|
||||
print()
|
||||
|
||||
all_passed = True
|
||||
@@ -113,14 +133,16 @@ def main():
|
||||
|
||||
print(f" {CHECK_MARK} UniLabOS installed")
|
||||
|
||||
# Check environment without auto-install (verification only)
|
||||
# Check environment with optional auto-install
|
||||
# Set show_details=False to suppress detailed Chinese output that may cause encoding issues
|
||||
env_check_passed = check_environment(auto_install=False, show_details=False)
|
||||
env_check_passed = check_environment(auto_install=args.auto_install, show_details=False)
|
||||
|
||||
if env_check_passed:
|
||||
print(f" {CHECK_MARK} All required packages available")
|
||||
else:
|
||||
print(f" {CROSS_MARK} Some optional packages are missing")
|
||||
if not args.auto_install:
|
||||
print(" Hint: Run with --auto-install to automatically install missing packages")
|
||||
except ImportError:
|
||||
print(f" {CROSS_MARK} UniLabOS not installed")
|
||||
all_passed = False
|
||||
|
||||
@@ -170,15 +170,16 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
"max_volume": 1000.0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"size_x": 200,
|
||||
"size_y": 150,
|
||||
"size_z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquids": [
|
||||
{
|
||||
"liquid_type": "DMF",
|
||||
"liquid_volume": 1000.0
|
||||
}
|
||||
]
|
||||
"liquids": [["DMF", 500.0]],
|
||||
"pending_liquids": [["DMF", 500.0]]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -194,15 +195,16 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
"max_volume": 1000.0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"size_x": 200,
|
||||
"size_y": 150,
|
||||
"size_z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquids": [
|
||||
{
|
||||
"liquid_type": "ethyl_acetate",
|
||||
"liquid_volume": 1000.0
|
||||
}
|
||||
]
|
||||
"liquids": [["ethyl_acetate", 1000.0]],
|
||||
"pending_liquids": [["ethyl_acetate", 1000.0]]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -218,15 +220,16 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
"max_volume": 1000.0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"size_x": 300,
|
||||
"size_y": 150,
|
||||
"size_z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquids": [
|
||||
{
|
||||
"liquid_type": "hexane",
|
||||
"liquid_volume": 1000.0
|
||||
}
|
||||
]
|
||||
"liquids": [["hexane", 1000.0]],
|
||||
"pending_liquids": [["hexane", 1000.0]]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -242,15 +245,16 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
"max_volume": 1000.0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"size_x": 900,
|
||||
"size_y": 150,
|
||||
"size_z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquids": [
|
||||
{
|
||||
"liquid_type": "methanol",
|
||||
"liquid_volume": 1000.0
|
||||
}
|
||||
]
|
||||
"liquids": [["methanol", 1000.0]],
|
||||
"pending_liquids": [["methanol", 1000.0]]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -266,15 +270,16 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
"max_volume": 1000.0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"size_x": 950,
|
||||
"size_y": 150,
|
||||
"size_z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquids": [
|
||||
{
|
||||
"liquid_type": "water",
|
||||
"liquid_volume": 1000.0
|
||||
}
|
||||
]
|
||||
"liquids": [["water", 1000.0]],
|
||||
"pending_liquids": [["water", 1000.0]]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -335,14 +340,16 @@
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 500.0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"max_temp": 200.0,
|
||||
"min_temp": -20.0,
|
||||
"has_stirrer": true,
|
||||
"has_heater": true
|
||||
},
|
||||
"data": {
|
||||
"liquids": [
|
||||
]
|
||||
"liquids": [],
|
||||
"pending_liquids": []
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -419,11 +426,16 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
"max_volume": 2000.0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"size_x": 500,
|
||||
"size_y": 400,
|
||||
"size_z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquids": [
|
||||
]
|
||||
"liquids": [],
|
||||
"pending_liquids": []
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -439,11 +451,16 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
"max_volume": 2000.0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"size_x": 1100,
|
||||
"size_y": 500,
|
||||
"size_z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquids": [
|
||||
]
|
||||
"liquids": [],
|
||||
"pending_liquids": []
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -649,11 +666,16 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 250.0
|
||||
"max_volume": 250.0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"size_x": 900,
|
||||
"size_y": 500,
|
||||
"size_z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquids": [
|
||||
]
|
||||
"liquids": [],
|
||||
"pending_liquids": []
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -669,11 +691,16 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 250.0
|
||||
"max_volume": 250.0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"size_x": 950,
|
||||
"size_y": 500,
|
||||
"size_z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquids": [
|
||||
]
|
||||
"liquids": [],
|
||||
"pending_liquids": []
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -689,11 +716,16 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 250.0
|
||||
"max_volume": 250.0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"size_x": 1050,
|
||||
"size_y": 500,
|
||||
"size_z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquids": [
|
||||
]
|
||||
"liquids": [],
|
||||
"pending_liquids": []
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -733,6 +765,11 @@
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 500.0,
|
||||
"size_x": 550,
|
||||
"size_y": 250,
|
||||
"size_z": 0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"reagent": "sodium_chloride",
|
||||
"physical_state": "solid"
|
||||
},
|
||||
@@ -756,6 +793,11 @@
|
||||
},
|
||||
"config": {
|
||||
"volume": 500.0,
|
||||
"size_x": 600,
|
||||
"size_y": 250,
|
||||
"size_z": 0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"reagent": "sodium_carbonate",
|
||||
"physical_state": "solid"
|
||||
},
|
||||
@@ -779,6 +821,11 @@
|
||||
},
|
||||
"config": {
|
||||
"volume": 500.0,
|
||||
"size_x": 650,
|
||||
"size_y": 250,
|
||||
"size_z": 0,
|
||||
"type": "RegularContainer",
|
||||
"category": "container",
|
||||
"reagent": "magnesium_chloride",
|
||||
"physical_state": "solid"
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "dispensing_station.bioyond",
|
||||
"class": "workstation.bioyond_dispensing_station",
|
||||
"config": {
|
||||
"config": {
|
||||
"api_key": "DE9BDDA0",
|
||||
@@ -20,13 +20,6 @@
|
||||
"_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": []
|
||||
},
|
||||
"data": {}
|
||||
@@ -57,4 +50,4 @@
|
||||
"data": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,13 @@
|
||||
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
||||
},
|
||||
"material_type_mappings": {
|
||||
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
|
||||
"烧杯": ["BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"],
|
||||
"试剂瓶": ["BIOYOND_PolymerStation_1BottleCarrier", ""],
|
||||
"样品板": ["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": {
|
||||
@@ -42,7 +46,6 @@
|
||||
{
|
||||
"id": "Bioyond_Deck",
|
||||
"name": "Bioyond_Deck",
|
||||
"sample_id": null,
|
||||
"children": [
|
||||
],
|
||||
"parent": "reaction_station_bioyond",
|
||||
|
||||
@@ -12,23 +12,13 @@ lab_registry.setup()
|
||||
|
||||
|
||||
type_mapping = {
|
||||
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||
"样品板": "BIOYOND_PolymerStation_6StockCarrier",
|
||||
"分装板": "BIOYOND_PolymerStation_6VialCarrier",
|
||||
"样品瓶": "BIOYOND_PolymerStation_Solid_Stock",
|
||||
"90%分装小瓶": "BIOYOND_PolymerStation_Solid_Vial",
|
||||
"10%分装小瓶": "BIOYOND_PolymerStation_Liquid_Vial",
|
||||
}
|
||||
|
||||
type_uuid_mapping = {
|
||||
"烧杯": "",
|
||||
"试剂瓶": "",
|
||||
"样品板": "",
|
||||
"分装板": "3a14196e-5dfe-6e21-0c79-fe2036d052c4",
|
||||
"样品瓶": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
|
||||
"90%分装小瓶": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
|
||||
"10%分装小瓶": "3a14196c-76be-2279-4e22-7310d69aed68",
|
||||
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
|
||||
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
|
||||
"样品板": ("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"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
115
test/resources/test_itemized_carrier.py
Normal file
115
test/resources/test_itemized_carrier.py
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试修改后的 get_child_identifier 函数
|
||||
"""
|
||||
|
||||
from unilabos.resources.itemized_carrier import ItemizedCarrier, Bottle
|
||||
from pylabrobot.resources.coordinate import Coordinate
|
||||
|
||||
def test_get_child_identifier_with_indices():
|
||||
"""测试返回x,y,z索引的 get_child_identifier 函数"""
|
||||
|
||||
# 创建一些测试瓶子
|
||||
bottle1 = Bottle("bottle1", diameter=25.0, height=50.0, max_volume=15.0)
|
||||
bottle1.location = Coordinate(10, 20, 5)
|
||||
|
||||
bottle2 = Bottle("bottle2", diameter=25.0, height=50.0, max_volume=15.0)
|
||||
bottle2.location = Coordinate(50, 20, 5)
|
||||
|
||||
bottle3 = Bottle("bottle3", diameter=25.0, height=50.0, max_volume=15.0)
|
||||
bottle3.location = Coordinate(90, 20, 5)
|
||||
|
||||
# 创建载架,指定维度
|
||||
sites = {
|
||||
"A1": bottle1,
|
||||
"A2": bottle2,
|
||||
"A3": bottle3,
|
||||
"B1": None, # 空位
|
||||
"B2": None,
|
||||
"B3": None
|
||||
}
|
||||
|
||||
carrier = ItemizedCarrier(
|
||||
name="test_carrier",
|
||||
size_x=150,
|
||||
size_y=100,
|
||||
size_z=30,
|
||||
num_items_x=3, # 3列
|
||||
num_items_y=2, # 2行
|
||||
num_items_z=1, # 1层
|
||||
sites=sites
|
||||
)
|
||||
|
||||
print("测试载架维度:")
|
||||
print(f"num_items_x: {carrier.num_items_x}")
|
||||
print(f"num_items_y: {carrier.num_items_y}")
|
||||
print(f"num_items_z: {carrier.num_items_z}")
|
||||
print()
|
||||
|
||||
# 测试获取bottle1的标识符信息 (A1 = idx:0, x:0, y:0, z:0)
|
||||
result1 = carrier.get_child_identifier(bottle1)
|
||||
print("测试bottle1 (A1):")
|
||||
print(f" identifier: {result1['identifier']}")
|
||||
print(f" idx: {result1['idx']}")
|
||||
print(f" x index: {result1['x']}")
|
||||
print(f" y index: {result1['y']}")
|
||||
print(f" z index: {result1['z']}")
|
||||
|
||||
# Assert 验证 bottle1 (A1) 的结果
|
||||
assert result1['identifier'] == 'A1', f"Expected identifier 'A1', got '{result1['identifier']}'"
|
||||
assert result1['idx'] == 0, f"Expected idx 0, got {result1['idx']}"
|
||||
assert result1['x'] == 0, f"Expected x index 0, got {result1['x']}"
|
||||
assert result1['y'] == 0, f"Expected y index 0, got {result1['y']}"
|
||||
assert result1['z'] == 0, f"Expected z index 0, got {result1['z']}"
|
||||
print(" ✓ bottle1 (A1) 测试通过")
|
||||
print()
|
||||
|
||||
# 测试获取bottle2的标识符信息 (A2 = idx:1, x:1, y:0, z:0)
|
||||
result2 = carrier.get_child_identifier(bottle2)
|
||||
print("测试bottle2 (A2):")
|
||||
print(f" identifier: {result2['identifier']}")
|
||||
print(f" idx: {result2['idx']}")
|
||||
print(f" x index: {result2['x']}")
|
||||
print(f" y index: {result2['y']}")
|
||||
print(f" z index: {result2['z']}")
|
||||
|
||||
# Assert 验证 bottle2 (A2) 的结果
|
||||
assert result2['identifier'] == 'A2', f"Expected identifier 'A2', got '{result2['identifier']}'"
|
||||
assert result2['idx'] == 1, f"Expected idx 1, got {result2['idx']}"
|
||||
assert result2['x'] == 1, f"Expected x index 1, got {result2['x']}"
|
||||
assert result2['y'] == 0, f"Expected y index 0, got {result2['y']}"
|
||||
assert result2['z'] == 0, f"Expected z index 0, got {result2['z']}"
|
||||
print(" ✓ bottle2 (A2) 测试通过")
|
||||
print()
|
||||
|
||||
# 测试获取bottle3的标识符信息 (A3 = idx:2, x:2, y:0, z:0)
|
||||
result3 = carrier.get_child_identifier(bottle3)
|
||||
print("测试bottle3 (A3):")
|
||||
print(f" identifier: {result3['identifier']}")
|
||||
print(f" idx: {result3['idx']}")
|
||||
print(f" x index: {result3['x']}")
|
||||
print(f" y index: {result3['y']}")
|
||||
print(f" z index: {result3['z']}")
|
||||
|
||||
# Assert 验证 bottle3 (A3) 的结果
|
||||
assert result3['identifier'] == 'A3', f"Expected identifier 'A3', got '{result3['identifier']}'"
|
||||
assert result3['idx'] == 2, f"Expected idx 2, got {result3['idx']}"
|
||||
assert result3['x'] == 2, f"Expected x index 2, got {result3['x']}"
|
||||
assert result3['y'] == 0, f"Expected y index 0, got {result3['y']}"
|
||||
assert result3['z'] == 0, f"Expected z index 0, got {result3['z']}"
|
||||
print(" ✓ bottle3 (A3) 测试通过")
|
||||
print()
|
||||
|
||||
# 测试错误情况:查找不存在的资源
|
||||
bottle_not_exists = Bottle("bottle_not_exists", diameter=25.0, height=50.0, max_volume=15.0)
|
||||
try:
|
||||
carrier.get_child_identifier(bottle_not_exists)
|
||||
assert False, "应该抛出 ValueError 异常"
|
||||
except ValueError as e:
|
||||
print("✓ 正确抛出了 ValueError 异常:", str(e))
|
||||
assert "is not assigned to this carrier" in str(e), "异常消息应该包含预期的文本"
|
||||
|
||||
print("\n🎉 所有测试都通过了!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_get_child_identifier_with_indices()
|
||||
@@ -11,18 +11,14 @@ from typing import Dict, Any, List
|
||||
import networkx as nx
|
||||
import yaml
|
||||
|
||||
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet, ResourceDict
|
||||
|
||||
# 首先添加项目根目录到路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||
if unilabos_dir not in sys.path:
|
||||
sys.path.append(unilabos_dir)
|
||||
|
||||
from unilabos.config.config import load_config, BasicConfig, HTTPConfig
|
||||
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
||||
from unilabos.resources.graphio import modify_to_backend_format
|
||||
|
||||
from unilabos.config.config import load_config, BasicConfig, HTTPConfig
|
||||
|
||||
def load_config_from_file(config_path):
|
||||
if config_path is None:
|
||||
@@ -184,6 +180,7 @@ def main():
|
||||
working_dir = os.path.abspath(os.getcwd())
|
||||
else:
|
||||
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||
|
||||
if args_dict.get("working_dir"):
|
||||
working_dir = args_dict.get("working_dir", "")
|
||||
if config_path and not os.path.exists(config_path):
|
||||
@@ -215,6 +212,14 @@ def main():
|
||||
# 加载配置文件
|
||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||
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":
|
||||
print_status("使用测试环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
||||
@@ -268,6 +273,8 @@ def main():
|
||||
from unilabos.app.web import http_client
|
||||
from unilabos.app.web import start_server
|
||||
from unilabos.app.register import register_devices_and_resources
|
||||
from unilabos.resources.graphio import modify_to_backend_format
|
||||
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet, ResourceDict
|
||||
|
||||
# 显示启动横幅
|
||||
print_unilab_banner(args_dict)
|
||||
|
||||
@@ -73,6 +73,8 @@ class HTTPClient:
|
||||
Returns:
|
||||
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)
|
||||
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
|
||||
if not self.initialized or first_add:
|
||||
@@ -92,6 +94,8 @@ class HTTPClient:
|
||||
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_mapping = {}
|
||||
if response.status_code == 200:
|
||||
|
||||
@@ -2,7 +2,7 @@ import base64
|
||||
import traceback
|
||||
import os
|
||||
import importlib.util
|
||||
from typing import Optional
|
||||
from typing import Optional, Literal
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ class BasicConfig:
|
||||
vis_2d_enable = False
|
||||
enable_resource_load = True
|
||||
communication_protocol = "websocket"
|
||||
log_level: Literal['TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = "DEBUG" # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
||||
|
||||
@classmethod
|
||||
def auth_secret(cls):
|
||||
|
||||
@@ -37,7 +37,7 @@ def _initialize_material_system(self, deck_config: Dict[str, Any], children_conf
|
||||
**定义在**: `workstation_base.py`
|
||||
|
||||
**设计目的**:
|
||||
- 提供外部物料系统(如Bioyong、LIMS等)集成的标准接口
|
||||
- 提供外部物料系统(如Bioyond、LIMS等)集成的标准接口
|
||||
- 双向同步:从外部系统同步到本地deck,以及将本地变更同步到外部系统
|
||||
- 处理外部系统的变更通知
|
||||
|
||||
@@ -59,7 +59,7 @@ async def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
||||
**扩展功能**:
|
||||
- HTTP报送接收服务集成
|
||||
- 具体工作流实现(液体转移、板洗等)
|
||||
- Bioyong物料系统同步器示例
|
||||
- Bioyond物料系统同步器示例
|
||||
- 外部报送处理方法
|
||||
|
||||
## 技术栈
|
||||
@@ -142,11 +142,11 @@ success = workstation.execute_workflow("liquid_transfer", {
|
||||
### 3. 外部系统集成
|
||||
|
||||
```python
|
||||
class BioyongResourceSynchronizer(ResourceSynchronizer):
|
||||
"""Bioyong系统同步器"""
|
||||
class BioyondResourceSynchronizer(ResourceSynchronizer):
|
||||
"""Bioyond系统同步器"""
|
||||
|
||||
async def sync_from_external(self) -> bool:
|
||||
# 从Bioyong API获取物料
|
||||
# 从Bioyond API获取物料
|
||||
external_materials = await self._fetch_bioyong_materials()
|
||||
|
||||
# 转换并添加到本地deck
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,22 +9,6 @@ API_CONFIG = {
|
||||
"api_host": ""
|
||||
}
|
||||
|
||||
# 站点类型配置
|
||||
STATION_TYPES = {
|
||||
"REACTION": "reaction_station", # 仅反应站
|
||||
"DISPENSING": "dispensing_station", # 仅配液站
|
||||
"HYBRID": "hybrid_station" # 混合模式
|
||||
}
|
||||
|
||||
# 默认站点配置
|
||||
DEFAULT_STATION_CONFIG = {
|
||||
"station_type": STATION_TYPES["REACTION"], # 默认反应站模式
|
||||
"enable_reaction_station": True, # 是否启用反应站功能
|
||||
"enable_dispensing_station": False, # 是否启用配液站功能
|
||||
"station_name": "BioyondReactionStation", # 站点名称
|
||||
"description": "Bioyond反应工作站" # 站点描述
|
||||
}
|
||||
|
||||
# 工作流映射配置
|
||||
WORKFLOW_MAPPINGS = {
|
||||
"reactor_taken_out": "",
|
||||
@@ -49,52 +33,75 @@ WORKFLOW_TO_SECTION_MAP = {
|
||||
}
|
||||
|
||||
# 库位映射配置
|
||||
LOCATION_MAPPING = {
|
||||
'A01': '',
|
||||
'A02': '',
|
||||
'A03': '',
|
||||
'A04': '',
|
||||
'A05': '',
|
||||
'A06': '',
|
||||
'A07': '',
|
||||
'A08': '',
|
||||
'B01': '',
|
||||
'B02': '',
|
||||
'B03': '',
|
||||
'B04': '',
|
||||
'B05': '',
|
||||
'B06': '',
|
||||
'B07': '',
|
||||
'B08': '',
|
||||
'C01': '',
|
||||
'C02': '',
|
||||
'C03': '',
|
||||
'C04': '',
|
||||
'C05': '',
|
||||
'C06': '',
|
||||
'C07': '',
|
||||
'C08': '',
|
||||
'D01': '',
|
||||
'D02': '',
|
||||
'D03': '',
|
||||
'D04': '',
|
||||
'D05': '',
|
||||
'D06': '',
|
||||
'D07': '',
|
||||
'D08': '',
|
||||
WAREHOUSE_MAPPING = {
|
||||
"粉末堆栈": {
|
||||
"uuid": "",
|
||||
"site_uuids": {
|
||||
# 样品板
|
||||
"A1": "3a14198e-6929-31f0-8a22-0f98f72260df",
|
||||
"A2": "3a14198e-6929-4379-affa-9a2935c17f99",
|
||||
"A3": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af",
|
||||
"A4": "3a14198e-6929-5e99-2b79-80720f7cfb54",
|
||||
"B1": "3a14198e-6929-f525-9a1b-1857552b28ee",
|
||||
"B2": "3a14198e-6929-bf98-0fd5-26e1d68bf62d",
|
||||
"B3": "3a14198e-6929-2d86-a468-602175a2b5aa",
|
||||
"B4": "3a14198e-6929-1a98-ae57-e97660c489ad",
|
||||
# 分装板
|
||||
"C1": "3a14198e-6929-46fe-841e-03dd753f1e4a",
|
||||
"C2": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95",
|
||||
"C3": "3a14198e-6929-72ac-32ce-9b50245682b8",
|
||||
"C4": "3a14198e-6929-3bd8-e6c7-4a9fd93be118",
|
||||
"D1": "3a14198e-6929-8a0b-b686-6f4a2955c4e2",
|
||||
"D2": "3a14198e-6929-dde1-fc78-34a84b71afdf",
|
||||
"D3": "3a14198e-6929-a0ec-5f15-c0f9f339f963",
|
||||
"D4": "3a14198e-6929-7ac8-915a-fea51cb2e884"
|
||||
}
|
||||
},
|
||||
"溶液堆栈": {
|
||||
"uuid": "",
|
||||
"site_uuids": {
|
||||
"A1": "3a14198e-d724-e036-afdc-2ae39a7f3383",
|
||||
"A2": "3a14198e-d724-afa4-fc82-0ac8a9016791",
|
||||
"A3": "3a14198e-d724-ca48-bb9e-7e85751e55b6",
|
||||
"A4": "3a14198e-d724-df6d-5e32-5483b3cab583",
|
||||
"B1": "3a14198e-d724-d818-6d4f-5725191a24b5",
|
||||
"B2": "3a14198e-d724-be8a-5e0b-012675e195c6",
|
||||
"B3": "3a14198e-d724-cc1e-5c2c-228a130f40a8",
|
||||
"B4": "3a14198e-d724-1e28-c885-574c3df468d0",
|
||||
"C1": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31",
|
||||
"C2": "3a14198e-d724-ab4e-48cb-817c3c146707",
|
||||
"C3": "3a14198e-d724-7f18-1853-39d0c62e1d33",
|
||||
"C4": "3a14198e-d724-28a2-a760-baa896f46b66",
|
||||
"D1": "3a14198e-d724-d378-d266-2508a224a19f",
|
||||
"D2": "3a14198e-d724-f56e-468b-0110a8feb36a",
|
||||
"D3": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c",
|
||||
"D4": "3a14198e-d724-0ddd-9654-f9352a421de9"
|
||||
}
|
||||
},
|
||||
"试剂堆栈": {
|
||||
"uuid": "",
|
||||
"site_uuids": {
|
||||
"A1": "3a14198c-c2cf-8b40-af28-b467808f1c36",
|
||||
"A2": "3a14198c-c2d0-f3e7-871a-e470d144296f",
|
||||
"A3": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094",
|
||||
"A4": "3a14198c-c2d0-2070-efc8-44e245f10c6f",
|
||||
"B1": "3a14198c-c2d0-354f-39ad-642e1a72fcb8",
|
||||
"B2": "3a14198c-c2d0-1559-105d-0ea30682cab4",
|
||||
"B3": "3a14198c-c2d0-725e-523d-34c037ac2440",
|
||||
"B4": "3a14198c-c2d0-efce-0939-69ca5a7dfd39"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 物料类型配置
|
||||
MATERIAL_TYPE_IDS = {
|
||||
"样品板": "",
|
||||
"样品": "",
|
||||
"烧杯": ""
|
||||
}
|
||||
|
||||
MATERIAL_TYPE_MAPPINGS = {
|
||||
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||
"样品板": "BIOYOND_PolymerStation_6VialCarrier",
|
||||
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
|
||||
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
|
||||
"样品板": ("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"),
|
||||
}
|
||||
|
||||
# 步骤参数配置(各工作流的步骤UUID)
|
||||
@@ -127,3 +134,5 @@ WORKFLOW_STEP_IDS = {
|
||||
"observe": ""
|
||||
}
|
||||
}
|
||||
|
||||
LOCATION_MAPPING = {}
|
||||
@@ -0,0 +1,831 @@
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
|
||||
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
||||
|
||||
|
||||
class BioyondDispensingStation(BioyondWorkstation):
|
||||
def __init__(
|
||||
self,
|
||||
config,
|
||||
# 桌子
|
||||
deck,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(config, deck, *args, **kwargs)
|
||||
# self.config = config
|
||||
# self.api_key = config["api_key"]
|
||||
# self.host = config["api_host"]
|
||||
#
|
||||
# # 使用简单的Logger替代原来的logger
|
||||
# self._logger = SimpleLogger()
|
||||
# self.is_running = False
|
||||
|
||||
# 90%10%小瓶投料任务创建方法
|
||||
def create_90_10_vial_feeding_task(self,
|
||||
order_name: str = None,
|
||||
speed: str = None,
|
||||
temperature: str = None,
|
||||
delay_time: str = None,
|
||||
percent_90_1_assign_material_name: str = None,
|
||||
percent_90_1_target_weigh: str = None,
|
||||
percent_90_2_assign_material_name: str = None,
|
||||
percent_90_2_target_weigh: str = None,
|
||||
percent_90_3_assign_material_name: str = None,
|
||||
percent_90_3_target_weigh: str = None,
|
||||
percent_10_1_assign_material_name: str = None,
|
||||
percent_10_1_target_weigh: str = None,
|
||||
percent_10_1_volume: str = None,
|
||||
percent_10_1_liquid_material_name: str = None,
|
||||
percent_10_2_assign_material_name: str = None,
|
||||
percent_10_2_target_weigh: str = None,
|
||||
percent_10_2_volume: str = None,
|
||||
percent_10_2_liquid_material_name: str = None,
|
||||
percent_10_3_assign_material_name: str = None,
|
||||
percent_10_3_target_weigh: str = None,
|
||||
percent_10_3_volume: str = None,
|
||||
percent_10_3_liquid_material_name: str = None,
|
||||
hold_m_name: str = None) -> dict:
|
||||
"""
|
||||
创建90%10%小瓶投料任务
|
||||
|
||||
参数说明:
|
||||
- order_name: 任务名称,如果为None则使用默认名称
|
||||
- speed: 搅拌速度,如果为None则使用默认值400
|
||||
- temperature: 温度,如果为None则使用默认值40
|
||||
- delay_time: 延迟时间,如果为None则使用默认值600
|
||||
- percent_90_1_assign_material_name: 90%_1物料名称
|
||||
- percent_90_1_target_weigh: 90%_1目标重量
|
||||
- percent_90_2_assign_material_name: 90%_2物料名称
|
||||
- percent_90_2_target_weigh: 90%_2目标重量
|
||||
- percent_90_3_assign_material_name: 90%_3物料名称
|
||||
- percent_90_3_target_weigh: 90%_3目标重量
|
||||
- percent_10_1_assign_material_name: 10%_1固体物料名称
|
||||
- percent_10_1_target_weigh: 10%_1固体目标重量
|
||||
- percent_10_1_volume: 10%_1液体体积
|
||||
- percent_10_1_liquid_material_name: 10%_1液体物料名称
|
||||
- percent_10_2_assign_material_name: 10%_2固体物料名称
|
||||
- percent_10_2_target_weigh: 10%_2固体目标重量
|
||||
- percent_10_2_volume: 10%_2液体体积
|
||||
- percent_10_2_liquid_material_name: 10%_2液体物料名称
|
||||
- percent_10_3_assign_material_name: 10%_3固体物料名称
|
||||
- percent_10_3_target_weigh: 10%_3固体目标重量
|
||||
- percent_10_3_volume: 10%_3液体体积
|
||||
- percent_10_3_liquid_material_name: 10%_3液体物料名称
|
||||
- hold_m_name: 库位名称,如"C01",用于查找对应的holdMId
|
||||
|
||||
返回: 任务创建结果
|
||||
|
||||
异常:
|
||||
- BioyondException: 各种错误情况下的统一异常
|
||||
"""
|
||||
try:
|
||||
# 1. 参数验证
|
||||
if not hold_m_name:
|
||||
raise BioyondException("hold_m_name 是必填参数")
|
||||
|
||||
# 检查90%物料参数的完整性
|
||||
# 90%_1物料:如果有物料名称或目标重量,就必须有全部参数
|
||||
if percent_90_1_assign_material_name or percent_90_1_target_weigh:
|
||||
if not percent_90_1_assign_material_name:
|
||||
raise BioyondException("90%_1物料:如果提供了目标重量,必须同时提供物料名称")
|
||||
if not percent_90_1_target_weigh:
|
||||
raise BioyondException("90%_1物料:如果提供了物料名称,必须同时提供目标重量")
|
||||
|
||||
# 90%_2物料:如果有物料名称或目标重量,就必须有全部参数
|
||||
if percent_90_2_assign_material_name or percent_90_2_target_weigh:
|
||||
if not percent_90_2_assign_material_name:
|
||||
raise BioyondException("90%_2物料:如果提供了目标重量,必须同时提供物料名称")
|
||||
if not percent_90_2_target_weigh:
|
||||
raise BioyondException("90%_2物料:如果提供了物料名称,必须同时提供目标重量")
|
||||
|
||||
# 90%_3物料:如果有物料名称或目标重量,就必须有全部参数
|
||||
if percent_90_3_assign_material_name or percent_90_3_target_weigh:
|
||||
if not percent_90_3_assign_material_name:
|
||||
raise BioyondException("90%_3物料:如果提供了目标重量,必须同时提供物料名称")
|
||||
if not percent_90_3_target_weigh:
|
||||
raise BioyondException("90%_3物料:如果提供了物料名称,必须同时提供目标重量")
|
||||
|
||||
# 检查10%物料参数的完整性
|
||||
# 10%_1物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数
|
||||
if any([percent_10_1_assign_material_name, percent_10_1_target_weigh, percent_10_1_volume, percent_10_1_liquid_material_name]):
|
||||
if not percent_10_1_assign_material_name:
|
||||
raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供固体物料名称")
|
||||
if not percent_10_1_target_weigh:
|
||||
raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供固体目标重量")
|
||||
if not percent_10_1_volume:
|
||||
raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供液体体积")
|
||||
if not percent_10_1_liquid_material_name:
|
||||
raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供液体物料名称")
|
||||
|
||||
# 10%_2物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数
|
||||
if any([percent_10_2_assign_material_name, percent_10_2_target_weigh, percent_10_2_volume, percent_10_2_liquid_material_name]):
|
||||
if not percent_10_2_assign_material_name:
|
||||
raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供固体物料名称")
|
||||
if not percent_10_2_target_weigh:
|
||||
raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供固体目标重量")
|
||||
if not percent_10_2_volume:
|
||||
raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供液体体积")
|
||||
if not percent_10_2_liquid_material_name:
|
||||
raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供液体物料名称")
|
||||
|
||||
# 10%_3物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数
|
||||
if any([percent_10_3_assign_material_name, percent_10_3_target_weigh, percent_10_3_volume, percent_10_3_liquid_material_name]):
|
||||
if not percent_10_3_assign_material_name:
|
||||
raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供固体物料名称")
|
||||
if not percent_10_3_target_weigh:
|
||||
raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供固体目标重量")
|
||||
if not percent_10_3_volume:
|
||||
raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供液体体积")
|
||||
if not percent_10_3_liquid_material_name:
|
||||
raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供液体物料名称")
|
||||
|
||||
# 2. 生成任务编码和设置默认值
|
||||
order_code = "task_vial_" + str(int(datetime.now().timestamp()))
|
||||
if order_name is None:
|
||||
order_name = "90%10%小瓶投料任务"
|
||||
if speed is None:
|
||||
speed = "400"
|
||||
if temperature is None:
|
||||
temperature = "40"
|
||||
if delay_time is None:
|
||||
delay_time = "600"
|
||||
|
||||
# 3. 工作流ID
|
||||
workflow_id = "3a19310d-16b9-9d81-b109-0748e953694b"
|
||||
|
||||
# 4. 查询工作流对应的holdMID
|
||||
material_info = self.hardware_interface.material_id_query(workflow_id)
|
||||
if not material_info:
|
||||
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
|
||||
|
||||
# 获取locations列表
|
||||
locations = material_info.get("locations", []) if isinstance(material_info, dict) else []
|
||||
if not locations:
|
||||
raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息")
|
||||
|
||||
# 查找指定名称的库位
|
||||
hold_mid = None
|
||||
for location in locations:
|
||||
if location.get("holdMName") == hold_m_name:
|
||||
hold_mid = location.get("holdMId")
|
||||
break
|
||||
|
||||
if not hold_mid:
|
||||
raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确")
|
||||
|
||||
extend_properties = f"{{\"{ hold_mid }\": {{}}}}"
|
||||
self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}")
|
||||
|
||||
# 5. 构建任务参数
|
||||
order_data = {
|
||||
"orderCode": order_code,
|
||||
"orderName": order_name,
|
||||
"workflowId": workflow_id,
|
||||
"borderNumber": 1,
|
||||
"paramValues": {},
|
||||
"ExtendProperties": extend_properties
|
||||
}
|
||||
|
||||
# 添加搅拌参数
|
||||
order_data["paramValues"]["e8264e47-c319-d9d9-8676-4dd5cb382b11"] = [
|
||||
{"m": 0, "n": 3, "Key": "speed", "Value": speed},
|
||||
{"m": 0, "n": 3, "Key": "temperature", "Value": temperature}
|
||||
]
|
||||
|
||||
# 添加延迟时间参数
|
||||
order_data["paramValues"]["dc5dba79-5e4b-8eae-cbc5-e93482e43b1f"] = [
|
||||
{"m": 0, "n": 4, "Key": "DelayTime", "Value": delay_time}
|
||||
]
|
||||
|
||||
# 添加90%_1参数
|
||||
if percent_90_1_assign_material_name is not None and percent_90_1_target_weigh is not None:
|
||||
order_data["paramValues"]["e7d3c0a3-25c2-c42d-c84b-860c4a5ef844"] = [
|
||||
{"m": 15, "n": 1, "Key": "targetWeigh", "Value": percent_90_1_target_weigh},
|
||||
{"m": 15, "n": 1, "Key": "assignMaterialName", "Value": percent_90_1_assign_material_name}
|
||||
]
|
||||
|
||||
# 添加90%_2参数
|
||||
if percent_90_2_assign_material_name is not None and percent_90_2_target_weigh is not None:
|
||||
order_data["paramValues"]["50b912c4-6c81-0734-1c8b-532428b2a4a5"] = [
|
||||
{"m": 18, "n": 1, "Key": "targetWeigh", "Value": percent_90_2_target_weigh},
|
||||
{"m": 18, "n": 1, "Key": "assignMaterialName", "Value": percent_90_2_assign_material_name}
|
||||
]
|
||||
|
||||
# 添加90%_3参数
|
||||
if percent_90_3_assign_material_name is not None and percent_90_3_target_weigh is not None:
|
||||
order_data["paramValues"]["9c3674b3-c7cb-946e-fa03-fa2861d8aec4"] = [
|
||||
{"m": 21, "n": 1, "Key": "targetWeigh", "Value": percent_90_3_target_weigh},
|
||||
{"m": 21, "n": 1, "Key": "assignMaterialName", "Value": percent_90_3_assign_material_name}
|
||||
]
|
||||
|
||||
# 添加10%_1固体参数
|
||||
if percent_10_1_assign_material_name is not None and percent_10_1_target_weigh is not None:
|
||||
order_data["paramValues"]["73a0bfd8-1967-45e9-4bab-c07ccd1a2727"] = [
|
||||
{"m": 3, "n": 1, "Key": "targetWeigh", "Value": percent_10_1_target_weigh},
|
||||
{"m": 3, "n": 1, "Key": "assignMaterialName", "Value": percent_10_1_assign_material_name}
|
||||
]
|
||||
|
||||
# 添加10%_1液体参数
|
||||
if percent_10_1_liquid_material_name is not None and percent_10_1_volume is not None:
|
||||
order_data["paramValues"]["39634d40-c623-473a-8e5f-bc301aca2522"] = [
|
||||
{"m": 3, "n": 3, "Key": "volume", "Value": percent_10_1_volume},
|
||||
{"m": 3, "n": 3, "Key": "assignMaterialName", "Value": percent_10_1_liquid_material_name}
|
||||
]
|
||||
|
||||
# 添加10%_2固体参数
|
||||
if percent_10_2_assign_material_name is not None and percent_10_2_target_weigh is not None:
|
||||
order_data["paramValues"]["2d9c16fa-2a19-cd47-a67b-3cadff9e3e3d"] = [
|
||||
{"m": 7, "n": 1, "Key": "targetWeigh", "Value": percent_10_2_target_weigh},
|
||||
{"m": 7, "n": 1, "Key": "assignMaterialName", "Value": percent_10_2_assign_material_name}
|
||||
]
|
||||
|
||||
# 添加10%_2液体参数
|
||||
if percent_10_2_liquid_material_name is not None and percent_10_2_volume is not None:
|
||||
order_data["paramValues"]["e60541bb-ed68-e839-7305-2b4abe38a13d"] = [
|
||||
{"m": 7, "n": 3, "Key": "volume", "Value": percent_10_2_volume},
|
||||
{"m": 7, "n": 3, "Key": "assignMaterialName", "Value": percent_10_2_liquid_material_name}
|
||||
]
|
||||
|
||||
# 添加10%_3固体参数
|
||||
if percent_10_3_assign_material_name is not None and percent_10_3_target_weigh is not None:
|
||||
order_data["paramValues"]["27494733-0f71-a916-7cd2-1929a0125f17"] = [
|
||||
{"m": 11, "n": 1, "Key": "targetWeigh", "Value": percent_10_3_target_weigh},
|
||||
{"m": 11, "n": 1, "Key": "assignMaterialName", "Value": percent_10_3_assign_material_name}
|
||||
]
|
||||
|
||||
# 添加10%_3液体参数
|
||||
if percent_10_3_liquid_material_name is not None and percent_10_3_volume is not None:
|
||||
order_data["paramValues"]["c8798c29-786f-6858-7d7f-5330b890f2a6"] = [
|
||||
{"m": 11, "n": 3, "Key": "volume", "Value": percent_10_3_volume},
|
||||
{"m": 11, "n": 3, "Key": "assignMaterialName", "Value": percent_10_3_liquid_material_name}
|
||||
]
|
||||
|
||||
# 6. 转换为JSON字符串并创建任务
|
||||
json_str = json.dumps([order_data], ensure_ascii=False)
|
||||
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务参数: {json_str}")
|
||||
|
||||
# 7. 调用create_order方法创建任务
|
||||
result = self.hardware_interface.create_order(json_str)
|
||||
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}")
|
||||
return json.dumps({"suc": True})
|
||||
|
||||
except BioyondException:
|
||||
# 重新抛出BioyondException
|
||||
raise
|
||||
except Exception as e:
|
||||
# 捕获其他未预期的异常,转换为BioyondException
|
||||
error_msg = f"创建90%10%小瓶投料任务时发生未预期的错误: {str(e)}"
|
||||
self.hardware_interface._logger.error(error_msg)
|
||||
raise BioyondException(error_msg)
|
||||
|
||||
# 二胺溶液配置任务创建方法
|
||||
def create_diamine_solution_task(self,
|
||||
order_name: str = None,
|
||||
material_name: str = None,
|
||||
target_weigh: str = None,
|
||||
volume: str = None,
|
||||
liquid_material_name: str = "NMP",
|
||||
speed: str = None,
|
||||
temperature: str = None,
|
||||
delay_time: str = None,
|
||||
hold_m_name: str = None) -> dict:
|
||||
"""
|
||||
创建二胺溶液配置任务
|
||||
|
||||
参数说明:
|
||||
- order_name: 任务名称,如果为None则使用默认名称
|
||||
- material_name: 固体物料名称,必填
|
||||
- target_weigh: 固体目标重量,必填
|
||||
- volume: 液体体积,必填
|
||||
- liquid_material_name: 液体物料名称,默认为NMP
|
||||
- speed: 搅拌速度,如果为None则使用默认值400
|
||||
- temperature: 温度,如果为None则使用默认值20
|
||||
- delay_time: 延迟时间,如果为None则使用默认值600
|
||||
- hold_m_name: 库位名称,如"ODA-1",用于查找对应的holdMId
|
||||
|
||||
返回: 任务创建结果
|
||||
|
||||
异常:
|
||||
- BioyondException: 各种错误情况下的统一异常
|
||||
"""
|
||||
try:
|
||||
# 1. 参数验证
|
||||
if not material_name:
|
||||
raise BioyondException("material_name 是必填参数")
|
||||
if not target_weigh:
|
||||
raise BioyondException("target_weigh 是必填参数")
|
||||
if not volume:
|
||||
raise BioyondException("volume 是必填参数")
|
||||
if not hold_m_name:
|
||||
raise BioyondException("hold_m_name 是必填参数")
|
||||
|
||||
|
||||
# 2. 生成任务编码和设置默认值
|
||||
order_code = "task_oda_" + str(int(datetime.now().timestamp()))
|
||||
if order_name is None:
|
||||
order_name = f"二胺溶液配置-{material_name}"
|
||||
if speed is None:
|
||||
speed = "400"
|
||||
if temperature is None:
|
||||
temperature = "20"
|
||||
if delay_time is None:
|
||||
delay_time = "600"
|
||||
|
||||
# 3. 工作流ID - 二胺溶液配置工作流
|
||||
workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5"
|
||||
|
||||
# 4. 查询工作流对应的holdMID
|
||||
material_info = self.material_id_query(workflow_id)
|
||||
if not material_info:
|
||||
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
|
||||
|
||||
# 获取locations列表
|
||||
locations = material_info.get("locations", []) if isinstance(material_info, dict) else []
|
||||
if not locations:
|
||||
raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息")
|
||||
|
||||
# 查找指定名称的库位
|
||||
hold_mid = None
|
||||
for location in locations:
|
||||
if location.get("holdMName") == hold_m_name:
|
||||
hold_mid = location.get("holdMId")
|
||||
break
|
||||
|
||||
if not hold_mid:
|
||||
raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确")
|
||||
|
||||
extend_properties = f"{{\"{ hold_mid }\": {{}}}}"
|
||||
self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}")
|
||||
|
||||
# 5. 构建任务参数
|
||||
order_data = {
|
||||
"orderCode": order_code,
|
||||
"orderName": order_name,
|
||||
"workflowId": workflow_id,
|
||||
"borderNumber": 1,
|
||||
"paramValues": {
|
||||
# 固体物料参数
|
||||
"3a15d4a1-3bde-f5bc-053f-1ae0bf1f357e": [
|
||||
{"m": 3, "n": 2, "Key": "targetWeigh", "Value": target_weigh},
|
||||
{"m": 3, "n": 2, "Key": "assignMaterialName", "Value": material_name}
|
||||
],
|
||||
# 液体物料参数
|
||||
"3a15d4a1-3bde-d584-b309-e661ae8f1c01": [
|
||||
{"m": 3, "n": 3, "Key": "volume", "Value": volume},
|
||||
{"m": 3, "n": 3, "Key": "assignMaterialName", "Value": liquid_material_name}
|
||||
],
|
||||
# 搅拌参数
|
||||
"3a15d4a1-3bde-8ec4-1ced-92efc97ed73d": [
|
||||
{"m": 3, "n": 6, "Key": "speed", "Value": speed},
|
||||
{"m": 3, "n": 6, "Key": "temperature", "Value": temperature}
|
||||
],
|
||||
# 延迟时间参数
|
||||
"3a15d4a1-3bde-3b92-83ff-8923a0addbbc": [
|
||||
{"m": 3, "n": 7, "Key": "DelayTime", "Value": delay_time}
|
||||
]
|
||||
},
|
||||
"ExtendProperties": extend_properties
|
||||
}
|
||||
|
||||
# 6. 转换为JSON字符串并创建任务
|
||||
json_str = json.dumps([order_data], ensure_ascii=False)
|
||||
self.hardware_interface._logger.info(f"创建二胺溶液配置任务参数: {json_str}")
|
||||
|
||||
# 7. 调用create_order方法创建任务
|
||||
result = self.hardware_interface.create_order(json_str)
|
||||
self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}")
|
||||
|
||||
return json.dumps({"suc": True})
|
||||
|
||||
except BioyondException:
|
||||
# 重新抛出BioyondException
|
||||
raise
|
||||
except Exception as e:
|
||||
# 捕获其他未预期的异常,转换为BioyondException
|
||||
error_msg = f"创建二胺溶液配置任务时发生未预期的错误: {str(e)}"
|
||||
self.hardware_interface._logger.error(error_msg)
|
||||
raise BioyondException(error_msg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
bioyond = BioyondDispensingStation(config={
|
||||
"api_key": "DE9BDDA0",
|
||||
"api_host": "http://192.168.1.200:44388"
|
||||
})
|
||||
|
||||
# 示例1:使用material_id_query查询工作流对应的holdMID
|
||||
workflow_id_1 = "3a15d4a1-3bbe-76f9-a458-292896a338f5" # 二胺溶液配置工作流ID
|
||||
workflow_id_2 = "3a19310d-16b9-9d81-b109-0748e953694b" # 90%10%小瓶投料工作流ID
|
||||
|
||||
#示例2:创建二胺溶液配置任务 - ODA,指定库位名称
|
||||
# bioyond.create_diamine_solution_task(
|
||||
# order_code="task_oda_" + str(int(datetime.now().timestamp())),
|
||||
# order_name="二胺溶液配置-ODA",
|
||||
# material_name="ODA-1",
|
||||
# target_weigh="12.000",
|
||||
# volume="60",
|
||||
# liquid_material_name= "NMP",
|
||||
# speed="400",
|
||||
# temperature="20",
|
||||
# delay_time="600",
|
||||
# hold_m_name="烧杯ODA"
|
||||
# )
|
||||
|
||||
# bioyond.create_diamine_solution_task(
|
||||
# order_code="task_pda_" + str(int(datetime.now().timestamp())),
|
||||
# order_name="二胺溶液配置-PDA",
|
||||
# material_name="PDA-1",
|
||||
# target_weigh="4.178",
|
||||
# volume="60",
|
||||
# liquid_material_name= "NMP",
|
||||
# speed="400",
|
||||
# temperature="20",
|
||||
# delay_time="600",
|
||||
# hold_m_name="烧杯PDA-2"
|
||||
# )
|
||||
|
||||
# bioyond.create_diamine_solution_task(
|
||||
# order_code="task_mpda_" + str(int(datetime.now().timestamp())),
|
||||
# order_name="二胺溶液配置-MPDA",
|
||||
# material_name="MPDA-1",
|
||||
# target_weigh="3.298",
|
||||
# volume="50",
|
||||
# liquid_material_name= "NMP",
|
||||
# speed="400",
|
||||
# temperature="20",
|
||||
# delay_time="600",
|
||||
# hold_m_name="烧杯MPDA"
|
||||
# )
|
||||
|
||||
bioyond.material_id_query("3a19310d-16b9-9d81-b109-0748e953694b")
|
||||
bioyond.material_id_query("3a15d4a1-3bbe-76f9-a458-292896a338f5")
|
||||
|
||||
|
||||
#示例4:创建90%10%小瓶投料任务
|
||||
# vial_result = bioyond.create_90_10_vial_feeding_task(
|
||||
# order_code="task_vial_" + str(int(datetime.now().timestamp())),
|
||||
# order_name="90%10%小瓶投料-1",
|
||||
# percent_90_1_assign_material_name="BTDA-1",
|
||||
# percent_90_1_target_weigh="7.392",
|
||||
# percent_90_2_assign_material_name="BTDA-1",
|
||||
# percent_90_2_target_weigh="7.392",
|
||||
# percent_90_3_assign_material_name="BTDA-2",
|
||||
# percent_90_3_target_weigh="7.392",
|
||||
# percent_10_1_assign_material_name="BTDA-2",
|
||||
# percent_10_1_target_weigh="1.500",
|
||||
# percent_10_1_volume="20",
|
||||
# percent_10_1_liquid_material_name="NMP",
|
||||
# # percent_10_2_assign_material_name="BTDA-c",
|
||||
# # percent_10_2_target_weigh="1.2",
|
||||
# # percent_10_2_volume="20",
|
||||
# # percent_10_2_liquid_material_name="NMP",
|
||||
# speed="400",
|
||||
# temperature="60",
|
||||
# delay_time="1200",
|
||||
# hold_m_name="8.4分装板-1"
|
||||
# )
|
||||
|
||||
# vial_result = bioyond.create_90_10_vial_feeding_task(
|
||||
# order_code="task_vial_" + str(int(datetime.now().timestamp())),
|
||||
# order_name="90%10%小瓶投料-2",
|
||||
# percent_90_1_assign_material_name="BPDA-1",
|
||||
# percent_90_1_target_weigh="5.006",
|
||||
# percent_90_2_assign_material_name="PMDA-1",
|
||||
# percent_90_2_target_weigh="3.810",
|
||||
# percent_90_3_assign_material_name="BPDA-1",
|
||||
# percent_90_3_target_weigh="8.399",
|
||||
# percent_10_1_assign_material_name="BPDA-1",
|
||||
# percent_10_1_target_weigh="1.200",
|
||||
# percent_10_1_volume="20",
|
||||
# percent_10_1_liquid_material_name="NMP",
|
||||
# percent_10_2_assign_material_name="BPDA-1",
|
||||
# percent_10_2_target_weigh="1.200",
|
||||
# percent_10_2_volume="20",
|
||||
# percent_10_2_liquid_material_name="NMP",
|
||||
# speed="400",
|
||||
# temperature="60",
|
||||
# delay_time="1200",
|
||||
# hold_m_name="8.4分装板-2"
|
||||
# )
|
||||
|
||||
#启动调度器
|
||||
#bioyond.scheduler_start()
|
||||
|
||||
#继续调度器
|
||||
#bioyond.scheduler_continue()
|
||||
|
||||
result0 = bioyond.stock_material('{"typeMode": 0, "includeDetail": true}')
|
||||
result1 = bioyond.stock_material('{"typeMode": 1, "includeDetail": true}')
|
||||
result2 = bioyond.stock_material('{"typeMode": 2, "includeDetail": true}')
|
||||
|
||||
matpos1 = bioyond.query_warehouse_by_material_type("3a14196e-b7a0-a5da-1931-35f3000281e9")
|
||||
matpos2 = bioyond.query_warehouse_by_material_type("3a14196e-5dfe-6e21-0c79-fe2036d052c4")
|
||||
matpos3 = bioyond.query_warehouse_by_material_type("3a14196b-24f2-ca49-9081-0cab8021bf1a")
|
||||
|
||||
#样品板(里面有样品瓶)
|
||||
material_data_yp = {
|
||||
"typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "8.4样品板",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
"details": [
|
||||
{
|
||||
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
|
||||
#"code": "物料编码001",
|
||||
"name": "BTDA-1",
|
||||
"quantity": 20,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
|
||||
#"code": "物料编码001",
|
||||
"name": "BPDA-1",
|
||||
"quantity": 20,
|
||||
"x": 2,
|
||||
"y": 1, #x1y2是A02
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
|
||||
#"code": "物料编码001",
|
||||
"name": "BTDA-2",
|
||||
"quantity": 20,
|
||||
"x": 1,
|
||||
"y": 2, #x1y2是A02
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
|
||||
#"code": "物料编码001",
|
||||
"name": "PMDA-1",
|
||||
"quantity": 20,
|
||||
"x": 2,
|
||||
"y": 2, #x1y2是A02
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
}
|
||||
],
|
||||
"Parameters":"{}"
|
||||
}
|
||||
|
||||
material_data_yp = {
|
||||
"typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "8.7样品板",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
"details": [
|
||||
{
|
||||
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
|
||||
#"code": "物料编码001",
|
||||
"name": "mianfen",
|
||||
"quantity": 13,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
|
||||
#"code": "物料编码001",
|
||||
"name": "mianfen2",
|
||||
"quantity": 13,
|
||||
"x": 1,
|
||||
"y": 2, #x1y2是A02
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
}
|
||||
],
|
||||
"Parameters":"{}"
|
||||
}
|
||||
|
||||
#分装板
|
||||
material_data_fzb_1 = {
|
||||
"typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "8.7分装板",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
"details": [
|
||||
{
|
||||
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
|
||||
#"code": "物料编码001",
|
||||
"name": "10%小瓶1",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
|
||||
#"code": "物料编码001",
|
||||
"name": "10%小瓶2",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
|
||||
#"code": "物料编码001",
|
||||
"name": "10%小瓶3",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 3,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
|
||||
#"code": "物料编码001",
|
||||
"name": "90%小瓶1",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 1, #x1y2是A02
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
|
||||
#"code": "物料编码001",
|
||||
"name": "90%小瓶2",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
|
||||
#"code": "物料编码001",
|
||||
"name": "90%小瓶3",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 3,
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
}
|
||||
],
|
||||
"Parameters":"{}"
|
||||
}
|
||||
|
||||
material_data_fzb_2 = {
|
||||
"typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "8.4分装板-2",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
"details": [
|
||||
{
|
||||
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
|
||||
#"code": "物料编码001",
|
||||
"name": "10%小瓶1",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
|
||||
#"code": "物料编码001",
|
||||
"name": "10%小瓶2",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
|
||||
#"code": "物料编码001",
|
||||
"name": "10%小瓶3",
|
||||
"quantity": 1,
|
||||
"x": 1,
|
||||
"y": 3,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
|
||||
#"code": "物料编码001",
|
||||
"name": "90%小瓶1",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 1, #x1y2是A02
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
|
||||
#"code": "物料编码001",
|
||||
"name": "90%小瓶2",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
#"unit": "单位"
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
},
|
||||
{
|
||||
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
|
||||
#"code": "物料编码001",
|
||||
"name": "90%小瓶3",
|
||||
"quantity": 1,
|
||||
"x": 2,
|
||||
"y": 3,
|
||||
"molecular": 1,
|
||||
"Parameters":"{\"molecular\": 1}"
|
||||
}
|
||||
],
|
||||
"Parameters":"{}"
|
||||
}
|
||||
|
||||
#烧杯
|
||||
material_data_sb_oda = {
|
||||
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "mianfen1",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
"Parameters":"{}"
|
||||
}
|
||||
|
||||
material_data_sb_pda_2 = {
|
||||
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
||||
#"code": "物料编码001",
|
||||
#"barCode": "物料条码001",
|
||||
"name": "mianfen2",
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
"Parameters":"{}"
|
||||
}
|
||||
|
||||
# material_data_sb_mpda = {
|
||||
# "typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
||||
# #"code": "物料编码001",
|
||||
# #"barCode": "物料条码001",
|
||||
# "name": "烧杯MPDA",
|
||||
# "unit": "个",
|
||||
# "quantity": 1,
|
||||
# "Parameters":"{}"
|
||||
# }
|
||||
|
||||
|
||||
#result_1 = bioyond.add_material(json.dumps(material_data_yp, ensure_ascii=False))
|
||||
#result_2 = bioyond.add_material(json.dumps(material_data_fzb_1, ensure_ascii=False))
|
||||
# result_3 = bioyond.add_material(json.dumps(material_data_fzb_2, ensure_ascii=False))
|
||||
# result_4 = bioyond.add_material(json.dumps(material_data_sb_oda, ensure_ascii=False))
|
||||
# result_5 = bioyond.add_material(json.dumps(material_data_sb_pda_2, ensure_ascii=False))
|
||||
# #result会返回id
|
||||
# #样品板1id:3a1b3e7d-339d-0291-dfd3-13e2a78fe521
|
||||
|
||||
|
||||
# #将指定物料入库到指定库位
|
||||
#bioyond.material_inbound(result_1, "3a14198e-6929-31f0-8a22-0f98f72260df")
|
||||
#bioyond.material_inbound(result_2, "3a14198e-6929-46fe-841e-03dd753f1e4a")
|
||||
# bioyond.material_inbound(result_3, "3a14198e-6929-72ac-32ce-9b50245682b8")
|
||||
# bioyond.material_inbound(result_4, "3a14198e-d724-e036-afdc-2ae39a7f3383")
|
||||
# bioyond.material_inbound(result_5, "3a14198e-d724-d818-6d4f-5725191a24b5")
|
||||
|
||||
#bioyond.material_outbound(result_1, "3a14198e-6929-31f0-8a22-0f98f72260df")
|
||||
|
||||
# bioyond.stock_material('{"typeMode": 2, "includeDetail": true}')
|
||||
|
||||
query_order = {"status":"100", "pageCount": "10"}
|
||||
bioyond.order_query(json.dumps(query_order, ensure_ascii=False))
|
||||
|
||||
# id = "3a1bce3c-4f31-c8f3-5525-f3b273bc34dc"
|
||||
# bioyond.sample_waste_removal(id)
|
||||
|
||||
@@ -1,203 +1,205 @@
|
||||
# experiment_workflow.py
|
||||
"""
|
||||
实验流程主程序
|
||||
"""
|
||||
|
||||
import json
|
||||
from bioyond_rpc import BioyondV1RPC
|
||||
from config import API_CONFIG, WORKFLOW_MAPPINGS
|
||||
from reaction_station import BioyondReactionStation
|
||||
from config import API_CONFIG, WORKFLOW_MAPPINGS, DECK_CONFIG, MATERIAL_TYPE_MAPPINGS
|
||||
|
||||
|
||||
def run_experiment():
|
||||
"""运行实验流程"""
|
||||
|
||||
|
||||
# 初始化Bioyond客户端
|
||||
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============= 多工作流参数测试(简化接口+材料缓存)=============")
|
||||
|
||||
|
||||
# 显示可用的材料名称(前20个)
|
||||
available_materials = Bioyond.get_available_materials()
|
||||
available_materials = Bioyond.hardware_interface.get_available_materials()
|
||||
print(f"可用材料名称(前20个): {available_materials[:20]}")
|
||||
print(f"总共有 {len(available_materials)} 个材料可用\n")
|
||||
|
||||
|
||||
# 1. 反应器放入
|
||||
print("1. 添加反应器放入工作流,带参数...")
|
||||
Bioyond.reactor_taken_in(
|
||||
assign_material_name="BTDA-DD",
|
||||
cutoff="10000",
|
||||
assign_material_name="BTDA-DD",
|
||||
cutoff="10000",
|
||||
temperature="-10"
|
||||
)
|
||||
|
||||
|
||||
# 2. 液体投料-烧杯 (第一个)
|
||||
print("2. 添加液体投料-烧杯,带参数...")
|
||||
Bioyond.liquid_feeding_beaker(
|
||||
volume="34768.7",
|
||||
volume="34768.7",
|
||||
assign_material_name="ODA",
|
||||
time="0",
|
||||
torque_variation="1",
|
||||
titrationType="1",
|
||||
time="0",
|
||||
torque_variation="1",
|
||||
titrationType="1",
|
||||
temperature=-10
|
||||
)
|
||||
|
||||
|
||||
# 3. 液体投料-烧杯 (第二个)
|
||||
print("3. 添加液体投料-烧杯,带参数...")
|
||||
Bioyond.liquid_feeding_beaker(
|
||||
volume="34080.9",
|
||||
volume="34080.9",
|
||||
assign_material_name="MPDA",
|
||||
time="5",
|
||||
torque_variation="2",
|
||||
titrationType="1",
|
||||
time="5",
|
||||
torque_variation="2",
|
||||
titrationType="1",
|
||||
temperature=0
|
||||
)
|
||||
|
||||
|
||||
# 4. 液体投料-小瓶非滴定
|
||||
print("4. 添加液体投料-小瓶非滴定,带参数...")
|
||||
Bioyond.liquid_feeding_vials_non_titration(
|
||||
volumeFormula="639.5",
|
||||
assign_material_name="SIDA",
|
||||
titration_type="1",
|
||||
time="0",
|
||||
torque_variation="1",
|
||||
volumeFormula="639.5",
|
||||
assign_material_name="SIDA",
|
||||
titration_type="1",
|
||||
time="0",
|
||||
torque_variation="1",
|
||||
temperature=-10
|
||||
)
|
||||
|
||||
|
||||
# 5. 液体投料溶剂
|
||||
print("5. 添加液体投料溶剂,带参数...")
|
||||
Bioyond.liquid_feeding_solvents(
|
||||
assign_material_name="NMP",
|
||||
volume="19000",
|
||||
titration_type="1",
|
||||
time="5",
|
||||
torque_variation="2",
|
||||
volume="19000",
|
||||
titration_type="1",
|
||||
time="5",
|
||||
torque_variation="2",
|
||||
temperature=-10
|
||||
)
|
||||
|
||||
|
||||
# 6-8. 固体进料小瓶 (三个)
|
||||
print("6. 添加固体进料小瓶,带参数...")
|
||||
Bioyond.solid_feeding_vials(
|
||||
material_id="3",
|
||||
time="180",
|
||||
material_id="3",
|
||||
time="180",
|
||||
torque_variation="2",
|
||||
assign_material_name="BTDA-1",
|
||||
assign_material_name="BTDA1",
|
||||
temperature=-10.00
|
||||
)
|
||||
|
||||
#二杆,样品版90
|
||||
print("7. 添加固体进料小瓶,带参数...")
|
||||
Bioyond.solid_feeding_vials(
|
||||
material_id="3",
|
||||
time="180",
|
||||
material_id="3",
|
||||
time="180",
|
||||
torque_variation="2",
|
||||
assign_material_name="BTDA-2",
|
||||
assign_material_name="BTDA2",
|
||||
temperature=25.00
|
||||
)
|
||||
|
||||
#二杆,样品版90
|
||||
print("8. 添加固体进料小瓶,带参数...")
|
||||
Bioyond.solid_feeding_vials(
|
||||
material_id="3",
|
||||
time="480",
|
||||
material_id="3",
|
||||
time="480",
|
||||
torque_variation="2",
|
||||
assign_material_name="BTDA-3",
|
||||
assign_material_name="BTDA3",
|
||||
temperature=25.00
|
||||
)
|
||||
|
||||
|
||||
# 液体投料滴定(第一个)
|
||||
print("9. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="1000",
|
||||
volume_formula="{{6-0-5}}+{{7-0-5}}+{{8-0-5}}",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
|
||||
# 液体投料滴定(第二个)
|
||||
print("10. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="500",
|
||||
volume_formula="500",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
# 液体投料滴定(第三个)
|
||||
print("11. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="500",
|
||||
volume_formula="500",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
|
||||
print("12. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="500",
|
||||
volume_formula="500",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
|
||||
print("13. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="500",
|
||||
volume_formula="500",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
|
||||
print("14. 添加液体投料滴定,带参数...") # ODPA
|
||||
Bioyond.liquid_feeding_titration(
|
||||
volume_formula="500",
|
||||
volume_formula="500",
|
||||
assign_material_name="BTDA-DD",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature="25.00"
|
||||
)
|
||||
|
||||
|
||||
|
||||
print("15. 添加液体投料溶剂,带参数...")
|
||||
Bioyond.liquid_feeding_solvents(
|
||||
assign_material_name="PGME",
|
||||
volume="16894.6",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
volume="16894.6",
|
||||
titration_type="1",
|
||||
time="360",
|
||||
torque_variation="2",
|
||||
temperature=25.00
|
||||
)
|
||||
|
||||
|
||||
# 16. 反应器取出
|
||||
print("16. 添加反应器取出工作流...")
|
||||
Bioyond.reactor_taken_out()
|
||||
|
||||
|
||||
# 显示当前工作流序列
|
||||
sequence = Bioyond.get_workflow_sequence()
|
||||
print("\n当前工作流执行顺序:")
|
||||
print(sequence)
|
||||
|
||||
|
||||
# 执行process_and_execute_workflow,合并工作流并创建任务
|
||||
print("\n4. 执行process_and_execute_workflow...")
|
||||
|
||||
|
||||
result = Bioyond.process_and_execute_workflow(
|
||||
workflow_name="test3_86",
|
||||
task_name="实验3_86"
|
||||
workflow_name="test3_8",
|
||||
task_name="实验3_8"
|
||||
)
|
||||
|
||||
|
||||
# 显示执行结果
|
||||
print("\n5. 执行结果:")
|
||||
if isinstance(result, str):
|
||||
@@ -220,16 +222,16 @@ def run_experiment():
|
||||
print(f"- 任务结果: {result.get('task')}")
|
||||
else:
|
||||
print(f"任务创建失败: {result.get('error')}")
|
||||
|
||||
|
||||
# 可选:启动调度器
|
||||
# Bioyond.scheduler_start()
|
||||
|
||||
|
||||
return Bioyond
|
||||
|
||||
|
||||
def prepare_materials(bioyond):
|
||||
"""准备实验材料(可选)"""
|
||||
|
||||
|
||||
# 样品板材料数据定义
|
||||
material_data_yp_1 = {
|
||||
"typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e",
|
||||
@@ -288,7 +290,7 @@ def prepare_materials(bioyond):
|
||||
],
|
||||
"Parameters": "{}"
|
||||
}
|
||||
|
||||
|
||||
material_data_yp_2 = {
|
||||
"typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e",
|
||||
"name": "样品板-2",
|
||||
@@ -338,7 +340,7 @@ def prepare_materials(bioyond):
|
||||
],
|
||||
"Parameters": "{}"
|
||||
}
|
||||
|
||||
|
||||
# 烧杯材料数据定义
|
||||
beaker_materials = [
|
||||
{
|
||||
@@ -377,12 +379,12 @@ def prepare_materials(bioyond):
|
||||
"parameters": "{\"DeviceMaterialType\":\"NMP\"}"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# 如果需要,可以在这里调用add_material方法添加材料
|
||||
# 例如:
|
||||
# result = bioyond.add_material(json.dumps(material_data_yp_1))
|
||||
# print(f"添加材料结果: {result}")
|
||||
|
||||
|
||||
return {
|
||||
"sample_plates": [material_data_yp_1, material_data_yp_2],
|
||||
"beakers": beaker_materials
|
||||
@@ -392,7 +394,7 @@ def prepare_materials(bioyond):
|
||||
if __name__ == "__main__":
|
||||
# 运行主实验流程
|
||||
bioyond_client = run_experiment()
|
||||
|
||||
|
||||
# 可选:准备材料数据
|
||||
# materials = prepare_materials(bioyond_client)
|
||||
# print(f"\n准备的材料数据: {materials}")
|
||||
|
||||
639
unilabos/devices/workstation/bioyond_studio/reaction_station.py
Normal file
639
unilabos/devices/workstation/bioyond_studio/reaction_station.py
Normal file
@@ -0,0 +1,639 @@
|
||||
import json
|
||||
from typing import List, Dict, Any
|
||||
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
||||
from unilabos.devices.workstation.bioyond_studio.config import (
|
||||
WORKFLOW_STEP_IDS,
|
||||
WORKFLOW_TO_SECTION_MAP
|
||||
)
|
||||
|
||||
|
||||
class BioyondReactionStation(BioyondWorkstation):
|
||||
"""Bioyond反应站类
|
||||
|
||||
继承自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):
|
||||
"""反应器取出"""
|
||||
self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_out"}')
|
||||
reactor_taken_out_params = {"param_values": {}}
|
||||
self.pending_task_params.append(reactor_taken_out_params)
|
||||
print(f"成功添加反应器取出工作流")
|
||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||
return json.dumps({"suc": True})
|
||||
|
||||
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"}')
|
||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||
|
||||
if isinstance(temperature, str):
|
||||
temperature = float(temperature)
|
||||
|
||||
step_id = WORKFLOW_STEP_IDS["reactor_taken_in"]["config"]
|
||||
reactor_taken_in_params = {
|
||||
"param_values": {
|
||||
step_id: [
|
||||
{"m": 0, "n": 3, "Key": "cutoff", "Value": cutoff},
|
||||
{"m": 0, "n": 3, "Key": "temperature", "Value": f"{temperature:.2f}"},
|
||||
{"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
self.pending_task_params.append(reactor_taken_in_params)
|
||||
print(f"成功添加反应器放入参数: material={assign_material_name}->ID:{material_id}, cutoff={cutoff}, temp={temperature:.2f}")
|
||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||
return json.dumps({"suc": True})
|
||||
|
||||
def solid_feeding_vials(
|
||||
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"}')
|
||||
material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||
|
||||
if isinstance(temperature, str):
|
||||
temperature = float(temperature)
|
||||
|
||||
feeding_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"]
|
||||
observe_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"]
|
||||
|
||||
solid_feeding_vials_params = {
|
||||
"param_values": {
|
||||
feeding_id: [
|
||||
{"m": 0, "n": 3, "Key": "materialId", "Value": material_id},
|
||||
{"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id_m}
|
||||
],
|
||||
observe_id: [
|
||||
{"m": 1, "n": 0, "Key": "time", "Value": time},
|
||||
{"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation},
|
||||
{"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
self.pending_task_params.append(solid_feeding_vials_params)
|
||||
print(f"成功添加固体进料小瓶参数: material_id={material_id}, time={time}min, temp={temperature:.2f}°C")
|
||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||
return json.dumps({"suc": True})
|
||||
|
||||
def liquid_feeding_vials_non_titration(
|
||||
self,
|
||||
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)"}')
|
||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||
|
||||
if isinstance(temperature, str):
|
||||
temperature = float(temperature)
|
||||
|
||||
liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"]
|
||||
observe_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"]
|
||||
|
||||
params = {
|
||||
"param_values": {
|
||||
liquid_id: [
|
||||
{"m": 0, "n": 3, "Key": "volumeFormula", "Value": volumeFormula},
|
||||
{"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id},
|
||||
{"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type}
|
||||
],
|
||||
observe_id: [
|
||||
{"m": 1, "n": 0, "Key": "time", "Value": time},
|
||||
{"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation},
|
||||
{"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
self.pending_task_params.append(params)
|
||||
print(f"成功添加液体进料小瓶(非滴定)参数: volume={volumeFormula}μL, material={assign_material_name}->ID:{material_id}")
|
||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||
return json.dumps({"suc": True})
|
||||
|
||||
def liquid_feeding_solvents(
|
||||
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"}')
|
||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||
|
||||
if isinstance(temperature, str):
|
||||
temperature = float(temperature)
|
||||
|
||||
liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"]
|
||||
observe_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"]
|
||||
|
||||
params = {
|
||||
"param_values": {
|
||||
liquid_id: [
|
||||
{"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type},
|
||||
{"m": 0, "n": 1, "Key": "volume", "Value": volume},
|
||||
{"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id}
|
||||
],
|
||||
observe_id: [
|
||||
{"m": 1, "n": 0, "Key": "time", "Value": time},
|
||||
{"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation},
|
||||
{"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
self.pending_task_params.append(params)
|
||||
print(f"成功添加液体进料溶剂参数: material={assign_material_name}->ID:{material_id}, volume={volume}μL")
|
||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||
return json.dumps({"suc": True})
|
||||
|
||||
def liquid_feeding_titration(
|
||||
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)"}')
|
||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||
|
||||
if isinstance(temperature, str):
|
||||
temperature = float(temperature)
|
||||
|
||||
liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"]
|
||||
observe_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"]
|
||||
|
||||
params = {
|
||||
"param_values": {
|
||||
liquid_id: [
|
||||
{"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula},
|
||||
{"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type},
|
||||
{"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id}
|
||||
],
|
||||
observe_id: [
|
||||
{"m": 1, "n": 0, "Key": "time", "Value": time},
|
||||
{"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)},
|
||||
{"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
self.pending_task_params.append(params)
|
||||
print(f"成功添加液体进料滴定参数: volume={volume_formula}μL, material={assign_material_name}->ID:{material_id}")
|
||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||
return json.dumps({"suc": True})
|
||||
|
||||
def liquid_feeding_beaker(
|
||||
self,
|
||||
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"}')
|
||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||
|
||||
if isinstance(temperature, str):
|
||||
temperature = float(temperature)
|
||||
|
||||
liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["liquid"]
|
||||
observe_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["observe"]
|
||||
|
||||
params = {
|
||||
"param_values": {
|
||||
liquid_id: [
|
||||
{"m": 0, "n": 2, "Key": "volume", "Value": volume},
|
||||
{"m": 0, "n": 2, "Key": "assignMaterialName", "Value": material_id},
|
||||
{"m": 0, "n": 2, "Key": "titrationType", "Value": titrationType}
|
||||
],
|
||||
observe_id: [
|
||||
{"m": 1, "n": 0, "Key": "time", "Value": time},
|
||||
{"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation},
|
||||
{"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
self.pending_task_params.append(params)
|
||||
print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}")
|
||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -171,7 +171,6 @@ class WorkstationBase(ABC):
|
||||
def post_init(self, ros_node: ROS2WorkstationNode) -> None:
|
||||
# 初始化物料系统
|
||||
self._ros_node = ros_node
|
||||
self._ros_node.update_resource([self.deck])
|
||||
|
||||
def _build_resource_mappings(self, deck: Deck):
|
||||
"""递归构建资源映射"""
|
||||
|
||||
252
unilabos/registry/devices/bioyond.yaml
Normal file
252
unilabos/registry/devices/bioyond.yaml
Normal file
@@ -0,0 +1,252 @@
|
||||
workstation.bioyond_dispensing_station:
|
||||
category:
|
||||
- workstation
|
||||
- bioyond
|
||||
class:
|
||||
action_value_mappings:
|
||||
create_90_10_vial_feeding_task:
|
||||
feedback: {}
|
||||
goal:
|
||||
delay_time: delay_time
|
||||
hold_m_name: hold_m_name
|
||||
order_name: order_name
|
||||
percent_10_1_assign_material_name: percent_10_1_assign_material_name
|
||||
percent_10_1_liquid_material_name: percent_10_1_liquid_material_name
|
||||
percent_10_1_target_weigh: percent_10_1_target_weigh
|
||||
percent_10_1_volume: percent_10_1_volume
|
||||
percent_10_2_assign_material_name: percent_10_2_assign_material_name
|
||||
percent_10_2_liquid_material_name: percent_10_2_liquid_material_name
|
||||
percent_10_2_target_weigh: percent_10_2_target_weigh
|
||||
percent_10_2_volume: percent_10_2_volume
|
||||
percent_10_3_assign_material_name: percent_10_3_assign_material_name
|
||||
percent_10_3_liquid_material_name: percent_10_3_liquid_material_name
|
||||
percent_10_3_target_weigh: percent_10_3_target_weigh
|
||||
percent_10_3_volume: percent_10_3_volume
|
||||
percent_90_1_assign_material_name: percent_90_1_assign_material_name
|
||||
percent_90_1_target_weigh: percent_90_1_target_weigh
|
||||
percent_90_2_assign_material_name: percent_90_2_assign_material_name
|
||||
percent_90_2_target_weigh: percent_90_2_target_weigh
|
||||
percent_90_3_assign_material_name: percent_90_3_assign_material_name
|
||||
percent_90_3_target_weigh: percent_90_3_target_weigh
|
||||
speed: speed
|
||||
temperature: temperature
|
||||
goal_default:
|
||||
delay_time: ''
|
||||
hold_m_name: ''
|
||||
order_name: ''
|
||||
percent_10_1_assign_material_name: ''
|
||||
percent_10_1_liquid_material_name: ''
|
||||
percent_10_1_target_weigh: ''
|
||||
percent_10_1_volume: ''
|
||||
percent_10_2_assign_material_name: ''
|
||||
percent_10_2_liquid_material_name: ''
|
||||
percent_10_2_target_weigh: ''
|
||||
percent_10_2_volume: ''
|
||||
percent_10_3_assign_material_name: ''
|
||||
percent_10_3_liquid_material_name: ''
|
||||
percent_10_3_target_weigh: ''
|
||||
percent_10_3_volume: ''
|
||||
percent_90_1_assign_material_name: ''
|
||||
percent_90_1_target_weigh: ''
|
||||
percent_90_2_assign_material_name: ''
|
||||
percent_90_2_target_weigh: ''
|
||||
percent_90_3_assign_material_name: ''
|
||||
percent_90_3_target_weigh: ''
|
||||
speed: ''
|
||||
temperature: ''
|
||||
handles: {}
|
||||
result:
|
||||
return_info: return_info
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: DispenStationVialFeed_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
delay_time:
|
||||
type: string
|
||||
hold_m_name:
|
||||
type: string
|
||||
order_name:
|
||||
type: string
|
||||
percent_10_1_assign_material_name:
|
||||
type: string
|
||||
percent_10_1_liquid_material_name:
|
||||
type: string
|
||||
percent_10_1_target_weigh:
|
||||
type: string
|
||||
percent_10_1_volume:
|
||||
type: string
|
||||
percent_10_2_assign_material_name:
|
||||
type: string
|
||||
percent_10_2_liquid_material_name:
|
||||
type: string
|
||||
percent_10_2_target_weigh:
|
||||
type: string
|
||||
percent_10_2_volume:
|
||||
type: string
|
||||
percent_10_3_assign_material_name:
|
||||
type: string
|
||||
percent_10_3_liquid_material_name:
|
||||
type: string
|
||||
percent_10_3_target_weigh:
|
||||
type: string
|
||||
percent_10_3_volume:
|
||||
type: string
|
||||
percent_90_1_assign_material_name:
|
||||
type: string
|
||||
percent_90_1_target_weigh:
|
||||
type: string
|
||||
percent_90_2_assign_material_name:
|
||||
type: string
|
||||
percent_90_2_target_weigh:
|
||||
type: string
|
||||
percent_90_3_assign_material_name:
|
||||
type: string
|
||||
percent_90_3_target_weigh:
|
||||
type: string
|
||||
speed:
|
||||
type: string
|
||||
temperature:
|
||||
type: string
|
||||
required:
|
||||
- order_name
|
||||
- percent_90_1_assign_material_name
|
||||
- percent_90_1_target_weigh
|
||||
- percent_90_2_assign_material_name
|
||||
- percent_90_2_target_weigh
|
||||
- percent_90_3_assign_material_name
|
||||
- percent_90_3_target_weigh
|
||||
- percent_10_1_assign_material_name
|
||||
- percent_10_1_target_weigh
|
||||
- percent_10_1_volume
|
||||
- percent_10_1_liquid_material_name
|
||||
- percent_10_2_assign_material_name
|
||||
- percent_10_2_target_weigh
|
||||
- percent_10_2_volume
|
||||
- percent_10_2_liquid_material_name
|
||||
- percent_10_3_assign_material_name
|
||||
- percent_10_3_target_weigh
|
||||
- percent_10_3_volume
|
||||
- percent_10_3_liquid_material_name
|
||||
- speed
|
||||
- temperature
|
||||
- delay_time
|
||||
- hold_m_name
|
||||
title: DispenStationVialFeed_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: DispenStationVialFeed_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: DispenStationVialFeed
|
||||
type: object
|
||||
type: DispenStationVialFeed
|
||||
create_diamine_solution_task:
|
||||
feedback: {}
|
||||
goal:
|
||||
delay_time: delay_time
|
||||
hold_m_name: hold_m_name
|
||||
liquid_material_name: liquid_material_name
|
||||
material_name: material_name
|
||||
order_name: order_name
|
||||
speed: speed
|
||||
target_weigh: target_weigh
|
||||
temperature: temperature
|
||||
volume: volume
|
||||
goal_default:
|
||||
delay_time: ''
|
||||
hold_m_name: ''
|
||||
liquid_material_name: ''
|
||||
material_name: ''
|
||||
order_name: ''
|
||||
speed: ''
|
||||
target_weigh: ''
|
||||
temperature: ''
|
||||
volume: ''
|
||||
handles: {}
|
||||
result:
|
||||
return_info: return_info
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: DispenStationSolnPrep_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
delay_time:
|
||||
type: string
|
||||
hold_m_name:
|
||||
type: string
|
||||
liquid_material_name:
|
||||
type: string
|
||||
material_name:
|
||||
type: string
|
||||
order_name:
|
||||
type: string
|
||||
speed:
|
||||
type: string
|
||||
target_weigh:
|
||||
type: string
|
||||
temperature:
|
||||
type: string
|
||||
volume:
|
||||
type: string
|
||||
required:
|
||||
- order_name
|
||||
- material_name
|
||||
- target_weigh
|
||||
- volume
|
||||
- liquid_material_name
|
||||
- speed
|
||||
- temperature
|
||||
- delay_time
|
||||
- hold_m_name
|
||||
title: DispenStationSolnPrep_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: DispenStationSolnPrep_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: DispenStationSolnPrep
|
||||
type: object
|
||||
type: DispenStationSolnPrep
|
||||
module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation
|
||||
status_types: {}
|
||||
type: python
|
||||
config_info: []
|
||||
description: ''
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
config:
|
||||
type: string
|
||||
required:
|
||||
- config
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
version: 1.0.0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1361,8 +1361,7 @@ laiyu_liquid:
|
||||
mix_liquid_height: 0.0
|
||||
mix_rate: 0
|
||||
mix_stage: ''
|
||||
mix_times:
|
||||
- 0
|
||||
mix_times: 0
|
||||
mix_vol: 0
|
||||
none_keys:
|
||||
- ''
|
||||
@@ -1492,11 +1491,9 @@ laiyu_liquid:
|
||||
mix_stage:
|
||||
type: string
|
||||
mix_times:
|
||||
items:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
type: array
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
mix_vol:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
|
||||
@@ -3994,8 +3994,7 @@ liquid_handler:
|
||||
mix_liquid_height: 0.0
|
||||
mix_rate: 0
|
||||
mix_stage: ''
|
||||
mix_times:
|
||||
- 0
|
||||
mix_times: 0
|
||||
mix_vol: 0
|
||||
none_keys:
|
||||
- ''
|
||||
@@ -4151,11 +4150,9 @@ liquid_handler:
|
||||
mix_stage:
|
||||
type: string
|
||||
mix_times:
|
||||
items:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
type: array
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
mix_vol:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
@@ -5015,8 +5012,7 @@ liquid_handler.biomek:
|
||||
mix_liquid_height: 0.0
|
||||
mix_rate: 0
|
||||
mix_stage: ''
|
||||
mix_times:
|
||||
- 0
|
||||
mix_times: 0
|
||||
mix_vol: 0
|
||||
none_keys:
|
||||
- ''
|
||||
@@ -5159,11 +5155,9 @@ liquid_handler.biomek:
|
||||
mix_stage:
|
||||
type: string
|
||||
mix_times:
|
||||
items:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
type: array
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
mix_vol:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
@@ -7807,8 +7801,7 @@ liquid_handler.prcxi:
|
||||
mix_liquid_height: 0.0
|
||||
mix_rate: 0
|
||||
mix_stage: ''
|
||||
mix_times:
|
||||
- 0
|
||||
mix_times: 0
|
||||
mix_vol: 0
|
||||
none_keys:
|
||||
- ''
|
||||
@@ -7937,11 +7930,9 @@ liquid_handler.prcxi:
|
||||
mix_stage:
|
||||
type: string
|
||||
mix_times:
|
||||
items:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
type: array
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
mix_vol:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
|
||||
@@ -4,11 +4,11 @@ reaction_station.bioyond:
|
||||
- reaction_station_bioyond
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-add_material:
|
||||
auto-append_to_workflow_sequence:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
material_data: null
|
||||
web_workflow_name: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
@@ -18,22 +18,21 @@ reaction_station.bioyond:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_data:
|
||||
type: object
|
||||
web_workflow_name:
|
||||
type: string
|
||||
required:
|
||||
- material_data
|
||||
- web_workflow_name
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: add_material参数
|
||||
title: append_to_workflow_sequence参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-create_90_10_vial_feeding_task:
|
||||
auto-clear_workflows:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
task_data: null
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
@@ -42,470 +41,13 @@ reaction_station.bioyond:
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
task_data:
|
||||
type: string
|
||||
required:
|
||||
- task_data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_90_10_vial_feeding_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-create_batch_90_10_vial_feeding_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
batch_data: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
batch_data:
|
||||
type: string
|
||||
required:
|
||||
- batch_data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_batch_90_10_vial_feeding_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-create_batch_diamine_solution_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
batch_data: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
batch_data:
|
||||
type: string
|
||||
required:
|
||||
- batch_data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_batch_diamine_solution_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-create_diamine_solution_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
solution_data: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
solution_data:
|
||||
type: string
|
||||
required:
|
||||
- solution_data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_diamine_solution_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-create_order:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parameters: null
|
||||
task_name: null
|
||||
workflow_name: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parameters:
|
||||
type: object
|
||||
task_name:
|
||||
type: string
|
||||
workflow_name:
|
||||
type: string
|
||||
required:
|
||||
- workflow_name
|
||||
- task_name
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_order参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-create_resource:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
resource_data: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
resource_data:
|
||||
type: string
|
||||
required:
|
||||
- resource_data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_resource参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-delete_material:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
material_data: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_data:
|
||||
type: string
|
||||
required:
|
||||
- material_data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: delete_material参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-device_operation:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
device_id: null
|
||||
operation: null
|
||||
parameters: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
device_id:
|
||||
type: string
|
||||
operation:
|
||||
type: string
|
||||
parameters:
|
||||
type: object
|
||||
required:
|
||||
- device_id
|
||||
- operation
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: device_operation参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-dispensing_material_inbound:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
material_data: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_data:
|
||||
type: string
|
||||
required:
|
||||
- material_data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: dispensing_material_inbound参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-dispensing_material_outbound:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
material_data: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_data:
|
||||
type: string
|
||||
required:
|
||||
- material_data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: dispensing_material_outbound参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-drip_back:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
assign_material_name: Reactor
|
||||
temperature: 25.0
|
||||
time: '0'
|
||||
torque_variation: '1'
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
assign_material_name:
|
||||
default: Reactor
|
||||
type: string
|
||||
temperature:
|
||||
default: 25.0
|
||||
type: number
|
||||
time:
|
||||
default: '0'
|
||||
type: string
|
||||
torque_variation:
|
||||
default: '1'
|
||||
type: string
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: drip_back参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-execute_bioyond_sync_workflow:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parameters: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parameters:
|
||||
type: object
|
||||
required:
|
||||
- parameters
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: execute_bioyond_sync_workflow参数
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
auto-execute_bioyond_update_workflow:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parameters: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parameters:
|
||||
type: object
|
||||
required:
|
||||
- parameters
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: execute_bioyond_update_workflow参数
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
auto-liquid_feeding_beaker:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
material_name: ''
|
||||
volume: ''
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_name:
|
||||
default: ''
|
||||
type: string
|
||||
volume:
|
||||
default: ''
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: liquid_feeding_beaker参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-liquid_feeding_solvents:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
material_name: ''
|
||||
volume: ''
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_name:
|
||||
default: ''
|
||||
type: string
|
||||
volume:
|
||||
default: ''
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: liquid_feeding_solvents参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-liquid_feeding_titration:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
material_name: ''
|
||||
time: '120'
|
||||
titration_type: '1'
|
||||
torque_variation: '2'
|
||||
volume: ''
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_name:
|
||||
default: ''
|
||||
type: string
|
||||
time:
|
||||
default: '120'
|
||||
type: string
|
||||
titration_type:
|
||||
default: '1'
|
||||
type: string
|
||||
torque_variation:
|
||||
default: '2'
|
||||
type: string
|
||||
volume:
|
||||
default: ''
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: liquid_feeding_titration参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-liquid_feeding_vials_non_titration:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
material_name: ''
|
||||
volume: ''
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_name:
|
||||
default: ''
|
||||
type: string
|
||||
volume:
|
||||
default: ''
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: liquid_feeding_vials_non_titration参数
|
||||
title: clear_workflows参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-load_bioyond_data_from_file:
|
||||
@@ -533,74 +75,11 @@ reaction_station.bioyond:
|
||||
title: load_bioyond_data_from_file参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-material_inbound:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
location_name: null
|
||||
material_id: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
location_name:
|
||||
type: string
|
||||
material_id:
|
||||
type: string
|
||||
required:
|
||||
- material_id
|
||||
- location_name
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: material_inbound参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-material_outbound:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
location_name: null
|
||||
material_id: null
|
||||
quantity: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
location_name:
|
||||
type: string
|
||||
material_id:
|
||||
type: string
|
||||
quantity:
|
||||
type: integer
|
||||
required:
|
||||
- material_id
|
||||
- location_name
|
||||
- quantity
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: material_outbound参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-merge_workflow_with_parameters:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
name: null
|
||||
workflows: null
|
||||
json_str: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
@@ -610,15 +89,10 @@ reaction_station.bioyond:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
name:
|
||||
json_str:
|
||||
type: string
|
||||
workflows:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- name
|
||||
- workflows
|
||||
- json_str
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
@@ -626,31 +100,6 @@ reaction_station.bioyond:
|
||||
title: merge_workflow_with_parameters参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-order_query:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
query_data: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
query_data:
|
||||
type: string
|
||||
required:
|
||||
- query_data
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: order_query参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-post_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
@@ -676,33 +125,11 @@ reaction_station.bioyond:
|
||||
title: post_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-reactor_taken_in:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: reactor_taken_in参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-reactor_taken_out:
|
||||
auto-process_web_workflows:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
order_id: ''
|
||||
preintake_id: ''
|
||||
json_str: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
@@ -712,18 +139,15 @@ reaction_station.bioyond:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
order_id:
|
||||
default: ''
|
||||
json_str:
|
||||
type: string
|
||||
preintake_id:
|
||||
default: ''
|
||||
type: string
|
||||
required: []
|
||||
required:
|
||||
- json_str
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: reactor_taken_out参数
|
||||
title: process_web_workflows参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-reset_workstation:
|
||||
@@ -747,11 +171,11 @@ reaction_station.bioyond:
|
||||
title: reset_workstation参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-sample_waste_removal:
|
||||
auto-resource_tree_add:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
waste_data: null
|
||||
resources: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
@@ -761,119 +185,42 @@ reaction_station.bioyond:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
waste_data:
|
||||
resources:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- resources
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: resource_tree_add参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_workflow_sequence:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
json_str: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
json_str:
|
||||
type: string
|
||||
required:
|
||||
- waste_data
|
||||
- json_str
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: sample_waste_removal参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-solid_feeding_vials:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
material_name: ''
|
||||
volume: ''
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
material_name:
|
||||
default: ''
|
||||
type: string
|
||||
volume:
|
||||
default: ''
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: solid_feeding_vials参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-start_scheduler:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: start_scheduler参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-stock_material:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
location: null
|
||||
material_id: null
|
||||
quantity: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
location:
|
||||
type: string
|
||||
material_id:
|
||||
type: string
|
||||
quantity:
|
||||
type: integer
|
||||
required:
|
||||
- material_id
|
||||
- location
|
||||
- quantity
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: stock_material参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-stop_scheduler:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: stop_scheduler参数
|
||||
title: set_workflow_sequence参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-transfer_resource_to_another:
|
||||
@@ -1064,33 +411,6 @@ reaction_station.bioyond:
|
||||
title: transfer_resource_to_another参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-validate_workflow_parameters:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
workflows: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
workflows:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- workflows
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: validate_workflow_parameters参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
bioyond_sync:
|
||||
feedback: {}
|
||||
goal:
|
||||
@@ -1407,11 +727,8 @@ reaction_station.bioyond:
|
||||
module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation
|
||||
protocol_type: []
|
||||
status_types:
|
||||
all_workflows: dict
|
||||
bioyond_status: dict
|
||||
device_list: dict
|
||||
scheduler_status: dict
|
||||
station_info: dict
|
||||
workflow_parameter_template: dict
|
||||
workstation_status: dict
|
||||
type: python
|
||||
config_info: []
|
||||
@@ -1431,24 +748,15 @@ reaction_station.bioyond:
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
all_workflows:
|
||||
type: object
|
||||
bioyond_status:
|
||||
type: object
|
||||
device_list:
|
||||
type: object
|
||||
scheduler_status:
|
||||
type: object
|
||||
station_info:
|
||||
type: object
|
||||
workflow_parameter_template:
|
||||
type: object
|
||||
workstation_status:
|
||||
type: object
|
||||
required:
|
||||
- station_info
|
||||
- bioyond_status
|
||||
- workflow_parameter_template
|
||||
- scheduler_status
|
||||
- device_list
|
||||
- all_workflows
|
||||
- workstation_status
|
||||
type: object
|
||||
version: 1.0.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ container:
|
||||
- container
|
||||
class:
|
||||
module: unilabos.resources.container:RegularContainer
|
||||
type: unilabos
|
||||
type: pylabrobot
|
||||
description: regular organic container
|
||||
handles:
|
||||
- data_key: fluid_in
|
||||
|
||||
@@ -1,67 +1,84 @@
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
|
||||
from pylabrobot.resources import Container
|
||||
from unilabos_msgs.msg import Resource
|
||||
|
||||
from unilabos.ros.msgs.message_converter import convert_from_ros_msg
|
||||
|
||||
|
||||
class RegularContainer(object):
|
||||
# 第一个参数必须是id传入
|
||||
# noinspection PyShadowingBuiltins
|
||||
def __init__(self, id: str):
|
||||
self.id = id
|
||||
self.ulr_resource = Resource()
|
||||
self._data = None
|
||||
class RegularContainer(Container):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "size_x" not in kwargs:
|
||||
kwargs["size_x"] = 0
|
||||
if "size_y" not in kwargs:
|
||||
kwargs["size_y"] = 0
|
||||
if "size_z" not in kwargs:
|
||||
kwargs["size_z"] = 0
|
||||
self.kwargs = kwargs
|
||||
self.state = {}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def ulr_resource_data(self):
|
||||
if self._data is None:
|
||||
self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {}
|
||||
return self._data
|
||||
|
||||
@ulr_resource_data.setter
|
||||
def ulr_resource_data(self, value: dict):
|
||||
self._data = value
|
||||
self.ulr_resource.data = json.dumps(self._data)
|
||||
|
||||
@property
|
||||
def liquid_type(self):
|
||||
return self.ulr_resource_data.get("liquid_type", None)
|
||||
|
||||
@liquid_type.setter
|
||||
def liquid_type(self, value: str):
|
||||
if value is not None:
|
||||
self.ulr_resource_data["liquid_type"] = value
|
||||
else:
|
||||
self.ulr_resource_data.pop("liquid_type", None)
|
||||
|
||||
@property
|
||||
def liquid_volume(self):
|
||||
return self.ulr_resource_data.get("liquid_volume", None)
|
||||
|
||||
@liquid_volume.setter
|
||||
def liquid_volume(self, value: float):
|
||||
if value is not None:
|
||||
self.ulr_resource_data["liquid_volume"] = value
|
||||
else:
|
||||
self.ulr_resource_data.pop("liquid_volume", None)
|
||||
|
||||
def get_ulr_resource(self) -> Resource:
|
||||
"""
|
||||
获取UlrResource对象
|
||||
:return: UlrResource对象
|
||||
"""
|
||||
self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新
|
||||
return self.ulr_resource
|
||||
|
||||
def get_ulr_resource_as_dict(self) -> Resource:
|
||||
"""
|
||||
获取UlrResource对象
|
||||
:return: UlrResource对象
|
||||
"""
|
||||
to_dict = convert_from_ros_msg(self.get_ulr_resource())
|
||||
to_dict["type"] = "container"
|
||||
return to_dict
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id}"
|
||||
def load_state(self, state: Dict[str, Any]):
|
||||
self.state = state
|
||||
#
|
||||
# class RegularContainer(object):
|
||||
# # 第一个参数必须是id传入
|
||||
# # noinspection PyShadowingBuiltins
|
||||
# def __init__(self, id: str):
|
||||
# self.id = id
|
||||
# self.ulr_resource = Resource()
|
||||
# self._data = None
|
||||
#
|
||||
# @property
|
||||
# def ulr_resource_data(self):
|
||||
# if self._data is None:
|
||||
# self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {}
|
||||
# return self._data
|
||||
#
|
||||
# @ulr_resource_data.setter
|
||||
# def ulr_resource_data(self, value: dict):
|
||||
# self._data = value
|
||||
# self.ulr_resource.data = json.dumps(self._data)
|
||||
#
|
||||
# @property
|
||||
# def liquid_type(self):
|
||||
# return self.ulr_resource_data.get("liquid_type", None)
|
||||
#
|
||||
# @liquid_type.setter
|
||||
# def liquid_type(self, value: str):
|
||||
# if value is not None:
|
||||
# self.ulr_resource_data["liquid_type"] = value
|
||||
# else:
|
||||
# self.ulr_resource_data.pop("liquid_type", None)
|
||||
#
|
||||
# @property
|
||||
# def liquid_volume(self):
|
||||
# return self.ulr_resource_data.get("liquid_volume", None)
|
||||
#
|
||||
# @liquid_volume.setter
|
||||
# def liquid_volume(self, value: float):
|
||||
# if value is not None:
|
||||
# self.ulr_resource_data["liquid_volume"] = value
|
||||
# else:
|
||||
# self.ulr_resource_data.pop("liquid_volume", None)
|
||||
#
|
||||
# def get_ulr_resource(self) -> Resource:
|
||||
# """
|
||||
# 获取UlrResource对象
|
||||
# :return: UlrResource对象
|
||||
# """
|
||||
# self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新
|
||||
# return self.ulr_resource
|
||||
#
|
||||
# def get_ulr_resource_as_dict(self) -> Resource:
|
||||
# """
|
||||
# 获取UlrResource对象
|
||||
# :return: UlrResource对象
|
||||
# """
|
||||
# to_dict = convert_from_ros_msg(self.get_ulr_resource())
|
||||
# to_dict["type"] = "container"
|
||||
# return to_dict
|
||||
#
|
||||
# def __str__(self):
|
||||
# return f"{self.id}"
|
||||
@@ -1,18 +1,23 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import json
|
||||
import os.path
|
||||
import traceback
|
||||
from typing import Union, Any, Dict, List
|
||||
from typing import Union, Any, Dict, List, Tuple
|
||||
import uuid
|
||||
import networkx as nx
|
||||
from pylabrobot.resources import ResourceHolder
|
||||
from unilabos_msgs.msg import Resource
|
||||
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.resources.container import RegularContainer
|
||||
from unilabos.resources.itemized_carrier import ItemizedCarrier
|
||||
from unilabos.ros.msgs.message_converter import convert_to_ros_msg
|
||||
from unilabos.ros.nodes.resource_tracker import (
|
||||
ResourceDictInstance,
|
||||
ResourceTreeSet,
|
||||
)
|
||||
from unilabos.utils import logger
|
||||
from unilabos.utils.banner_print import print_status
|
||||
|
||||
try:
|
||||
@@ -44,6 +49,33 @@ def canonicalize_nodes_data(
|
||||
if node.get("label") is not None:
|
||||
node_id = node.pop("label")
|
||||
node["id"] = node["name"] = node_id
|
||||
if not isinstance(node.get("config"), dict):
|
||||
node["config"] = {}
|
||||
if not node.get("type"):
|
||||
node["type"] = "device"
|
||||
print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'type', defaulting to 'device'", "warning")
|
||||
if node.get("name", None) is None:
|
||||
node["name"] = node.get("id")
|
||||
print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'name', defaulting to {node['name']}", "warning")
|
||||
if not isinstance(node.get("position"), dict):
|
||||
node["position"] = {"position": {}}
|
||||
x = node.pop("x", None)
|
||||
if x is not None:
|
||||
node["position"]["position"]["x"] = x
|
||||
y = node.pop("y", None)
|
||||
if y is not None:
|
||||
node["position"]["position"]["y"] = y
|
||||
z = node.pop("z", None)
|
||||
if z is not None:
|
||||
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()):
|
||||
if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children"]:
|
||||
v = node.pop(k)
|
||||
node["config"][k] = v
|
||||
|
||||
# 第二步:处理parent_relation
|
||||
id2idx = {node["id"]: idx for idx, node in enumerate(nodes)}
|
||||
@@ -301,6 +333,10 @@ def read_graphml(graphml_file: str) -> tuple[nx.Graph, ResourceTreeSet, List[Dic
|
||||
"nodes": [node.res_content.model_dump(by_alias=True) for node in resource_tree_set.all_nodes],
|
||||
"links": standardized_links,
|
||||
}
|
||||
dump_json_path = os.path.join(BasicConfig.working_dir, os.path.basename(graphml_file).rsplit(".")[0] + ".json")
|
||||
with open(dump_json_path, "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(graph_data, indent=4, ensure_ascii=False))
|
||||
print_status(f"GraphML converted to JSON and saved to {dump_json_path}", "info")
|
||||
physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False)
|
||||
handle_communications(physical_setup_graph)
|
||||
|
||||
@@ -576,13 +612,13 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
|
||||
return r
|
||||
|
||||
|
||||
def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = {}, deck: Any = None) -> list[dict]:
|
||||
def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[str, Tuple[str, str]] = {}, deck: Any = None) -> list[dict]:
|
||||
"""
|
||||
将 bioyond 物料格式转换为 ulab 物料格式
|
||||
|
||||
Args:
|
||||
bioyond_materials: bioyond 系统的物料查询结果列表
|
||||
type_mapping: 物料类型映射字典,格式 {bioyond_type: plr_class_name}
|
||||
type_mapping: 物料类型映射字典,格式 {bioyond_type: [plr_class_name, class_uuid]}
|
||||
location_id_mapping: 库位 ID 到名称的映射字典,格式 {location_id: location_name}
|
||||
|
||||
Returns:
|
||||
@@ -592,13 +628,14 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
|
||||
|
||||
for material in bioyond_materials:
|
||||
className = (
|
||||
type_mapping.get(material.get("typeName"), "RegularContainer") if type_mapping else "RegularContainer"
|
||||
type_mapping.get(material.get("typeName"), ("RegularContainer", ""))[0] if type_mapping else "RegularContainer"
|
||||
)
|
||||
|
||||
plr_material: ResourcePLR = initialize_resource(
|
||||
{"name": material["name"], "class": className}, resource_type=ResourcePLR
|
||||
)
|
||||
plr_material.code = material.get("code", "") and material.get("barCode", "") or ""
|
||||
plr_material.unilabos_uuid = str(uuid.uuid4())
|
||||
|
||||
# 处理子物料(detail)
|
||||
if material.get("detail") and len(material["detail"]) > 0:
|
||||
@@ -614,7 +651,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
|
||||
# plr_material.unassign_child_resource(bottle)
|
||||
plr_material.sites[number] = None
|
||||
plr_material[number] = initialize_resource(
|
||||
{"name": f'{detail["name"]}_{number}', "class": type_mapping[detail["name"]]}, resource_type=ResourcePLR
|
||||
{"name": f'{detail["name"]}_{number}', "class": type_mapping[detail["name"]][0]}, resource_type=ResourcePLR
|
||||
)
|
||||
else:
|
||||
bottle.tracker.liquids = [
|
||||
@@ -645,32 +682,59 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
|
||||
return plr_materials
|
||||
|
||||
|
||||
def resource_plr_to_bioyond(plr_materials: list[ResourcePLR], type_mapping: dict = {}, warehouse_mapping: dict = {}) -> list[dict]:
|
||||
def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict = {}, warehouse_mapping: dict = {}) -> list[dict]:
|
||||
bioyond_materials = []
|
||||
for plr_material in plr_materials:
|
||||
material = {
|
||||
"name": plr_material.name,
|
||||
"typeName": plr_material.__class__.__name__,
|
||||
"code": plr_material.code,
|
||||
"quantity": 0,
|
||||
"detail": [],
|
||||
"locations": [],
|
||||
}
|
||||
if hasattr(plr_material, "capacity") and plr_material.capacity > 1:
|
||||
for idx in range(plr_material.capacity):
|
||||
bottle = plr_material[idx]
|
||||
detail = {
|
||||
"x": (idx // (plr_material.num_items_x * plr_material.num_items_y)) + 1,
|
||||
"y": ((idx % (plr_material.num_items_x * plr_material.num_items_y)) // plr_material.num_items_x) + 1,
|
||||
"z": (idx % plr_material.num_items_x) + 1,
|
||||
for resource in plr_resources:
|
||||
if hasattr(resource, "capacity") and resource.capacity > 1:
|
||||
material = {
|
||||
"typeId": type_mapping.get(resource.model)[1],
|
||||
"name": resource.name,
|
||||
"unit": "个",
|
||||
"quantity": 1,
|
||||
"details": [],
|
||||
"Parameters": "{}"
|
||||
}
|
||||
for bottle in resource.children:
|
||||
if isinstance(resource, ItemizedCarrier):
|
||||
site = resource.get_child_identifier(bottle)
|
||||
else:
|
||||
site = {"x": bottle.location.x - 1, "y": bottle.location.y - 1}
|
||||
detail_item = {
|
||||
"typeId": type_mapping.get(bottle.model)[1],
|
||||
"name": bottle.name,
|
||||
"code": bottle.code if hasattr(bottle, "code") else "",
|
||||
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
|
||||
"x": site["x"] + 1,
|
||||
"y": site["y"] + 1,
|
||||
"molecular": 1,
|
||||
"Parameters": json.dumps({"molecular": 1})
|
||||
}
|
||||
material["detail"].append(detail)
|
||||
material["quantity"] = 1.0
|
||||
material["details"].append(detail_item)
|
||||
else:
|
||||
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
|
||||
material["quantity"] = sum(qty for _, qty in bottle.tracker.liquids) if hasattr(plr_material, "tracker") else 0
|
||||
bottle = resource[0] if resource.capacity > 0 else resource
|
||||
material = {
|
||||
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
||||
"name": resource.get("name", ""),
|
||||
"unit": "",
|
||||
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
|
||||
"Parameters": "{}"
|
||||
}
|
||||
|
||||
if resource.parent is not None and isinstance(resource.parent, ItemizedCarrier):
|
||||
site_in_parent = resource.parent.get_child_identifier(resource)
|
||||
material["locations"] = [
|
||||
{
|
||||
"id": warehouse_mapping[resource.parent.name]["site_uuids"][site_in_parent["identifier"]],
|
||||
"whid": warehouse_mapping[resource.parent.name]["uuid"],
|
||||
"whName": resource.parent.name,
|
||||
"x": site_in_parent["z"] + 1,
|
||||
"y": site_in_parent["y"] + 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
|
||||
print(f"material_data: {material}")
|
||||
bioyond_materials.append(material)
|
||||
return bioyond_materials
|
||||
|
||||
@@ -717,6 +781,7 @@ def initialize_resource(resource_config: dict, resource_type: Any = None) -> Uni
|
||||
else:
|
||||
r = resource_plr
|
||||
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.ulr_resource = convert_to_ros_msg(
|
||||
Resource, {k: v for k, v in resource_config.items() if k != "class"}
|
||||
|
||||
@@ -163,6 +163,89 @@ class ItemizedCarrier(ResourcePLR):
|
||||
if hasattr(resource, "unassign"):
|
||||
resource.unassign()
|
||||
|
||||
def get_child_identifier(self, child: ResourcePLR):
|
||||
"""Get the identifier information for a given child resource.
|
||||
|
||||
Args:
|
||||
child: The Resource object to find the identifier for
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing:
|
||||
- identifier: The string identifier (e.g. "A1", "B2")
|
||||
- idx: The integer index in the sites list
|
||||
- x: The x index (column index, 0-based)
|
||||
- y: The y index (row index, 0-based)
|
||||
- z: The z index (layer index, 0-based)
|
||||
|
||||
Raises:
|
||||
ValueError: If the child resource is not found in this carrier
|
||||
"""
|
||||
# Find the child resource in sites
|
||||
for idx, resource in enumerate(self.sites):
|
||||
if resource is child:
|
||||
# Get the identifier from ordering keys
|
||||
identifier = list(self._ordering.keys())[idx]
|
||||
|
||||
# Parse identifier to get x, y, z indices
|
||||
x_idx, y_idx, z_idx = self._parse_identifier_to_indices(identifier, idx)
|
||||
|
||||
return {
|
||||
"identifier": identifier,
|
||||
"idx": idx,
|
||||
"x": x_idx,
|
||||
"y": y_idx,
|
||||
"z": z_idx
|
||||
}
|
||||
|
||||
# If not found, raise an error
|
||||
raise ValueError(f"Resource {child} is not assigned to this carrier")
|
||||
|
||||
def _parse_identifier_to_indices(self, identifier: str, idx: int) -> Tuple[int, int, int]:
|
||||
"""Parse identifier string to get x, y, z indices.
|
||||
|
||||
Args:
|
||||
identifier: String identifier like "A1", "B2", etc.
|
||||
idx: Linear index as fallback for calculation
|
||||
|
||||
Returns:
|
||||
Tuple of (x_idx, y_idx, z_idx)
|
||||
"""
|
||||
# If we have explicit dimensions, calculate from idx
|
||||
if self.num_items_x > 0 and self.num_items_y > 0:
|
||||
# Calculate 3D indices from linear index
|
||||
z_idx = idx // (self.num_items_x * self.num_items_y) if self.num_items_z > 0 else 0
|
||||
remaining = idx % (self.num_items_x * self.num_items_y)
|
||||
y_idx = remaining // self.num_items_x
|
||||
x_idx = remaining % self.num_items_x
|
||||
return x_idx, y_idx, z_idx
|
||||
|
||||
# Fallback: parse from Excel-style identifier
|
||||
if isinstance(identifier, str) and len(identifier) >= 2:
|
||||
# Extract row (letter) and column (number)
|
||||
row_letters = ""
|
||||
col_numbers = ""
|
||||
|
||||
for char in identifier:
|
||||
if char.isalpha():
|
||||
row_letters += char
|
||||
elif char.isdigit():
|
||||
col_numbers += char
|
||||
|
||||
if row_letters and col_numbers:
|
||||
# Convert letter(s) to row index (A=0, B=1, etc.)
|
||||
y_idx = 0
|
||||
for char in row_letters:
|
||||
y_idx = y_idx * 26 + (ord(char.upper()) - ord('A'))
|
||||
|
||||
# Convert number to column index (1-based to 0-based)
|
||||
x_idx = int(col_numbers) - 1
|
||||
z_idx = 0 # Default layer
|
||||
|
||||
return x_idx, y_idx, z_idx
|
||||
|
||||
# If all else fails, assume linear arrangement
|
||||
return idx, 0, 0
|
||||
|
||||
def __getitem__(
|
||||
self,
|
||||
identifier: Union[str, int, Sequence[int], Sequence[str], slice, range],
|
||||
|
||||
@@ -26,6 +26,7 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device
|
||||
d = None
|
||||
original_device_config = copy.deepcopy(device_config)
|
||||
device_class_config = device_config["class"]
|
||||
uid = device_config["uuid"]
|
||||
if isinstance(device_class_config, str): # 如果是字符串,则直接去lab_registry中查找,获取class
|
||||
if len(device_class_config) == 0:
|
||||
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:
|
||||
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:
|
||||
return d
|
||||
|
||||
@@ -6,7 +6,7 @@ import threading
|
||||
import time
|
||||
import traceback
|
||||
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
|
||||
import asyncio
|
||||
@@ -132,6 +132,7 @@ class ROSLoggerAdapter:
|
||||
def init_wrapper(
|
||||
self,
|
||||
device_id: str,
|
||||
device_uuid: str,
|
||||
driver_class: type[T],
|
||||
device_config: Dict[str, Any],
|
||||
status_types: Dict[str, Any],
|
||||
@@ -150,6 +151,7 @@ def init_wrapper(
|
||||
if children is None:
|
||||
children = []
|
||||
kwargs["device_id"] = device_id
|
||||
kwargs["device_uuid"] = device_uuid
|
||||
kwargs["driver_class"] = driver_class
|
||||
kwargs["device_config"] = device_config
|
||||
kwargs["driver_params"] = driver_params
|
||||
@@ -266,6 +268,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
self,
|
||||
driver_instance: T,
|
||||
device_id: str,
|
||||
device_uuid: str,
|
||||
status_types: Dict[str, Any],
|
||||
action_value_mappings: Dict[str, Any],
|
||||
hardware_interface: Dict[str, Any],
|
||||
@@ -278,6 +281,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
Args:
|
||||
driver_instance: 设备实例
|
||||
device_id: 设备标识符
|
||||
device_uuid: 设备标识符
|
||||
status_types: 需要发布的状态和传感器信息
|
||||
action_value_mappings: 设备动作
|
||||
hardware_interface: 硬件接口配置
|
||||
@@ -285,7 +289,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
"""
|
||||
self.driver_instance = driver_instance
|
||||
self.device_id = device_id
|
||||
self.uuid = str(uuid.uuid4())
|
||||
self.uuid = device_uuid
|
||||
self.publish_high_frequency = False
|
||||
self.callback_group = ReentrantCallbackGroup()
|
||||
self.resource_tracker = resource_tracker
|
||||
@@ -554,6 +558,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
async def update_resource(self, resources: List["ResourcePLR"]):
|
||||
r = SerialCommand.Request()
|
||||
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"})
|
||||
response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore
|
||||
try:
|
||||
@@ -648,15 +657,27 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
results.append({"success": True, "action": "update"})
|
||||
elif action == "remove":
|
||||
# 移除资源
|
||||
plr_resources: List[ResourcePLR] = [
|
||||
self.resource_tracker.uuid_to_resources[i] for i in resources_uuid
|
||||
]
|
||||
found_resources: List[List[Union[ResourcePLR, dict]]] = self.resource_tracker.figure_resource(
|
||||
[{"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)
|
||||
if callable(func):
|
||||
func(plr_resources)
|
||||
for plr_resource in plr_resources:
|
||||
func(found_plr_resources)
|
||||
for plr_resource in found_plr_resources:
|
||||
plr_resource.parent.unassign_child_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"})
|
||||
except Exception as 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]
|
||||
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:
|
||||
self.lab_logger().error(f"{action_name} 物料实例获取失败: {e}\n{traceback.format_exc()}")
|
||||
@@ -1347,6 +1371,7 @@ class ROS2DeviceNode:
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
device_uuid: str,
|
||||
driver_class: Type[T],
|
||||
device_config: Dict[str, Any],
|
||||
driver_params: Dict[str, Any],
|
||||
@@ -1362,6 +1387,7 @@ class ROS2DeviceNode:
|
||||
|
||||
Args:
|
||||
device_id: 设备标识符
|
||||
device_uuid: 设备uuid
|
||||
driver_class: 设备类
|
||||
device_config: 原始初始化的json
|
||||
driver_params: driver初始化的参数
|
||||
@@ -1436,6 +1462,7 @@ class ROS2DeviceNode:
|
||||
children=children,
|
||||
driver_instance=self._driver_instance, # type: ignore
|
||||
device_id=device_id,
|
||||
device_uuid=device_uuid,
|
||||
status_types=status_types,
|
||||
action_value_mappings=action_value_mappings,
|
||||
hardware_interface=hardware_interface,
|
||||
@@ -1446,6 +1473,7 @@ class ROS2DeviceNode:
|
||||
self._ros_node = BaseROS2DeviceNode(
|
||||
driver_instance=self._driver_instance,
|
||||
device_id=device_id,
|
||||
device_uuid=device_uuid,
|
||||
status_types=status_types,
|
||||
action_value_mappings=action_value_mappings,
|
||||
hardware_interface=hardware_interface,
|
||||
|
||||
@@ -18,7 +18,7 @@ from unilabos_msgs.srv import (
|
||||
ResourceDelete,
|
||||
ResourceUpdate,
|
||||
ResourceList,
|
||||
SerialCommand,
|
||||
SerialCommand, ResourceGet,
|
||||
) # type: ignore
|
||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||
from unique_identifier_msgs.msg import UUID
|
||||
@@ -41,6 +41,7 @@ from unilabos.ros.nodes.resource_tracker import (
|
||||
ResourceTreeSet,
|
||||
ResourceTreeInstance,
|
||||
)
|
||||
from unilabos.utils import logger
|
||||
from unilabos.utils.exception import DeviceClassInvalid
|
||||
from unilabos.utils.type_check import serialize_result_info
|
||||
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
||||
@@ -99,17 +100,6 @@ class HostNode(BaseROS2DeviceNode):
|
||||
"""
|
||||
if self._instance is not None:
|
||||
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
|
||||
@@ -127,6 +117,91 @@ class HostNode(BaseROS2DeviceNode):
|
||||
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_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例
|
||||
@@ -207,81 +282,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
].items():
|
||||
controller_config["update_rate"] = update_rate
|
||||
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(
|
||||
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"])
|
||||
mount_uuid = data["mount_uuid"]
|
||||
first_add = data["first_add"]
|
||||
@@ -903,7 +904,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
response.response = json.dumps(uuid_mapping) if success else "FAILED"
|
||||
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"]
|
||||
with_children: bool = data["with_children"]
|
||||
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)
|
||||
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物料树删除
|
||||
"""
|
||||
@@ -919,7 +920,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
response.response = "OK"
|
||||
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物料树更新
|
||||
"""
|
||||
@@ -932,20 +933,29 @@ class HostNode(BaseROS2DeviceNode):
|
||||
|
||||
from unilabos.app.web.client import http_client
|
||||
|
||||
resource_start_time = time.time()
|
||||
uuid_mapping = http_client.resource_tree_update(resource_tree_set, "", False)
|
||||
success = bool(uuid_mapping)
|
||||
resource_end_time = time.time()
|
||||
self.lab_logger().info(
|
||||
f"[Host Node-Resource] 物料更新上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
|
||||
)
|
||||
if uuid_mapping:
|
||||
self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点")
|
||||
# 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式
|
||||
response.response = json.dumps(uuid_mapping)
|
||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
|
||||
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)
|
||||
|
||||
def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
|
||||
for uid, trees in uuid_to_trees.items():
|
||||
new_tree_set = ResourceTreeSet(trees)
|
||||
resource_start_time = time.time()
|
||||
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)
|
||||
resource_end_time = time.time()
|
||||
self.lab_logger().info(
|
||||
f"[Host Node-Resource] 物料更新上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
|
||||
)
|
||||
if uuid_mapping:
|
||||
self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点")
|
||||
# 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式
|
||||
response.response = json.dumps(uuid_mapping)
|
||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
|
||||
|
||||
async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
|
||||
"""
|
||||
子节点通知Host物料树更新
|
||||
|
||||
@@ -958,13 +968,13 @@ class HostNode(BaseROS2DeviceNode):
|
||||
action = data["action"]
|
||||
data = data["data"]
|
||||
if action == "add":
|
||||
self._resource_tree_action_add_callback(data, response)
|
||||
await self._resource_tree_action_add_callback(data, response)
|
||||
elif action == "get":
|
||||
self._resource_tree_action_get_callback(data, response)
|
||||
await self._resource_tree_action_get_callback(data, response)
|
||||
elif action == "update":
|
||||
self._resource_tree_action_update_callback(data, response)
|
||||
await self._resource_tree_action_update_callback(data, response)
|
||||
elif action == "remove":
|
||||
self._resource_tree_action_remove_callback(data, response)
|
||||
await self._resource_tree_action_remove_callback(data, response)
|
||||
else:
|
||||
self.lab_logger().error(f"[Host Node-Resource] Invalid action: {action}")
|
||||
response.response = "ERROR"
|
||||
|
||||
@@ -6,13 +6,14 @@ from typing import List, Dict, Any, Optional, TYPE_CHECKING
|
||||
|
||||
import rclpy
|
||||
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 rclpy.action import ActionServer, ActionClient
|
||||
from rclpy.action.server import ServerGoalHandle
|
||||
from rclpy.callback_groups import ReentrantCallbackGroup
|
||||
from unilabos_msgs.msg import Resource # type: ignore
|
||||
from unilabos_msgs.srv import ResourceGet, ResourceUpdate # type: ignore
|
||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||
|
||||
from unilabos.compile import action_protocol_generators
|
||||
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 (
|
||||
get_action_type,
|
||||
convert_to_ros_msg,
|
||||
convert_from_ros_msg,
|
||||
convert_from_ros_msg_with_mapping,
|
||||
)
|
||||
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:
|
||||
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||
@@ -50,6 +51,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
||||
*,
|
||||
driver_instance: "WorkstationBase",
|
||||
device_id: str,
|
||||
device_uuid: str,
|
||||
status_types: Dict[str, Any],
|
||||
action_value_mappings: Dict[str, Any],
|
||||
hardware_interface: Dict[str, Any],
|
||||
@@ -64,6 +66,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
||||
super().__init__(
|
||||
driver_instance=driver_instance,
|
||||
device_id=device_id,
|
||||
device_uuid=device_uuid,
|
||||
status_types=status_types,
|
||||
action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
|
||||
hardware_interface=hardware_interface,
|
||||
@@ -222,16 +225,28 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
||||
# 向Host查询物料当前状态
|
||||
for k, v in goal.get_fields_and_field_types().items():
|
||||
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||
r = ResourceGet.Request()
|
||||
resource_id = (
|
||||
protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"]
|
||||
)
|
||||
r.id = resource_id
|
||||
r.with_children = True
|
||||
response = await self._resource_clients["resource_get"].call_async(r)
|
||||
protocol_kwargs[k] = list_to_nested_dict(
|
||||
[convert_from_ros_msg(rs) for rs in response.resources]
|
||||
)
|
||||
self.lab_logger().info(f"{protocol_name} 查询资源状态: Key: {k} Type: {v}")
|
||||
|
||||
try:
|
||||
# 统一处理单个或多个资源
|
||||
resource_id = (
|
||||
protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"]
|
||||
)
|
||||
r = SerialCommand_Request()
|
||||
r.command = json.dumps({"id": resource_id, "with_children": True})
|
||||
# 发送请求并等待响应
|
||||
response: SerialCommand_Response = await self._resource_clients[
|
||||
"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')}")
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import traceback
|
||||
import uuid
|
||||
from pydantic import BaseModel, field_serializer, field_validator
|
||||
from pydantic import Field
|
||||
@@ -49,7 +50,7 @@ class ResourceDict(BaseModel):
|
||||
uuid: str = Field(description="Resource UUID")
|
||||
name: str = Field(description="Resource name")
|
||||
description: str = Field(description="Resource description", default="")
|
||||
schema: Dict[str, Any] = Field(description="Resource schema", default_factory=dict)
|
||||
resource_schema: Dict[str, Any] = Field(description="Resource schema", default_factory=dict, serialization_alias="schema", validation_alias="schema")
|
||||
model: Dict[str, Any] = Field(description="Resource model", default_factory=dict)
|
||||
icon: str = Field(description="Resource icon", default="")
|
||||
parent_uuid: Optional["str"] = Field(description="Parent resource uuid", default=None) # 先设定parent_uuid
|
||||
@@ -140,7 +141,7 @@ class ResourceDictInstance(object):
|
||||
def get_nested_dict(self) -> Dict[str, Any]:
|
||||
"""获取资源实例的嵌套字典表示"""
|
||||
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["position"] = self.res_content.position.position.model_dump()
|
||||
return res_dict
|
||||
@@ -213,7 +214,7 @@ class ResourceTreeInstance(object):
|
||||
if node.res_content.uuid:
|
||||
known_uuids.add(node.res_content.uuid)
|
||||
else:
|
||||
print(f"警告: 资源 {node.res_content.id} 没有uuid")
|
||||
logger.warning(f"警告: 资源 {node.res_content.id} 没有uuid")
|
||||
|
||||
# 验证并递归处理子节点
|
||||
for child in node.children:
|
||||
@@ -289,8 +290,6 @@ class ResourceTreeSet(object):
|
||||
elif isinstance(resource_list[0], ResourceTreeInstance):
|
||||
# 已经是ResourceTree列表
|
||||
self.trees = cast(List[ResourceTreeInstance], resource_list)
|
||||
elif isinstance(resource_list[0], list):
|
||||
pass
|
||||
else:
|
||||
raise TypeError(
|
||||
f"不支持的类型: {type(resource_list[0])}。"
|
||||
@@ -307,10 +306,7 @@ class ResourceTreeSet(object):
|
||||
replace_info = {
|
||||
"plate": "plate",
|
||||
"well": "well",
|
||||
"tip_spot": "container",
|
||||
"trash": "container",
|
||||
"deck": "deck",
|
||||
"tip_rack": "container",
|
||||
}
|
||||
if source in replace_info:
|
||||
return replace_info[source]
|
||||
@@ -320,7 +316,12 @@ class ResourceTreeSet(object):
|
||||
|
||||
def build_uuid_mapping(res: "PLRResource", uuid_list: list):
|
||||
"""递归构建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:
|
||||
build_uuid_mapping(child, uuid_list)
|
||||
|
||||
@@ -384,7 +385,7 @@ class ResourceTreeSet(object):
|
||||
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):
|
||||
"""一次遍历收集 name_to_uuid 和 all_states"""
|
||||
@@ -396,13 +397,13 @@ class ResourceTreeSet(object):
|
||||
def node_to_plr_dict(node: ResourceDictInstance, has_model: bool):
|
||||
"""转换节点为 PLR 字典格式"""
|
||||
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:
|
||||
logger.warning(f"未知类型 {res.type},使用默认类型 tip_spot")
|
||||
logger.warning(f"未知类型 {res.type}")
|
||||
|
||||
d = {
|
||||
"name": res.name,
|
||||
"type": plr_type,
|
||||
"type": res.config.get("type", plr_type),
|
||||
"size_x": res.config.get("size_x", 0),
|
||||
"size_y": res.config.get("size_y", 0),
|
||||
"size_z": res.config.get("size_z", 0),
|
||||
@@ -413,7 +414,7 @@ class ResourceTreeSet(object):
|
||||
"type": "Coordinate",
|
||||
},
|
||||
"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],
|
||||
"parent_name": res.parent_instance_name,
|
||||
**res.config,
|
||||
@@ -435,7 +436,7 @@ class ResourceTreeSet(object):
|
||||
try:
|
||||
sub_cls = find_subclass(plr_dict["type"], PLRResource)
|
||||
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)
|
||||
if "category" not in spec.parameters:
|
||||
plr_dict.pop("category", None)
|
||||
@@ -715,16 +716,9 @@ class ResourceTreeSet(object):
|
||||
Returns:
|
||||
ResourceTreeSet: 反序列化后的资源树集合
|
||||
"""
|
||||
# 将每个字典转换为 ResourceInstanceDict
|
||||
# FIXME: 需要重新确定parent关系
|
||||
nested_lists = []
|
||||
for tree_data in data:
|
||||
flatten_instances = [
|
||||
ResourceDictInstance.get_resource_instance_from_dict(node_dict) for node_dict in tree_data
|
||||
]
|
||||
nested_lists.append(flatten_instances)
|
||||
|
||||
# 使用现有的构造函数创建 ResourceTreeSet
|
||||
nested_lists.extend(ResourceTreeSet.from_raw_list(tree_data).trees)
|
||||
return cls(nested_lists)
|
||||
|
||||
|
||||
@@ -777,7 +771,8 @@ class DeviceNodeResourceTracker(object):
|
||||
else:
|
||||
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 两种类型
|
||||
|
||||
@@ -830,7 +825,7 @@ class DeviceNodeResourceTracker(object):
|
||||
resource_name = self._get_resource_attr(res, "name")
|
||||
if resource_name and resource_name in name_to_uuid_map:
|
||||
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
|
||||
logger.debug(f"设置资源UUID: {resource_name} -> {new_uuid}")
|
||||
return 1
|
||||
@@ -842,7 +837,7 @@ class DeviceNodeResourceTracker(object):
|
||||
"""
|
||||
递归遍历资源树,更新所有节点的uuid
|
||||
|
||||
Args:
|
||||
Args:0
|
||||
resource: 资源对象(可以是dict或实例)
|
||||
uuid_map: uuid映射字典,{old_uuid: new_uuid}
|
||||
|
||||
@@ -852,17 +847,18 @@ class DeviceNodeResourceTracker(object):
|
||||
|
||||
def process(res):
|
||||
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
|
||||
replaced = 0
|
||||
if current_uuid and current_uuid in uuid_map:
|
||||
new_uuid = uuid_map[current_uuid]
|
||||
if current_uuid != new_uuid:
|
||||
self._set_resource_uuid(res, new_uuid)
|
||||
self.set_resource_uuid(res, new_uuid)
|
||||
# 更新uuid_to_resources映射
|
||||
if current_uuid in self.uuid_to_resources:
|
||||
self.uuid_to_resources.pop(current_uuid)
|
||||
self.uuid_to_resources[new_uuid] = res
|
||||
logger.debug(f"更新uuid: {current_uuid} -> {new_uuid}")
|
||||
return 1
|
||||
return 0
|
||||
replaced = 1
|
||||
return replaced
|
||||
|
||||
return self._traverse_and_process(resource, process)
|
||||
|
||||
@@ -877,8 +873,9 @@ class DeviceNodeResourceTracker(object):
|
||||
def process(res):
|
||||
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
|
||||
if current_uuid:
|
||||
old = self.uuid_to_resources.get(current_uuid)
|
||||
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
|
||||
|
||||
self._traverse_and_process(resource, process)
|
||||
@@ -913,9 +910,23 @@ class DeviceNodeResourceTracker(object):
|
||||
Args:
|
||||
resource: 资源对象(可以是dict或实例)
|
||||
"""
|
||||
root_uuids = {}
|
||||
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):
|
||||
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)
|
||||
# 递归收集uuid映射
|
||||
self._collect_uuid_mapping(resource)
|
||||
@@ -1046,13 +1057,19 @@ class DeviceNodeResourceTracker(object):
|
||||
) -> List[Tuple[Any, Any]]:
|
||||
res_list = []
|
||||
# print(resource, target_resource_cls_type, identifier_key, compare_value)
|
||||
children = getattr(resource, "children", [])
|
||||
children = []
|
||||
if not isinstance(resource, dict):
|
||||
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:
|
||||
res_list.extend(
|
||||
self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource)
|
||||
)
|
||||
if issubclass(type(resource), target_resource_cls_type):
|
||||
if target_resource_cls_type == dict:
|
||||
if type(resource) == dict:
|
||||
# 对于字典类型,直接检查 identifier_key
|
||||
if identifier_key in resource:
|
||||
if resource[identifier_key] == compare_value:
|
||||
|
||||
@@ -336,6 +336,9 @@ class WorkstationNodeCreator(DeviceClassCreator[T]):
|
||||
try:
|
||||
# 创建实例,额外补充一个给protocol node的字段,后面考虑取消
|
||||
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")
|
||||
if deck_dict:
|
||||
from pylabrobot.resources import Deck, Resource
|
||||
|
||||
Reference in New Issue
Block a user