mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-09 00:15:10 +00:00
Compare commits
52 Commits
feat/unila
...
a2e609e592
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2e609e592 | ||
|
|
cb9840be59 | ||
|
|
b64de9b509 | ||
|
|
6f7adce998 | ||
|
|
af15ae0b3e | ||
|
|
0597029a84 | ||
|
|
34b85f6097 | ||
|
|
5a5f4e037b | ||
|
|
a90613f3de | ||
|
|
2b04457037 | ||
|
|
9a06ef3836 | ||
|
|
ce3f2b33c5 | ||
|
|
78729ef86c | ||
|
|
6f143b068b | ||
|
|
03423e4791 | ||
|
|
9fc6781406 | ||
|
|
9753ef02c3 | ||
|
|
6a0614c0c9 | ||
|
|
a25e8f6853 | ||
|
|
5c249e66a2 | ||
|
|
93ac095e0a | ||
|
|
288d9fea91 | ||
|
|
81b28cef71 | ||
|
|
d57e5ffdae | ||
|
|
d5e0d76311 | ||
|
|
beaa1d7213 | ||
|
|
1e5f6b0c04 | ||
|
|
5ae89d8607 | ||
|
|
74d0ea3379 | ||
|
|
440c9965fd | ||
|
|
9cac852bc3 | ||
|
|
de662a42aa | ||
|
|
632f9b90d1 | ||
|
|
d7c970d244 | ||
|
|
2c69e663a7 | ||
|
|
f03ff96ae4 | ||
|
|
c68903ed83 | ||
|
|
8efbbbe72a | ||
|
|
4a23b05abc | ||
|
|
6d8884a2c7 | ||
|
|
c0e7a69553 | ||
|
|
fb6ee79577 | ||
|
|
dbe129caab | ||
|
|
7250995891 | ||
|
|
68eddbdffd | ||
|
|
32bd234176 | ||
|
|
3d62e8bf6c | ||
|
|
efec1dd501 | ||
|
|
c16756ddb3 | ||
|
|
daf41871a1 | ||
|
|
6b0b28becf | ||
|
|
0f7366f3ee |
9
.conda/scripts/post-link.bat
Normal file
9
.conda/scripts/post-link.bat
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
REM upgrade pip
|
||||||
|
"%PREFIX%\python.exe" -m pip install --upgrade pip
|
||||||
|
|
||||||
|
REM install extra deps
|
||||||
|
"%PREFIX%\python.exe" -m pip install paho-mqtt opentrons_shared_data
|
||||||
|
"%PREFIX%\python.exe" -m pip install git+https://github.com/Xuwznln/pylabrobot.git
|
||||||
9
.conda/scripts/post-link.sh
Normal file
9
.conda/scripts/post-link.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
# make sure pip is available
|
||||||
|
"$PREFIX/bin/python" -m pip install --upgrade pip
|
||||||
|
|
||||||
|
# install extra deps
|
||||||
|
"$PREFIX/bin/python" -m pip install paho-mqtt opentrons_shared_data
|
||||||
|
"$PREFIX/bin/python" -m pip install git+https://github.com/Xuwznln/pylabrobot.git
|
||||||
26
.cursorignore
Normal file
26
.cursorignore
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.conda
|
||||||
|
# .github
|
||||||
|
.idea
|
||||||
|
# .vscode
|
||||||
|
output
|
||||||
|
pylabrobot_repo
|
||||||
|
recipes
|
||||||
|
scripts
|
||||||
|
service
|
||||||
|
temp
|
||||||
|
# unilabos/test
|
||||||
|
# unilabos/app/web
|
||||||
|
unilabos/device_mesh
|
||||||
|
unilabos_data
|
||||||
|
unilabos_msgs
|
||||||
|
unilabos.egg-info
|
||||||
|
CONTRIBUTORS
|
||||||
|
# LICENSE
|
||||||
|
MANIFEST.in
|
||||||
|
pyrightconfig.json
|
||||||
|
# README.md
|
||||||
|
# README_zh.md
|
||||||
|
setup.py
|
||||||
|
setup.cfg
|
||||||
|
.gitattrubutes
|
||||||
|
**/__pycache__
|
||||||
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
@@ -1,19 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
# GitHub Actions
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
target-branch: "dev"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
day: "monday"
|
|
||||||
time: "06:00"
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
reviewers:
|
|
||||||
- "msgcenterpy-team"
|
|
||||||
labels:
|
|
||||||
- "dependencies"
|
|
||||||
- "github-actions"
|
|
||||||
commit-message:
|
|
||||||
prefix: "ci"
|
|
||||||
include: "scope"
|
|
||||||
52
.github/workflows/ci-check.yml
vendored
52
.github/workflows/ci-check.yml
vendored
@@ -1,52 +0,0 @@
|
|||||||
name: CI Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main, dev]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
registry-check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash -l {0}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Miniconda
|
|
||||||
uses: conda-incubator/setup-miniconda@v3
|
|
||||||
with:
|
|
||||||
miniconda-version: 'latest'
|
|
||||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
|
||||||
channel-priority: strict
|
|
||||||
activate-environment: check-env
|
|
||||||
auto-activate-base: false
|
|
||||||
auto-update-conda: false
|
|
||||||
show-channel-urls: true
|
|
||||||
|
|
||||||
- name: Install minimal ROS dependencies
|
|
||||||
run: |
|
|
||||||
conda install ros-humble-ros-core ros-humble-std-msgs ros-humble-geometry-msgs ros-humble-control-msgs -c robostack-staging -c conda-forge
|
|
||||||
|
|
||||||
- name: Install unilabos-msgs and project
|
|
||||||
run: |
|
|
||||||
conda install ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge
|
|
||||||
pip install -e .
|
|
||||||
|
|
||||||
- name: Run check mode (complete_registry)
|
|
||||||
run: |
|
|
||||||
python -m unilabos --check_mode --skip_env_check
|
|
||||||
|
|
||||||
- name: Check for uncommitted changes
|
|
||||||
run: |
|
|
||||||
if ! git diff --exit-code; then
|
|
||||||
echo "::error::检测到文件变化!请先在本地运行 'python -m unilabos --complete_registry' 并提交变更"
|
|
||||||
echo "变化的文件:"
|
|
||||||
git diff --name-only
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "检查通过:无文件变化"
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,7 +4,6 @@ temp/
|
|||||||
output/
|
output/
|
||||||
unilabos_data/
|
unilabos_data/
|
||||||
pyrightconfig.json
|
pyrightconfig.json
|
||||||
.cursorignore
|
|
||||||
## Python
|
## Python
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|||||||
@@ -161,12 +161,6 @@ def parse_args():
|
|||||||
default=False,
|
default=False,
|
||||||
help="Complete registry information",
|
help="Complete registry information",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--check_mode",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Run in check mode for CI: validates registry imports and ensures no file changes",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no_update_feedback",
|
"--no_update_feedback",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -320,12 +314,6 @@ def main():
|
|||||||
BasicConfig.machine_name = machine_name
|
BasicConfig.machine_name = machine_name
|
||||||
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
||||||
|
|
||||||
# Check mode 处理
|
|
||||||
check_mode = args_dict.get("check_mode", False)
|
|
||||||
BasicConfig.check_mode = check_mode
|
|
||||||
if check_mode:
|
|
||||||
print_status("Check mode 启用,将进行 complete_registry 检查", "info")
|
|
||||||
|
|
||||||
from unilabos.resources.graphio import (
|
from unilabos.resources.graphio import (
|
||||||
read_node_link_json,
|
read_node_link_json,
|
||||||
read_graphml,
|
read_graphml,
|
||||||
@@ -343,14 +331,10 @@ def main():
|
|||||||
# 显示启动横幅
|
# 显示启动横幅
|
||||||
print_unilab_banner(args_dict)
|
print_unilab_banner(args_dict)
|
||||||
|
|
||||||
# 注册表 - check_mode 时强制启用 complete_registry
|
# 注册表
|
||||||
complete_registry = args_dict.get("complete_registry", False) or check_mode
|
lab_registry = build_registry(
|
||||||
lab_registry = build_registry(args_dict["registry_path"], complete_registry, BasicConfig.upload_registry)
|
args_dict["registry_path"], args_dict.get("complete_registry", False), BasicConfig.upload_registry
|
||||||
|
)
|
||||||
# Check mode: complete_registry 完成后直接退出,git diff 检测由 CI workflow 执行
|
|
||||||
if check_mode:
|
|
||||||
print_status("Check mode: complete_registry 完成,退出", "info")
|
|
||||||
os._exit(0)
|
|
||||||
|
|
||||||
if BasicConfig.upload_registry:
|
if BasicConfig.upload_registry:
|
||||||
# 设备注册到服务端 - 需要 ak 和 sk
|
# 设备注册到服务端 - 需要 ak 和 sk
|
||||||
|
|||||||
@@ -58,14 +58,14 @@ class JobResultStore:
|
|||||||
feedback=feedback or {},
|
feedback=feedback or {},
|
||||||
timestamp=time.time(),
|
timestamp=time.time(),
|
||||||
)
|
)
|
||||||
logger.trace(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}")
|
logger.debug(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}")
|
||||||
|
|
||||||
def get_and_remove(self, job_id: str) -> Optional[JobResult]:
|
def get_and_remove(self, job_id: str) -> Optional[JobResult]:
|
||||||
"""获取并删除任务结果"""
|
"""获取并删除任务结果"""
|
||||||
with self._results_lock:
|
with self._results_lock:
|
||||||
result = self._results.pop(job_id, None)
|
result = self._results.pop(job_id, None)
|
||||||
if result:
|
if result:
|
||||||
logger.trace(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}")
|
logger.debug(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_result(self, job_id: str) -> Optional[JobResult]:
|
def get_result(self, job_id: str) -> Optional[JobResult]:
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class DeviceActionManager:
|
|||||||
job_info.set_ready_timeout(10) # 设置10秒超时
|
job_info.set_ready_timeout(10) # 设置10秒超时
|
||||||
self.active_jobs[device_key] = job_info
|
self.active_jobs[device_key] = job_info
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
logger.trace(f"[DeviceActionManager] Job {job_log} can start immediately for {device_key}")
|
logger.info(f"[DeviceActionManager] Job {job_log} can start immediately for {device_key}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def start_job(self, job_id: str) -> bool:
|
def start_job(self, job_id: str) -> bool:
|
||||||
@@ -210,9 +210,8 @@ class DeviceActionManager:
|
|||||||
job_info.update_timestamp()
|
job_info.update_timestamp()
|
||||||
# 从all_jobs中移除已结束的job
|
# 从all_jobs中移除已结束的job
|
||||||
del self.all_jobs[job_id]
|
del self.all_jobs[job_id]
|
||||||
# job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
# logger.debug(f"[DeviceActionManager] Job {job_log} ended for {device_key}")
|
logger.info(f"[DeviceActionManager] Job {job_log} ended for {device_key}")
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
logger.warning(f"[DeviceActionManager] Job {job_log} was not active for {device_key}")
|
logger.warning(f"[DeviceActionManager] Job {job_log} was not active for {device_key}")
|
||||||
@@ -228,7 +227,7 @@ class DeviceActionManager:
|
|||||||
next_job_log = format_job_log(
|
next_job_log = format_job_log(
|
||||||
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||||
)
|
)
|
||||||
logger.trace(f"[DeviceActionManager] Next job {next_job_log} can start for {device_key}")
|
logger.info(f"[DeviceActionManager] Next job {next_job_log} can start for {device_key}")
|
||||||
return next_job
|
return next_job
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -269,7 +268,7 @@ class DeviceActionManager:
|
|||||||
# 从all_jobs中移除
|
# 从all_jobs中移除
|
||||||
del self.all_jobs[job_id]
|
del self.all_jobs[job_id]
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
logger.trace(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
|
logger.info(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
|
||||||
|
|
||||||
# 启动下一个任务
|
# 启动下一个任务
|
||||||
if device_key in self.device_queues and self.device_queues[device_key]:
|
if device_key in self.device_queues and self.device_queues[device_key]:
|
||||||
@@ -282,7 +281,7 @@ class DeviceActionManager:
|
|||||||
next_job_log = format_job_log(
|
next_job_log = format_job_log(
|
||||||
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||||
)
|
)
|
||||||
logger.trace(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
|
logger.info(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 如果是排队中的任务
|
# 如果是排队中的任务
|
||||||
@@ -296,7 +295,7 @@ class DeviceActionManager:
|
|||||||
job_log = format_job_log(
|
job_log = format_job_log(
|
||||||
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
||||||
)
|
)
|
||||||
logger.trace(f"[DeviceActionManager] Queued job {job_log} cancelled for {device_key}")
|
logger.info(f"[DeviceActionManager] Queued job {job_log} cancelled for {device_key}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
@@ -566,7 +565,7 @@ class MessageProcessor:
|
|||||||
|
|
||||||
async def _process_message(self, message_type: str, message_data: Dict[str, Any]):
|
async def _process_message(self, message_type: str, message_data: Dict[str, Any]):
|
||||||
"""处理收到的消息"""
|
"""处理收到的消息"""
|
||||||
logger.trace(f"[MessageProcessor] Processing message: {message_type}")
|
logger.debug(f"[MessageProcessor] Processing message: {message_type}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if message_type == "pong":
|
if message_type == "pong":
|
||||||
@@ -638,13 +637,13 @@ class MessageProcessor:
|
|||||||
await self._send_action_state_response(
|
await self._send_action_state_response(
|
||||||
device_id, action_name, task_id, job_id, "query_action_status", True, 0
|
device_id, action_name, task_id, job_id, "query_action_status", True, 0
|
||||||
)
|
)
|
||||||
logger.trace(f"[MessageProcessor] Job {job_log} can start immediately")
|
logger.info(f"[MessageProcessor] Job {job_log} can start immediately")
|
||||||
else:
|
else:
|
||||||
# 需要排队
|
# 需要排队
|
||||||
await self._send_action_state_response(
|
await self._send_action_state_response(
|
||||||
device_id, action_name, task_id, job_id, "query_action_status", False, 10
|
device_id, action_name, task_id, job_id, "query_action_status", False, 10
|
||||||
)
|
)
|
||||||
logger.trace(f"[MessageProcessor] Job {job_log} queued")
|
logger.info(f"[MessageProcessor] Job {job_log} queued")
|
||||||
|
|
||||||
# 通知QueueProcessor有新的队列更新
|
# 通知QueueProcessor有新的队列更新
|
||||||
if self.queue_processor:
|
if self.queue_processor:
|
||||||
@@ -1129,7 +1128,7 @@ class QueueProcessor:
|
|||||||
success = self.message_processor.send_message(message)
|
success = self.message_processor.send_message(message)
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
if success:
|
if success:
|
||||||
logger.trace(f"[QueueProcessor] Sent busy/need_more for queued job {job_log}")
|
logger.debug(f"[QueueProcessor] Sent busy/need_more for queued job {job_log}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[QueueProcessor] Failed to send busy status for job {job_log}")
|
logger.warning(f"[QueueProcessor] Failed to send busy status for job {job_log}")
|
||||||
|
|
||||||
@@ -1152,7 +1151,7 @@ class QueueProcessor:
|
|||||||
job_info.action_name,
|
job_info.action_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.trace(f"[QueueProcessor] Job {job_log} completed with status: {status}")
|
logger.info(f"[QueueProcessor] Job {job_log} completed with status: {status}")
|
||||||
|
|
||||||
# 结束任务,获取下一个可执行的任务
|
# 结束任务,获取下一个可执行的任务
|
||||||
next_job = self.device_manager.end_job(job_id)
|
next_job = self.device_manager.end_job(job_id)
|
||||||
@@ -1172,8 +1171,8 @@ class QueueProcessor:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
self.message_processor.send_message(message)
|
self.message_processor.send_message(message)
|
||||||
# next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name)
|
next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name)
|
||||||
# logger.debug(f"[QueueProcessor] Notified next job {next_job_log} can start")
|
logger.info(f"[QueueProcessor] Notified next job {next_job_log} can start")
|
||||||
|
|
||||||
# 立即触发下一轮状态检查
|
# 立即触发下一轮状态检查
|
||||||
self.notify_queue_update()
|
self.notify_queue_update()
|
||||||
@@ -1315,7 +1314,7 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
logger.warning(f"[WebSocketClient] Failed to remove job {item.job_id} from HostNode status")
|
logger.warning(f"[WebSocketClient] Failed to remove job {item.job_id} from HostNode status")
|
||||||
|
|
||||||
# logger.debug(f"[WebSocketClient] Intercepting final status for job_id: {item.job_id} - {status}")
|
logger.info(f"[WebSocketClient] Intercepting final status for job_id: {item.job_id} - {status}")
|
||||||
|
|
||||||
# 通知队列处理器job完成(包括timeout的job)
|
# 通知队列处理器job完成(包括timeout的job)
|
||||||
self.queue_processor.handle_job_completed(item.job_id, status)
|
self.queue_processor.handle_job_completed(item.job_id, status)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class BasicConfig:
|
|||||||
startup_json_path = None # 填写绝对路径
|
startup_json_path = None # 填写绝对路径
|
||||||
disable_browser = False # 禁止浏览器自动打开
|
disable_browser = False # 禁止浏览器自动打开
|
||||||
port = 8002 # 本地HTTP服务
|
port = 8002 # 本地HTTP服务
|
||||||
check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性
|
|
||||||
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
||||||
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
|
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,687 +0,0 @@
|
|||||||
"""
|
|
||||||
Virtual Workbench Device - 模拟工作台设备
|
|
||||||
包含:
|
|
||||||
- 1个机械臂 (每次操作3s, 独占锁)
|
|
||||||
- 3个加热台 (每次加热10s, 可并行)
|
|
||||||
|
|
||||||
工作流程:
|
|
||||||
1. A1-A5 物料同时启动,竞争机械臂
|
|
||||||
2. 机械臂将物料移动到空闲加热台
|
|
||||||
3. 加热完成后,机械臂将物料移动到C1-C5
|
|
||||||
|
|
||||||
注意:调用来自线程池,使用 threading.Lock 进行同步
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
from typing import Dict, Any, Optional
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
|
||||||
from threading import Lock, RLock
|
|
||||||
|
|
||||||
from typing_extensions import TypedDict
|
|
||||||
|
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
|
||||||
from unilabos.utils.decorator import not_action
|
|
||||||
|
|
||||||
|
|
||||||
# ============ TypedDict 返回类型定义 ============
|
|
||||||
|
|
||||||
class MoveToHeatingStationResult(TypedDict):
|
|
||||||
"""move_to_heating_station 返回类型"""
|
|
||||||
success: bool
|
|
||||||
station_id: int
|
|
||||||
material_id: str
|
|
||||||
material_number: int
|
|
||||||
message: str
|
|
||||||
|
|
||||||
|
|
||||||
class StartHeatingResult(TypedDict):
|
|
||||||
"""start_heating 返回类型"""
|
|
||||||
success: bool
|
|
||||||
station_id: int
|
|
||||||
material_id: str
|
|
||||||
material_number: int
|
|
||||||
message: str
|
|
||||||
|
|
||||||
|
|
||||||
class MoveToOutputResult(TypedDict):
|
|
||||||
"""move_to_output 返回类型"""
|
|
||||||
success: bool
|
|
||||||
station_id: int
|
|
||||||
material_id: str
|
|
||||||
|
|
||||||
|
|
||||||
class PrepareMaterialsResult(TypedDict):
|
|
||||||
"""prepare_materials 返回类型 - 批量准备物料"""
|
|
||||||
success: bool
|
|
||||||
count: int
|
|
||||||
material_1: int # 物料编号1
|
|
||||||
material_2: int # 物料编号2
|
|
||||||
material_3: int # 物料编号3
|
|
||||||
material_4: int # 物料编号4
|
|
||||||
material_5: int # 物料编号5
|
|
||||||
message: str
|
|
||||||
|
|
||||||
|
|
||||||
# ============ 状态枚举 ============
|
|
||||||
|
|
||||||
class HeatingStationState(Enum):
|
|
||||||
"""加热台状态枚举"""
|
|
||||||
IDLE = "idle" # 空闲
|
|
||||||
OCCUPIED = "occupied" # 已放置物料,等待加热
|
|
||||||
HEATING = "heating" # 加热中
|
|
||||||
COMPLETED = "completed" # 加热完成,等待取走
|
|
||||||
|
|
||||||
|
|
||||||
class ArmState(Enum):
|
|
||||||
"""机械臂状态枚举"""
|
|
||||||
IDLE = "idle" # 空闲
|
|
||||||
BUSY = "busy" # 工作中
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HeatingStation:
|
|
||||||
"""加热台数据结构"""
|
|
||||||
station_id: int
|
|
||||||
state: HeatingStationState = HeatingStationState.IDLE
|
|
||||||
current_material: Optional[str] = None # 当前物料 (如 "A1", "A2")
|
|
||||||
material_number: Optional[int] = None # 物料编号 (1-5)
|
|
||||||
heating_start_time: Optional[float] = None
|
|
||||||
heating_progress: float = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualWorkbench:
|
|
||||||
"""
|
|
||||||
Virtual Workbench Device - 虚拟工作台设备
|
|
||||||
|
|
||||||
模拟一个包含1个机械臂和3个加热台的工作站
|
|
||||||
- 机械臂操作耗时3秒,同一时间只能执行一个操作
|
|
||||||
- 加热台加热耗时10秒,3个加热台可并行工作
|
|
||||||
|
|
||||||
工作流:
|
|
||||||
1. 物料A1-A5并发启动(线程池),竞争机械臂使用权
|
|
||||||
2. 获取机械臂后,查找空闲加热台
|
|
||||||
3. 机械臂将物料放入加热台,开始加热
|
|
||||||
4. 加热完成后,机械臂将物料移动到目标位置Cn
|
|
||||||
"""
|
|
||||||
|
|
||||||
_ros_node: BaseROS2DeviceNode
|
|
||||||
|
|
||||||
# 配置常量
|
|
||||||
ARM_OPERATION_TIME: float = 3.0 # 机械臂操作时间(秒)
|
|
||||||
HEATING_TIME: float = 10.0 # 加热时间(秒)
|
|
||||||
NUM_HEATING_STATIONS: int = 3 # 加热台数量
|
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
|
||||||
# 处理可能的不同调用方式
|
|
||||||
if device_id is None and "id" in kwargs:
|
|
||||||
device_id = kwargs.pop("id")
|
|
||||||
if config is None and "config" in kwargs:
|
|
||||||
config = kwargs.pop("config")
|
|
||||||
|
|
||||||
self.device_id = device_id or "virtual_workbench"
|
|
||||||
self.config = config or {}
|
|
||||||
|
|
||||||
self.logger = logging.getLogger(f"VirtualWorkbench.{self.device_id}")
|
|
||||||
self.data: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
# 从config中获取可配置参数
|
|
||||||
self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", 3.0))
|
|
||||||
self.HEATING_TIME = float(self.config.get("heating_time", 10.0))
|
|
||||||
self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", 3))
|
|
||||||
|
|
||||||
# 机械臂状态和锁 (使用threading.Lock)
|
|
||||||
self._arm_lock = Lock()
|
|
||||||
self._arm_state = ArmState.IDLE
|
|
||||||
self._arm_current_task: Optional[str] = None
|
|
||||||
|
|
||||||
# 加热台状态 (station_id -> HeatingStation) - 立即初始化,不依赖initialize()
|
|
||||||
self._heating_stations: Dict[int, HeatingStation] = {
|
|
||||||
i: HeatingStation(station_id=i)
|
|
||||||
for i in range(1, self.NUM_HEATING_STATIONS + 1)
|
|
||||||
}
|
|
||||||
self._stations_lock = RLock() # 可重入锁,保护加热台状态
|
|
||||||
|
|
||||||
# 任务追踪
|
|
||||||
self._active_tasks: Dict[str, Dict[str, Any]] = {} # material_id -> task_info
|
|
||||||
self._tasks_lock = Lock()
|
|
||||||
|
|
||||||
# 处理其他kwargs参数
|
|
||||||
skip_keys = {"arm_operation_time", "heating_time", "num_heating_stations"}
|
|
||||||
for key, value in kwargs.items():
|
|
||||||
if key not in skip_keys and not hasattr(self, key):
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
self.logger.info(f"=== 虚拟工作台 {self.device_id} 已创建 ===")
|
|
||||||
self.logger.info(
|
|
||||||
f"机械臂操作时间: {self.ARM_OPERATION_TIME}s | "
|
|
||||||
f"加热时间: {self.HEATING_TIME}s | "
|
|
||||||
f"加热台数量: {self.NUM_HEATING_STATIONS}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@not_action
|
|
||||||
def post_init(self, ros_node: BaseROS2DeviceNode):
|
|
||||||
"""ROS节点初始化后回调"""
|
|
||||||
self._ros_node = ros_node
|
|
||||||
|
|
||||||
@not_action
|
|
||||||
def initialize(self) -> bool:
|
|
||||||
"""初始化虚拟工作台"""
|
|
||||||
self.logger.info(f"初始化虚拟工作台 {self.device_id}")
|
|
||||||
|
|
||||||
# 重置加热台状态 (已在__init__中创建,这里重置为初始状态)
|
|
||||||
with self._stations_lock:
|
|
||||||
for station in self._heating_stations.values():
|
|
||||||
station.state = HeatingStationState.IDLE
|
|
||||||
station.current_material = None
|
|
||||||
station.material_number = None
|
|
||||||
station.heating_progress = 0.0
|
|
||||||
|
|
||||||
# 初始化状态
|
|
||||||
self.data.update({
|
|
||||||
"status": "Ready",
|
|
||||||
"arm_state": ArmState.IDLE.value,
|
|
||||||
"arm_current_task": None,
|
|
||||||
"heating_stations": self._get_stations_status(),
|
|
||||||
"active_tasks_count": 0,
|
|
||||||
"message": "工作台就绪",
|
|
||||||
})
|
|
||||||
|
|
||||||
self.logger.info(f"工作台初始化完成: {self.NUM_HEATING_STATIONS}个加热台就绪")
|
|
||||||
return True
|
|
||||||
|
|
||||||
@not_action
|
|
||||||
def cleanup(self) -> bool:
|
|
||||||
"""清理虚拟工作台"""
|
|
||||||
self.logger.info(f"清理虚拟工作台 {self.device_id}")
|
|
||||||
|
|
||||||
self._arm_state = ArmState.IDLE
|
|
||||||
self._arm_current_task = None
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
self._heating_stations.clear()
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks.clear()
|
|
||||||
|
|
||||||
self.data.update({
|
|
||||||
"status": "Offline",
|
|
||||||
"arm_state": ArmState.IDLE.value,
|
|
||||||
"heating_stations": {},
|
|
||||||
"message": "工作台已关闭",
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_stations_status(self) -> Dict[int, Dict[str, Any]]:
|
|
||||||
"""获取所有加热台状态"""
|
|
||||||
with self._stations_lock:
|
|
||||||
return {
|
|
||||||
station_id: {
|
|
||||||
"state": station.state.value,
|
|
||||||
"current_material": station.current_material,
|
|
||||||
"material_number": station.material_number,
|
|
||||||
"heating_progress": station.heating_progress,
|
|
||||||
}
|
|
||||||
for station_id, station in self._heating_stations.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
def _update_data_status(self, message: Optional[str] = None):
|
|
||||||
"""更新状态数据"""
|
|
||||||
self.data.update({
|
|
||||||
"arm_state": self._arm_state.value,
|
|
||||||
"arm_current_task": self._arm_current_task,
|
|
||||||
"heating_stations": self._get_stations_status(),
|
|
||||||
"active_tasks_count": len(self._active_tasks),
|
|
||||||
})
|
|
||||||
if message:
|
|
||||||
self.data["message"] = message
|
|
||||||
|
|
||||||
def _find_available_heating_station(self) -> Optional[int]:
|
|
||||||
"""查找空闲的加热台
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
空闲加热台ID,如果没有则返回None
|
|
||||||
"""
|
|
||||||
with self._stations_lock:
|
|
||||||
for station_id, station in self._heating_stations.items():
|
|
||||||
if station.state == HeatingStationState.IDLE:
|
|
||||||
return station_id
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _acquire_arm(self, task_description: str) -> bool:
|
|
||||||
"""获取机械臂使用权(阻塞直到获取)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
task_description: 任务描述,用于日志
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
是否成功获取
|
|
||||||
"""
|
|
||||||
self.logger.info(f"[{task_description}] 等待获取机械臂...")
|
|
||||||
|
|
||||||
# 阻塞等待获取锁
|
|
||||||
self._arm_lock.acquire()
|
|
||||||
|
|
||||||
self._arm_state = ArmState.BUSY
|
|
||||||
self._arm_current_task = task_description
|
|
||||||
self._update_data_status(f"机械臂执行: {task_description}")
|
|
||||||
|
|
||||||
self.logger.info(f"[{task_description}] 成功获取机械臂使用权")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _release_arm(self):
|
|
||||||
"""释放机械臂"""
|
|
||||||
task = self._arm_current_task
|
|
||||||
self._arm_state = ArmState.IDLE
|
|
||||||
self._arm_current_task = None
|
|
||||||
self._arm_lock.release()
|
|
||||||
self._update_data_status(f"机械臂已释放 (完成: {task})")
|
|
||||||
self.logger.info(f"机械臂已释放 (完成: {task})")
|
|
||||||
|
|
||||||
def prepare_materials(
|
|
||||||
self,
|
|
||||||
count: int = 5,
|
|
||||||
) -> PrepareMaterialsResult:
|
|
||||||
"""
|
|
||||||
批量准备物料 - 虚拟起始节点
|
|
||||||
|
|
||||||
作为工作流的起始节点,生成指定数量的物料编号供后续节点使用。
|
|
||||||
输出5个handle (material_1 ~ material_5),分别对应实验1~5。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
count: 待生成的物料数量,默认5 (生成 A1-A5)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PrepareMaterialsResult: 包含 material_1 ~ material_5 用于传递给 move_to_heating_station
|
|
||||||
"""
|
|
||||||
# 生成物料列表 A1 - A{count}
|
|
||||||
materials = [i for i in range(1, count + 1)]
|
|
||||||
|
|
||||||
self.logger.info(
|
|
||||||
f"[准备物料] 生成 {count} 个物料: "
|
|
||||||
f"A1-A{count} -> material_1~material_{count}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"count": count,
|
|
||||||
"material_1": materials[0] if len(materials) > 0 else 0,
|
|
||||||
"material_2": materials[1] if len(materials) > 1 else 0,
|
|
||||||
"material_3": materials[2] if len(materials) > 2 else 0,
|
|
||||||
"material_4": materials[3] if len(materials) > 3 else 0,
|
|
||||||
"material_5": materials[4] if len(materials) > 4 else 0,
|
|
||||||
"message": f"已准备 {count} 个物料: A1-A{count}",
|
|
||||||
}
|
|
||||||
|
|
||||||
def move_to_heating_station(
|
|
||||||
self,
|
|
||||||
material_number: int,
|
|
||||||
) -> MoveToHeatingStationResult:
|
|
||||||
"""
|
|
||||||
将物料从An位置移动到加热台
|
|
||||||
|
|
||||||
多线程并发调用时,会竞争机械臂使用权,并自动查找空闲加热台
|
|
||||||
|
|
||||||
Args:
|
|
||||||
material_number: 物料编号 (1-5)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MoveToHeatingStationResult: 包含 station_id, material_number 等用于传递给下一个节点
|
|
||||||
"""
|
|
||||||
# 根据物料编号生成物料ID
|
|
||||||
material_id = f"A{material_number}"
|
|
||||||
task_desc = f"移动{material_id}到加热台"
|
|
||||||
self.logger.info(f"[任务] {task_desc} - 开始执行")
|
|
||||||
|
|
||||||
# 记录任务
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks[material_id] = {
|
|
||||||
"status": "waiting_for_arm",
|
|
||||||
"start_time": time.time(),
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 步骤1: 等待获取机械臂使用权(竞争)
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks[material_id]["status"] = "waiting_for_arm"
|
|
||||||
self._acquire_arm(task_desc)
|
|
||||||
|
|
||||||
# 步骤2: 查找空闲加热台
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks[material_id]["status"] = "finding_station"
|
|
||||||
station_id = None
|
|
||||||
|
|
||||||
# 循环等待直到找到空闲加热台
|
|
||||||
while station_id is None:
|
|
||||||
station_id = self._find_available_heating_station()
|
|
||||||
if station_id is None:
|
|
||||||
self.logger.info(f"[{material_id}] 没有空闲加热台,等待中...")
|
|
||||||
# 释放机械臂,等待后重试
|
|
||||||
self._release_arm()
|
|
||||||
time.sleep(0.5)
|
|
||||||
self._acquire_arm(task_desc)
|
|
||||||
|
|
||||||
# 步骤3: 占用加热台 - 立即标记为OCCUPIED,防止其他任务选择同一加热台
|
|
||||||
with self._stations_lock:
|
|
||||||
self._heating_stations[station_id].state = HeatingStationState.OCCUPIED
|
|
||||||
self._heating_stations[station_id].current_material = material_id
|
|
||||||
self._heating_stations[station_id].material_number = material_number
|
|
||||||
|
|
||||||
# 步骤4: 模拟机械臂移动操作 (3秒)
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks[material_id]["status"] = "arm_moving"
|
|
||||||
self._active_tasks[material_id]["assigned_station"] = station_id
|
|
||||||
self.logger.info(f"[{material_id}] 机械臂正在移动到加热台{station_id}...")
|
|
||||||
|
|
||||||
time.sleep(self.ARM_OPERATION_TIME)
|
|
||||||
|
|
||||||
# 步骤5: 放入加热台完成
|
|
||||||
self._update_data_status(f"{material_id}已放入加热台{station_id}")
|
|
||||||
self.logger.info(f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)")
|
|
||||||
|
|
||||||
# 释放机械臂
|
|
||||||
self._release_arm()
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks[material_id]["status"] = "placed_on_station"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": material_id,
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"{material_id}已成功移动到加热台{station_id}",
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"[{material_id}] 移动失败: {str(e)}")
|
|
||||||
if self._arm_lock.locked():
|
|
||||||
self._release_arm()
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": -1,
|
|
||||||
"material_id": material_id,
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"移动失败: {str(e)}",
|
|
||||||
}
|
|
||||||
|
|
||||||
def start_heating(
|
|
||||||
self,
|
|
||||||
station_id: int,
|
|
||||||
material_number: int,
|
|
||||||
) -> StartHeatingResult:
|
|
||||||
"""
|
|
||||||
启动指定加热台的加热程序
|
|
||||||
|
|
||||||
Args:
|
|
||||||
station_id: 加热台ID (1-3),从 move_to_heating_station 的 handle 传入
|
|
||||||
material_number: 物料编号,从 move_to_heating_station 的 handle 传入
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
StartHeatingResult: 包含 station_id, material_number 等用于传递给下一个节点
|
|
||||||
"""
|
|
||||||
self.logger.info(f"[加热台{station_id}] 开始加热")
|
|
||||||
|
|
||||||
if station_id not in self._heating_stations:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": "",
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"无效的加热台ID: {station_id}",
|
|
||||||
}
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations[station_id]
|
|
||||||
|
|
||||||
if station.current_material is None:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": "",
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"加热台{station_id}上没有物料",
|
|
||||||
}
|
|
||||||
|
|
||||||
if station.state == HeatingStationState.HEATING:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": station.current_material,
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"加热台{station_id}已经在加热中",
|
|
||||||
}
|
|
||||||
|
|
||||||
material_id = station.current_material
|
|
||||||
|
|
||||||
# 开始加热
|
|
||||||
station.state = HeatingStationState.HEATING
|
|
||||||
station.heating_start_time = time.time()
|
|
||||||
station.heating_progress = 0.0
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
if material_id in self._active_tasks:
|
|
||||||
self._active_tasks[material_id]["status"] = "heating"
|
|
||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}开始加热{material_id}")
|
|
||||||
|
|
||||||
# 模拟加热过程 (10秒)
|
|
||||||
start_time = time.time()
|
|
||||||
while True:
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
progress = min(100.0, (elapsed / self.HEATING_TIME) * 100)
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
self._heating_stations[station_id].heating_progress = progress
|
|
||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
|
|
||||||
|
|
||||||
if elapsed >= self.HEATING_TIME:
|
|
||||||
break
|
|
||||||
|
|
||||||
time.sleep(1.0)
|
|
||||||
|
|
||||||
# 加热完成
|
|
||||||
with self._stations_lock:
|
|
||||||
self._heating_stations[station_id].state = HeatingStationState.COMPLETED
|
|
||||||
self._heating_stations[station_id].heating_progress = 100.0
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
if material_id in self._active_tasks:
|
|
||||||
self._active_tasks[material_id]["status"] = "heating_completed"
|
|
||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}加热完成")
|
|
||||||
self.logger.info(f"[加热台{station_id}] {material_id}加热完成 (用时{self.HEATING_TIME}s)")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": material_id,
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"加热台{station_id}加热完成",
|
|
||||||
}
|
|
||||||
|
|
||||||
def move_to_output(
|
|
||||||
self,
|
|
||||||
station_id: int,
|
|
||||||
material_number: int,
|
|
||||||
) -> MoveToOutputResult:
|
|
||||||
"""
|
|
||||||
将物料从加热台移动到输出位置Cn
|
|
||||||
|
|
||||||
Args:
|
|
||||||
station_id: 加热台ID (1-3),从 start_heating 的 handle 传入
|
|
||||||
material_number: 物料编号,从 start_heating 的 handle 传入,用于确定输出位置 Cn
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MoveToOutputResult: 包含执行结果
|
|
||||||
"""
|
|
||||||
output_number = material_number # 物料编号决定输出位置
|
|
||||||
|
|
||||||
if station_id not in self._heating_stations:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": "",
|
|
||||||
"output_position": f"C{output_number}",
|
|
||||||
"message": f"无效的加热台ID: {station_id}",
|
|
||||||
}
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations[station_id]
|
|
||||||
material_id = station.current_material
|
|
||||||
|
|
||||||
if material_id is None:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": "",
|
|
||||||
"output_position": f"C{output_number}",
|
|
||||||
"message": f"加热台{station_id}上没有物料",
|
|
||||||
}
|
|
||||||
|
|
||||||
if station.state != HeatingStationState.COMPLETED:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": material_id,
|
|
||||||
"output_position": f"C{output_number}",
|
|
||||||
"message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})",
|
|
||||||
}
|
|
||||||
|
|
||||||
output_position = f"C{output_number}"
|
|
||||||
task_desc = f"从加热台{station_id}移动{material_id}到{output_position}"
|
|
||||||
self.logger.info(f"[任务] {task_desc}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self._tasks_lock:
|
|
||||||
if material_id in self._active_tasks:
|
|
||||||
self._active_tasks[material_id]["status"] = "waiting_for_arm_output"
|
|
||||||
|
|
||||||
# 获取机械臂
|
|
||||||
self._acquire_arm(task_desc)
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
if material_id in self._active_tasks:
|
|
||||||
self._active_tasks[material_id]["status"] = "arm_moving_to_output"
|
|
||||||
|
|
||||||
# 模拟机械臂操作 (3秒)
|
|
||||||
self.logger.info(f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}...")
|
|
||||||
time.sleep(self.ARM_OPERATION_TIME)
|
|
||||||
|
|
||||||
# 清空加热台
|
|
||||||
with self._stations_lock:
|
|
||||||
self._heating_stations[station_id].state = HeatingStationState.IDLE
|
|
||||||
self._heating_stations[station_id].current_material = None
|
|
||||||
self._heating_stations[station_id].material_number = None
|
|
||||||
self._heating_stations[station_id].heating_progress = 0.0
|
|
||||||
self._heating_stations[station_id].heating_start_time = None
|
|
||||||
|
|
||||||
# 释放机械臂
|
|
||||||
self._release_arm()
|
|
||||||
|
|
||||||
# 任务完成
|
|
||||||
with self._tasks_lock:
|
|
||||||
if material_id in self._active_tasks:
|
|
||||||
self._active_tasks[material_id]["status"] = "completed"
|
|
||||||
self._active_tasks[material_id]["end_time"] = time.time()
|
|
||||||
|
|
||||||
self._update_data_status(f"{material_id}已移动到{output_position}")
|
|
||||||
self.logger.info(f"[{material_id}] 已成功移动到{output_position} (用时{self.ARM_OPERATION_TIME}s)")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": material_id,
|
|
||||||
"output_position": output_position,
|
|
||||||
"message": f"{material_id}已成功移动到{output_position}",
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"移动到输出位置失败: {str(e)}")
|
|
||||||
if self._arm_lock.locked():
|
|
||||||
self._release_arm()
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": "",
|
|
||||||
"output_position": output_position,
|
|
||||||
"message": f"移动失败: {str(e)}",
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============ 状态属性 ============
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
return self.data.get("status", "Unknown")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def arm_state(self) -> str:
|
|
||||||
return self._arm_state.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def arm_current_task(self) -> str:
|
|
||||||
return self._arm_current_task or ""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_station_1_state(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(1)
|
|
||||||
return station.state.value if station else "unknown"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_station_1_material(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(1)
|
|
||||||
return station.current_material or "" if station else ""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_station_1_progress(self) -> float:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(1)
|
|
||||||
return station.heating_progress if station else 0.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_station_2_state(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(2)
|
|
||||||
return station.state.value if station else "unknown"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_station_2_material(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(2)
|
|
||||||
return station.current_material or "" if station else ""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_station_2_progress(self) -> float:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(2)
|
|
||||||
return station.heating_progress if station else 0.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_station_3_state(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(3)
|
|
||||||
return station.state.value if station else "unknown"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_station_3_material(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(3)
|
|
||||||
return station.current_material or "" if station else ""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_station_3_progress(self) -> float:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(3)
|
|
||||||
return station.heating_progress if station else 0.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def active_tasks_count(self) -> int:
|
|
||||||
with self._tasks_lock:
|
|
||||||
return len(self._active_tasks)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self) -> str:
|
|
||||||
return self.data.get("message", "")
|
|
||||||
589
unilabos/registry/devices/bioyond.yaml
Normal file
589
unilabos/registry/devices/bioyond.yaml
Normal file
@@ -0,0 +1,589 @@
|
|||||||
|
workstation.bioyond_dispensing_station:
|
||||||
|
category:
|
||||||
|
- workstation
|
||||||
|
- bioyond
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
auto-batch_create_90_10_vial_feeding_tasks:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
delay_time: null
|
||||||
|
hold_m_name: null
|
||||||
|
liquid_material_name: NMP
|
||||||
|
speed: null
|
||||||
|
temperature: null
|
||||||
|
titration: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
delay_time:
|
||||||
|
type: string
|
||||||
|
hold_m_name:
|
||||||
|
type: string
|
||||||
|
liquid_material_name:
|
||||||
|
default: NMP
|
||||||
|
type: string
|
||||||
|
speed:
|
||||||
|
type: string
|
||||||
|
temperature:
|
||||||
|
type: string
|
||||||
|
titration:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- titration
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: batch_create_90_10_vial_feeding_tasks参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-batch_create_diamine_solution_tasks:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
delay_time: null
|
||||||
|
liquid_material_name: NMP
|
||||||
|
solutions: null
|
||||||
|
speed: null
|
||||||
|
temperature: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
delay_time:
|
||||||
|
type: string
|
||||||
|
liquid_material_name:
|
||||||
|
default: NMP
|
||||||
|
type: string
|
||||||
|
solutions:
|
||||||
|
type: string
|
||||||
|
speed:
|
||||||
|
type: string
|
||||||
|
temperature:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- solutions
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: batch_create_diamine_solution_tasks参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-brief_step_parameters:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
data: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: brief_step_parameters参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-compute_experiment_design:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
m_tot: '70'
|
||||||
|
ratio: null
|
||||||
|
titration_percent: '0.03'
|
||||||
|
wt_percent: '0.25'
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
m_tot:
|
||||||
|
default: '70'
|
||||||
|
type: string
|
||||||
|
ratio:
|
||||||
|
type: object
|
||||||
|
titration_percent:
|
||||||
|
default: '0.03'
|
||||||
|
type: string
|
||||||
|
wt_percent:
|
||||||
|
default: '0.25'
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- ratio
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
feeding_order:
|
||||||
|
items: {}
|
||||||
|
title: Feeding Order
|
||||||
|
type: array
|
||||||
|
return_info:
|
||||||
|
title: Return Info
|
||||||
|
type: string
|
||||||
|
solutions:
|
||||||
|
items: {}
|
||||||
|
title: Solutions
|
||||||
|
type: array
|
||||||
|
solvents:
|
||||||
|
additionalProperties: true
|
||||||
|
title: Solvents
|
||||||
|
type: object
|
||||||
|
titration:
|
||||||
|
additionalProperties: true
|
||||||
|
title: Titration
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- solutions
|
||||||
|
- titration
|
||||||
|
- solvents
|
||||||
|
- feeding_order
|
||||||
|
- return_info
|
||||||
|
title: ComputeExperimentDesignReturn
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: compute_experiment_design参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-process_order_finish_report:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
report_request: null
|
||||||
|
used_materials: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
report_request:
|
||||||
|
type: string
|
||||||
|
used_materials:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- report_request
|
||||||
|
- used_materials
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: process_order_finish_report参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-project_order_report:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
order_id: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- order_id
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: project_order_report参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-query_resource_by_name:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
material_name: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
material_name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- material_name
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: query_resource_by_name参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-transfer_materials_to_reaction_station:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
target_device_id: null
|
||||||
|
transfer_groups: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
target_device_id:
|
||||||
|
type: string
|
||||||
|
transfer_groups:
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- target_device_id
|
||||||
|
- transfer_groups
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: transfer_materials_to_reaction_station参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-wait_for_multiple_orders_and_get_reports:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
batch_create_result: null
|
||||||
|
check_interval: 10
|
||||||
|
timeout: 7200
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
batch_create_result:
|
||||||
|
type: string
|
||||||
|
check_interval:
|
||||||
|
default: 10
|
||||||
|
type: integer
|
||||||
|
timeout:
|
||||||
|
default: 7200
|
||||||
|
type: integer
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: wait_for_multiple_orders_and_get_reports参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-workflow_sample_locations:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
workflow_id: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
workflow_id:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- workflow_id
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: workflow_sample_locations参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
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
|
||||||
|
deck:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- config
|
||||||
|
- deck
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,135 +5,6 @@ bioyond_dispensing_station:
|
|||||||
- bioyond_dispensing_station
|
- bioyond_dispensing_station
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
action_value_mappings:
|
||||||
auto-brief_step_parameters:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
data: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- data
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: brief_step_parameters参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-process_order_finish_report:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
report_request: null
|
|
||||||
used_materials: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
report_request:
|
|
||||||
type: string
|
|
||||||
used_materials:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- report_request
|
|
||||||
- used_materials
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: process_order_finish_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-project_order_report:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
order_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
order_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- order_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: project_order_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-query_resource_by_name:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
material_name: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
material_name:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- material_name
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: query_resource_by_name参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-workflow_sample_locations:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
workflow_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- workflow_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: workflow_sample_locations参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
batch_create_90_10_vial_feeding_tasks:
|
batch_create_90_10_vial_feeding_tasks:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ coincellassemblyworkstation_device:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
bottle_num:
|
bottle_num:
|
||||||
type: string
|
type: integer
|
||||||
required:
|
required:
|
||||||
- bottle_num
|
- bottle_num
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -49,7 +49,32 @@ opcua_example:
|
|||||||
title: load_config参数
|
title: load_config参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-refresh_node_values:
|
auto-post_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
ros_node: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
ros_node:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- ros_node
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: post_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-print_cache_stats:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
@@ -67,7 +92,32 @@ opcua_example:
|
|||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: refresh_node_values参数
|
title: print_cache_stats参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-read_node:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
node_name: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
node_name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- node_name
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: read_node参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-set_node_value:
|
auto-set_node_value:
|
||||||
@@ -99,50 +149,9 @@ opcua_example:
|
|||||||
title: set_node_value参数
|
title: set_node_value参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-start_node_refresh:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: start_node_refresh参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-stop_node_refresh:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: stop_node_refresh参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
module: unilabos.device_comms.opcua_client.client:OpcUaClient
|
module: unilabos.device_comms.opcua_client.client:OpcUaClient
|
||||||
status_types:
|
status_types:
|
||||||
|
cache_stats: dict
|
||||||
node_value: String
|
node_value: String
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
@@ -152,15 +161,23 @@ opcua_example:
|
|||||||
init_param_schema:
|
init_param_schema:
|
||||||
config:
|
config:
|
||||||
properties:
|
properties:
|
||||||
|
cache_timeout:
|
||||||
|
default: 5.0
|
||||||
|
type: number
|
||||||
config_path:
|
config_path:
|
||||||
type: string
|
type: string
|
||||||
|
deck:
|
||||||
|
type: string
|
||||||
password:
|
password:
|
||||||
type: string
|
type: string
|
||||||
refresh_interval:
|
subscription_interval:
|
||||||
default: 1.0
|
default: 500
|
||||||
type: number
|
type: integer
|
||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
|
use_subscription:
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
@@ -168,9 +185,12 @@ opcua_example:
|
|||||||
type: object
|
type: object
|
||||||
data:
|
data:
|
||||||
properties:
|
properties:
|
||||||
|
cache_stats:
|
||||||
|
type: object
|
||||||
node_value:
|
node_value:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- node_value
|
- node_value
|
||||||
|
- cache_stats
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -58,313 +58,6 @@ reaction_station.bioyond:
|
|||||||
title: add_time_constraint参数
|
title: add_time_constraint参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-clear_workflows:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: clear_workflows参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-create_order:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
json_str: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
json_str:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- json_str
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: create_order参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-hard_delete_merged_workflows:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
workflow_ids: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_ids:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- workflow_ids
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: hard_delete_merged_workflows参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-merge_workflow_with_parameters:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
json_str: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
json_str:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- json_str
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: merge_workflow_with_parameters参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-process_temperature_cutoff_report:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
report_request: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
report_request:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- report_request
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: process_temperature_cutoff_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-process_web_workflows:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
web_workflow_json: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
web_workflow_json:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- web_workflow_json
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: process_web_workflows参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-set_reactor_temperature:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
reactor_id: null
|
|
||||||
temperature: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
reactor_id:
|
|
||||||
type: integer
|
|
||||||
temperature:
|
|
||||||
type: number
|
|
||||||
required:
|
|
||||||
- reactor_id
|
|
||||||
- temperature
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: set_reactor_temperature参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-skip_titration_steps:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
preintake_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
preintake_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- preintake_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: skip_titration_steps参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-sync_workflow_sequence_from_bioyond:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: sync_workflow_sequence_from_bioyond参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-wait_for_multiple_orders_and_get_reports:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
batch_create_result: null
|
|
||||||
check_interval: 10
|
|
||||||
timeout: 7200
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
batch_create_result:
|
|
||||||
type: string
|
|
||||||
check_interval:
|
|
||||||
default: 10
|
|
||||||
type: integer
|
|
||||||
timeout:
|
|
||||||
default: 7200
|
|
||||||
type: integer
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: wait_for_multiple_orders_and_get_reports参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-workflow_sequence:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
value: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
value:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- value
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: workflow_sequence参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-workflow_step_query:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
workflow_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- workflow_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: workflow_step_query参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
clean_all_server_workflows:
|
clean_all_server_workflows:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -981,7 +674,17 @@ reaction_station.bioyond:
|
|||||||
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactionStation
|
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactionStation
|
||||||
protocol_type: []
|
protocol_type: []
|
||||||
status_types:
|
status_types:
|
||||||
workflow_sequence: str
|
average_viscosity: Float64
|
||||||
|
force: Float64
|
||||||
|
in_temperature: Float64
|
||||||
|
out_temperature: Float64
|
||||||
|
pt100_temperature: Float64
|
||||||
|
sensor_average_temperature: Float64
|
||||||
|
setting_temperature: Float64
|
||||||
|
speed: Float64
|
||||||
|
target_temperature: Float64
|
||||||
|
viscosity: Float64
|
||||||
|
workflow_sequence: String
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: Bioyond反应站
|
description: Bioyond反应站
|
||||||
@@ -1001,7 +704,9 @@ reaction_station.bioyond:
|
|||||||
data:
|
data:
|
||||||
properties:
|
properties:
|
||||||
workflow_sequence:
|
workflow_sequence:
|
||||||
type: string
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
required:
|
required:
|
||||||
- workflow_sequence
|
- workflow_sequence
|
||||||
type: object
|
type: object
|
||||||
@@ -1011,34 +716,19 @@ reaction_station.reactor:
|
|||||||
- reactor
|
- reactor
|
||||||
- reaction_station_bioyond
|
- reaction_station_bioyond
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
action_value_mappings: {}
|
||||||
auto-update_metrics:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
payload: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
payload:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- payload
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: update_metrics参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactor
|
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactor
|
||||||
status_types: {}
|
status_types:
|
||||||
|
average_viscosity: Float64
|
||||||
|
force: Float64
|
||||||
|
in_temperature: Float64
|
||||||
|
out_temperature: Float64
|
||||||
|
pt100_temperature: Float64
|
||||||
|
sensor_average_temperature: Float64
|
||||||
|
setting_temperature: Float64
|
||||||
|
speed: Float64
|
||||||
|
target_temperature: Float64
|
||||||
|
viscosity: Float64
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: 反应站子设备-反应器
|
description: 反应站子设备-反应器
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -71,20 +71,6 @@ class Registry:
|
|||||||
|
|
||||||
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
|
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
|
||||||
|
|
||||||
# 获取 HostNode 类的增强信息,用于自动生成 action schema
|
|
||||||
host_node_enhanced_info = get_enhanced_class_info(
|
|
||||||
"unilabos.ros.nodes.presets.host_node:HostNode", use_dynamic=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# 为 test_latency 生成 schema,保留原有 description
|
|
||||||
test_latency_method_info = host_node_enhanced_info.get("action_methods", {}).get("test_latency", {})
|
|
||||||
test_latency_schema = self._generate_unilab_json_command_schema(
|
|
||||||
test_latency_method_info.get("args", []),
|
|
||||||
"test_latency",
|
|
||||||
test_latency_method_info.get("return_annotation"),
|
|
||||||
)
|
|
||||||
test_latency_schema["description"] = "用于测试延迟的动作,返回延迟时间和时间差。"
|
|
||||||
|
|
||||||
self.device_type_registry.update(
|
self.device_type_registry.update(
|
||||||
{
|
{
|
||||||
"host_node": {
|
"host_node": {
|
||||||
@@ -163,22 +149,18 @@ class Registry:
|
|||||||
"res_id": "unilabos_resources", # 将当前实验室的全部物料id作为下拉框可选择
|
"res_id": "unilabos_resources", # 将当前实验室的全部物料id作为下拉框可选择
|
||||||
"device_id": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择
|
"device_id": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择
|
||||||
"parent": "unilabos_nodes", # 将当前实验室的设备/物料作为下拉框可选择
|
"parent": "unilabos_nodes", # 将当前实验室的设备/物料作为下拉框可选择
|
||||||
|
"class_name": "unilabos_class",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"test_latency": {
|
"test_latency": {
|
||||||
"type": (
|
"type": self.EmptyIn,
|
||||||
"UniLabJsonCommandAsync"
|
|
||||||
if test_latency_method_info.get("is_async", False)
|
|
||||||
else "UniLabJsonCommand"
|
|
||||||
),
|
|
||||||
"goal": {},
|
"goal": {},
|
||||||
"feedback": {},
|
"feedback": {},
|
||||||
"result": {},
|
"result": {},
|
||||||
"schema": test_latency_schema,
|
"schema": ros_action_to_json_schema(
|
||||||
"goal_default": {
|
self.EmptyIn, "用于测试延迟的动作,返回延迟时间和时间差。"
|
||||||
arg["name"]: arg["default"]
|
),
|
||||||
for arg in test_latency_method_info.get("args", [])
|
"goal_default": {},
|
||||||
},
|
|
||||||
"handles": {},
|
"handles": {},
|
||||||
},
|
},
|
||||||
"auto-test_resource": {
|
"auto-test_resource": {
|
||||||
@@ -498,11 +480,7 @@ class Registry:
|
|||||||
return status_schema
|
return status_schema
|
||||||
|
|
||||||
def _generate_unilab_json_command_schema(
|
def _generate_unilab_json_command_schema(
|
||||||
self,
|
self, method_args: List[Dict[str, Any]], method_name: str, return_annotation: Any = None
|
||||||
method_args: List[Dict[str, Any]],
|
|
||||||
method_name: str,
|
|
||||||
return_annotation: Any = None,
|
|
||||||
previous_schema: Dict[str, Any] | None = None,
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
根据UniLabJsonCommand方法信息生成JSON Schema,暂不支持嵌套类型
|
根据UniLabJsonCommand方法信息生成JSON Schema,暂不支持嵌套类型
|
||||||
@@ -511,7 +489,6 @@ class Registry:
|
|||||||
method_args: 方法信息字典,包含args等
|
method_args: 方法信息字典,包含args等
|
||||||
method_name: 方法名称
|
method_name: 方法名称
|
||||||
return_annotation: 返回类型注解,用于生成result schema(仅支持TypedDict)
|
return_annotation: 返回类型注解,用于生成result schema(仅支持TypedDict)
|
||||||
previous_schema: 之前的 schema,用于保留 goal/feedback/result 下一级字段的 description
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON Schema格式的参数schema
|
JSON Schema格式的参数schema
|
||||||
@@ -545,7 +522,7 @@ class Registry:
|
|||||||
if return_annotation is not None and self._is_typed_dict(return_annotation):
|
if return_annotation is not None and self._is_typed_dict(return_annotation):
|
||||||
result_schema = self._generate_typed_dict_result_schema(return_annotation)
|
result_schema = self._generate_typed_dict_result_schema(return_annotation)
|
||||||
|
|
||||||
final_schema = {
|
return {
|
||||||
"title": f"{method_name}参数",
|
"title": f"{method_name}参数",
|
||||||
"description": f"",
|
"description": f"",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -553,40 +530,6 @@ class Registry:
|
|||||||
"required": ["goal"],
|
"required": ["goal"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# 保留之前 schema 中 goal/feedback/result 下一级字段的 description
|
|
||||||
if previous_schema:
|
|
||||||
self._preserve_field_descriptions(final_schema, previous_schema)
|
|
||||||
|
|
||||||
return final_schema
|
|
||||||
|
|
||||||
def _preserve_field_descriptions(self, new_schema: Dict[str, Any], previous_schema: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
保留之前 schema 中 goal/feedback/result 下一级字段的 description 和 title
|
|
||||||
|
|
||||||
Args:
|
|
||||||
new_schema: 新生成的 schema(会被修改)
|
|
||||||
previous_schema: 之前的 schema
|
|
||||||
"""
|
|
||||||
for section in ["goal", "feedback", "result"]:
|
|
||||||
new_section = new_schema.get("properties", {}).get(section, {})
|
|
||||||
prev_section = previous_schema.get("properties", {}).get(section, {})
|
|
||||||
|
|
||||||
if not new_section or not prev_section:
|
|
||||||
continue
|
|
||||||
|
|
||||||
new_props = new_section.get("properties", {})
|
|
||||||
prev_props = prev_section.get("properties", {})
|
|
||||||
|
|
||||||
for field_name, field_schema in new_props.items():
|
|
||||||
if field_name in prev_props:
|
|
||||||
prev_field = prev_props[field_name]
|
|
||||||
# 保留字段的 description
|
|
||||||
if "description" in prev_field and prev_field["description"]:
|
|
||||||
field_schema["description"] = prev_field["description"]
|
|
||||||
# 保留字段的 title(用户自定义的中文名)
|
|
||||||
if "title" in prev_field and prev_field["title"]:
|
|
||||||
field_schema["title"] = prev_field["title"]
|
|
||||||
|
|
||||||
def _is_typed_dict(self, annotation: Any) -> bool:
|
def _is_typed_dict(self, annotation: Any) -> bool:
|
||||||
"""
|
"""
|
||||||
检查类型注解是否是TypedDict
|
检查类型注解是否是TypedDict
|
||||||
@@ -755,10 +698,13 @@ class Registry:
|
|||||||
sorted(device_config["class"]["status_types"].items())
|
sorted(device_config["class"]["status_types"].items())
|
||||||
)
|
)
|
||||||
if complete_registry:
|
if complete_registry:
|
||||||
# 保存原有的 action 配置(用于保留 schema 的 description 和 handles 等)
|
# 保存原有的description信息
|
||||||
old_action_configs = {}
|
old_descriptions = {}
|
||||||
for action_name, action_config in device_config["class"]["action_value_mappings"].items():
|
for action_name, action_config in device_config["class"]["action_value_mappings"].items():
|
||||||
old_action_configs[action_name] = action_config
|
if "description" in action_config.get("schema", {}):
|
||||||
|
description = action_config["schema"]["description"]
|
||||||
|
if len(description):
|
||||||
|
old_descriptions[action_name] = action_config["schema"]["description"]
|
||||||
|
|
||||||
device_config["class"]["action_value_mappings"] = {
|
device_config["class"]["action_value_mappings"] = {
|
||||||
k: v
|
k: v
|
||||||
@@ -774,15 +720,10 @@ class Registry:
|
|||||||
"feedback": {},
|
"feedback": {},
|
||||||
"result": {},
|
"result": {},
|
||||||
"schema": self._generate_unilab_json_command_schema(
|
"schema": self._generate_unilab_json_command_schema(
|
||||||
v["args"],
|
v["args"], k, v.get("return_annotation")
|
||||||
k,
|
|
||||||
v.get("return_annotation"),
|
|
||||||
# 传入旧的 schema 以保留字段 description
|
|
||||||
old_action_configs.get(f"auto-{k}", {}).get("schema"),
|
|
||||||
),
|
),
|
||||||
"goal_default": {i["name"]: i["default"] for i in v["args"]},
|
"goal_default": {i["name"]: i["default"] for i in v["args"]},
|
||||||
# 保留原有的 handles 配置
|
"handles": [],
|
||||||
"handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []),
|
|
||||||
"placeholder_keys": {
|
"placeholder_keys": {
|
||||||
i["name"]: (
|
i["name"]: (
|
||||||
"unilabos_resources"
|
"unilabos_resources"
|
||||||
@@ -806,14 +747,12 @@ class Registry:
|
|||||||
if k not in device_config["class"]["action_value_mappings"]
|
if k not in device_config["class"]["action_value_mappings"]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# 恢复原有的 description 信息(非 auto- 开头的动作)
|
# 恢复原有的description信息(auto开头的不修改)
|
||||||
for action_name, old_config in old_action_configs.items():
|
for action_name, description in old_descriptions.items():
|
||||||
if action_name in device_config["class"]["action_value_mappings"]: # 有一些会被删除
|
if action_name in device_config["class"]["action_value_mappings"]: # 有一些会被删除
|
||||||
old_schema = old_config.get("schema", {})
|
device_config["class"]["action_value_mappings"][action_name]["schema"][
|
||||||
if "description" in old_schema and old_schema["description"]:
|
"description"
|
||||||
device_config["class"]["action_value_mappings"][action_name]["schema"][
|
] = description
|
||||||
"description"
|
|
||||||
] = old_schema["description"]
|
|
||||||
device_config["init_param_schema"] = {}
|
device_config["init_param_schema"] = {}
|
||||||
device_config["init_param_schema"]["config"] = self._generate_unilab_json_command_schema(
|
device_config["init_param_schema"]["config"] = self._generate_unilab_json_command_schema(
|
||||||
enhanced_info["init_params"], "__init__"
|
enhanced_info["init_params"], "__init__"
|
||||||
|
|||||||
@@ -770,16 +770,13 @@ def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any
|
|||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
|
||||||
def ros_action_to_json_schema(
|
def ros_action_to_json_schema(action_class: Any, description="") -> Dict[str, Any]:
|
||||||
action_class: Any, description="", previous_schema: Optional[Dict[str, Any]] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
"""
|
||||||
将 ROS Action 类转换为 JSON Schema
|
将 ROS Action 类转换为 JSON Schema
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
action_class: ROS Action 类
|
action_class: ROS Action 类
|
||||||
description: 描述
|
description: 描述
|
||||||
previous_schema: 之前的 schema,用于保留 goal/feedback/result 下一级字段的 description
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
完整的 JSON Schema 定义
|
完整的 JSON Schema 定义
|
||||||
@@ -813,44 +810,9 @@ def ros_action_to_json_schema(
|
|||||||
"required": ["goal"],
|
"required": ["goal"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# 保留之前 schema 中 goal/feedback/result 下一级字段的 description
|
|
||||||
if previous_schema:
|
|
||||||
_preserve_field_descriptions(schema, previous_schema)
|
|
||||||
|
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
|
||||||
def _preserve_field_descriptions(
|
|
||||||
new_schema: Dict[str, Any], previous_schema: Dict[str, Any]
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
保留之前 schema 中 goal/feedback/result 下一级字段的 description 和 title
|
|
||||||
|
|
||||||
Args:
|
|
||||||
new_schema: 新生成的 schema(会被修改)
|
|
||||||
previous_schema: 之前的 schema
|
|
||||||
"""
|
|
||||||
for section in ["goal", "feedback", "result"]:
|
|
||||||
new_section = new_schema.get("properties", {}).get(section, {})
|
|
||||||
prev_section = previous_schema.get("properties", {}).get(section, {})
|
|
||||||
|
|
||||||
if not new_section or not prev_section:
|
|
||||||
continue
|
|
||||||
|
|
||||||
new_props = new_section.get("properties", {})
|
|
||||||
prev_props = prev_section.get("properties", {})
|
|
||||||
|
|
||||||
for field_name, field_schema in new_props.items():
|
|
||||||
if field_name in prev_props:
|
|
||||||
prev_field = prev_props[field_name]
|
|
||||||
# 保留字段的 description
|
|
||||||
if "description" in prev_field and prev_field["description"]:
|
|
||||||
field_schema["description"] = prev_field["description"]
|
|
||||||
# 保留字段的 title(用户自定义的中文名)
|
|
||||||
if "title" in prev_field and prev_field["title"]:
|
|
||||||
field_schema["title"] = prev_field["title"]
|
|
||||||
|
|
||||||
|
|
||||||
def convert_ros_action_to_jsonschema(
|
def convert_ros_action_to_jsonschema(
|
||||||
action_name_or_type: Union[str, Type], output_file: Optional[str] = None, format: str = "json"
|
action_name_or_type: Union[str, Type], output_file: Optional[str] = None, format: str = "json"
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
from typing import TYPE_CHECKING, Optional, Dict, Any, List, ClassVar, Set, Union
|
from typing import TYPE_CHECKING, Optional, Dict, Any, List, ClassVar, Set, TypedDict, Union
|
||||||
from typing_extensions import TypedDict
|
|
||||||
|
|
||||||
from action_msgs.msg import GoalStatus
|
from action_msgs.msg import GoalStatus
|
||||||
from geometry_msgs.msg import Point
|
from geometry_msgs.msg import Point
|
||||||
@@ -63,18 +62,6 @@ class TestResourceReturn(TypedDict):
|
|||||||
devices: List[DeviceSlot]
|
devices: List[DeviceSlot]
|
||||||
|
|
||||||
|
|
||||||
class TestLatencyReturn(TypedDict):
|
|
||||||
"""test_latency方法的返回值类型"""
|
|
||||||
|
|
||||||
avg_rtt_ms: float
|
|
||||||
avg_time_diff_ms: float
|
|
||||||
max_time_error_ms: float
|
|
||||||
task_delay_ms: float
|
|
||||||
raw_delay_ms: float
|
|
||||||
test_count: int
|
|
||||||
status: str
|
|
||||||
|
|
||||||
|
|
||||||
class HostNode(BaseROS2DeviceNode):
|
class HostNode(BaseROS2DeviceNode):
|
||||||
"""
|
"""
|
||||||
主机节点类,负责管理设备、资源和控制器
|
主机节点类,负责管理设备、资源和控制器
|
||||||
@@ -866,13 +853,8 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
# 适配后端的一些额外处理
|
# 适配后端的一些额外处理
|
||||||
return_value = return_info.get("return_value")
|
return_value = return_info.get("return_value")
|
||||||
if isinstance(return_value, dict):
|
if isinstance(return_value, dict):
|
||||||
unilabos_samples = return_value.pop("unilabos_samples", None)
|
unilabos_samples = return_info.get("unilabos_samples")
|
||||||
if isinstance(unilabos_samples, list) and unilabos_samples:
|
if isinstance(unilabos_samples, list):
|
||||||
self.lab_logger().info(
|
|
||||||
f"[Host Node] Job {job_id[:8]} returned {len(unilabos_samples)} sample(s): "
|
|
||||||
f"{[s.get('name', s.get('id', 'unknown')) if isinstance(s, dict) else str(s)[:20] for s in unilabos_samples[:5]]}"
|
|
||||||
f"{'...' if len(unilabos_samples) > 5 else ''}"
|
|
||||||
)
|
|
||||||
return_info["unilabos_samples"] = unilabos_samples
|
return_info["unilabos_samples"] = unilabos_samples
|
||||||
suc = return_info.get("suc", False)
|
suc = return_info.get("suc", False)
|
||||||
if not suc:
|
if not suc:
|
||||||
@@ -899,7 +881,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
# 清理 _goals 中的记录
|
# 清理 _goals 中的记录
|
||||||
if job_id in self._goals:
|
if job_id in self._goals:
|
||||||
del self._goals[job_id]
|
del self._goals[job_id]
|
||||||
self.lab_logger().trace(f"[Host Node] Removed goal {job_id[:8]} from _goals")
|
self.lab_logger().debug(f"[Host Node] Removed goal {job_id[:8]} from _goals")
|
||||||
|
|
||||||
# 存储结果供 HTTP API 查询
|
# 存储结果供 HTTP API 查询
|
||||||
try:
|
try:
|
||||||
@@ -1344,20 +1326,10 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self.lab_logger().debug(f"[Host Node-Resource] List parameters: {request}")
|
self.lab_logger().debug(f"[Host Node-Resource] List parameters: {request}")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def test_latency(self) -> TestLatencyReturn:
|
def test_latency(self):
|
||||||
"""
|
"""
|
||||||
测试网络延迟的action实现
|
测试网络延迟的action实现
|
||||||
通过5次ping-pong机制校对时间误差并计算实际延迟
|
通过5次ping-pong机制校对时间误差并计算实际延迟
|
||||||
|
|
||||||
Returns:
|
|
||||||
TestLatencyReturn: 包含延迟测试结果的字典,包括:
|
|
||||||
- avg_rtt_ms: 平均往返时间(毫秒)
|
|
||||||
- avg_time_diff_ms: 平均时间差(毫秒)
|
|
||||||
- max_time_error_ms: 最大时间误差(毫秒)
|
|
||||||
- task_delay_ms: 实际任务延迟(毫秒),-1表示无法计算
|
|
||||||
- raw_delay_ms: 原始时间差(毫秒),-1表示无法计算
|
|
||||||
- test_count: 有效测试次数
|
|
||||||
- status: 测试状态,"success"表示成功,"all_timeout"表示全部超时
|
|
||||||
"""
|
"""
|
||||||
import uuid as uuid_module
|
import uuid as uuid_module
|
||||||
|
|
||||||
@@ -1420,15 +1392,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
if not ping_results:
|
if not ping_results:
|
||||||
self.lab_logger().error("❌ 所有ping-pong测试都失败了")
|
self.lab_logger().error("❌ 所有ping-pong测试都失败了")
|
||||||
return {
|
return {"status": "all_timeout"}
|
||||||
"avg_rtt_ms": -1.0,
|
|
||||||
"avg_time_diff_ms": -1.0,
|
|
||||||
"max_time_error_ms": -1.0,
|
|
||||||
"task_delay_ms": -1.0,
|
|
||||||
"raw_delay_ms": -1.0,
|
|
||||||
"test_count": 0,
|
|
||||||
"status": "all_timeout",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 统计分析
|
# 统计分析
|
||||||
rtts = [r["rtt_ms"] for r in ping_results]
|
rtts = [r["rtt_ms"] for r in ping_results]
|
||||||
@@ -1436,7 +1400,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
avg_rtt_ms = sum(rtts) / len(rtts)
|
avg_rtt_ms = sum(rtts) / len(rtts)
|
||||||
avg_time_diff_ms = sum(time_diffs) / len(time_diffs)
|
avg_time_diff_ms = sum(time_diffs) / len(time_diffs)
|
||||||
max_time_diff_error_ms: float = max(abs(min(time_diffs)), abs(max(time_diffs)))
|
max_time_diff_error_ms = max(abs(min(time_diffs)), abs(max(time_diffs)))
|
||||||
|
|
||||||
self.lab_logger().info("-" * 50)
|
self.lab_logger().info("-" * 50)
|
||||||
self.lab_logger().info("[测试统计]")
|
self.lab_logger().info("[测试统计]")
|
||||||
@@ -1476,7 +1440,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
self.lab_logger().info("=" * 60)
|
self.lab_logger().info("=" * 60)
|
||||||
|
|
||||||
res: TestLatencyReturn = {
|
return {
|
||||||
"avg_rtt_ms": avg_rtt_ms,
|
"avg_rtt_ms": avg_rtt_ms,
|
||||||
"avg_time_diff_ms": avg_time_diff_ms,
|
"avg_time_diff_ms": avg_time_diff_ms,
|
||||||
"max_time_error_ms": max_time_diff_error_ms,
|
"max_time_error_ms": max_time_diff_error_ms,
|
||||||
@@ -1487,14 +1451,9 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
"test_count": len(ping_results),
|
"test_count": len(ping_results),
|
||||||
"status": "success",
|
"status": "success",
|
||||||
}
|
}
|
||||||
return res
|
|
||||||
|
|
||||||
def test_resource(
|
def test_resource(
|
||||||
self,
|
self, resource: ResourceSlot = None, resources: List[ResourceSlot] = None, device: DeviceSlot = None, devices: List[DeviceSlot] = None
|
||||||
resource: ResourceSlot = None,
|
|
||||||
resources: List[ResourceSlot] = None,
|
|
||||||
device: DeviceSlot = None,
|
|
||||||
devices: List[DeviceSlot] = None,
|
|
||||||
) -> TestResourceReturn:
|
) -> TestResourceReturn:
|
||||||
if resources is None:
|
if resources is None:
|
||||||
resources = []
|
resources = []
|
||||||
@@ -1555,9 +1514,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
# 构建服务地址
|
# 构建服务地址
|
||||||
srv_address = f"/srv{namespace}/s2c_resource_tree"
|
srv_address = f"/srv{namespace}/s2c_resource_tree"
|
||||||
self.lab_logger().trace(
|
self.lab_logger().trace(f"[Host Node-Resource] Host -> {device_id} ResourceTree {action} operation started -------")
|
||||||
f"[Host Node-Resource] Host -> {device_id} ResourceTree {action} operation started -------"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 创建服务客户端
|
# 创建服务客户端
|
||||||
sclient = self.create_client(SerialCommand, srv_address)
|
sclient = self.create_client(SerialCommand, srv_address)
|
||||||
@@ -1592,9 +1549,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
|
||||||
response = future.result()
|
response = future.result()
|
||||||
self.lab_logger().trace(
|
self.lab_logger().trace(f"[Host Node-Resource] Host -> {device_id} ResourceTree {action} operation completed -------")
|
||||||
f"[Host Node-Resource] Host -> {device_id} ResourceTree {action} operation completed -------"
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "workbench_1",
|
|
||||||
"name": "虚拟工作台",
|
|
||||||
"children": [],
|
|
||||||
"parent": null,
|
|
||||||
"type": "device",
|
|
||||||
"class": "virtual_workbench",
|
|
||||||
"position": {
|
|
||||||
"x": 400,
|
|
||||||
"y": 300,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"arm_operation_time": 3.0,
|
|
||||||
"heating_time": 10.0,
|
|
||||||
"num_heating_stations": 3
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"status": "Ready",
|
|
||||||
"arm_state": "idle",
|
|
||||||
"message": "工作台就绪"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"links": []
|
|
||||||
}
|
|
||||||
@@ -182,49 +182,3 @@ def get_all_subscriptions(instance) -> list:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return subscriptions
|
return subscriptions
|
||||||
|
|
||||||
|
|
||||||
def not_action(func: F) -> F:
|
|
||||||
"""
|
|
||||||
标记方法为非动作的装饰器
|
|
||||||
|
|
||||||
用于装饰 driver 类中的方法,使其在 complete_registry 时不被识别为动作。
|
|
||||||
适用于辅助方法、内部工具方法等不应暴露为设备动作的公共方法。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
class MyDriver:
|
|
||||||
@not_action
|
|
||||||
def helper_method(self):
|
|
||||||
# 这个方法不会被注册为动作
|
|
||||||
pass
|
|
||||||
|
|
||||||
def actual_action(self, param: str):
|
|
||||||
# 这个方法会被注册为动作
|
|
||||||
self.helper_method()
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- 可以与其他装饰器组合使用,@not_action 应放在最外层
|
|
||||||
- 仅影响 complete_registry 的动作识别,不影响方法的正常调用
|
|
||||||
"""
|
|
||||||
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
# 在函数上附加标记
|
|
||||||
wrapper._is_not_action = True # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
return wrapper # type: ignore[return-value]
|
|
||||||
|
|
||||||
|
|
||||||
def is_not_action(func) -> bool:
|
|
||||||
"""
|
|
||||||
检查函数是否被标记为非动作
|
|
||||||
|
|
||||||
Args:
|
|
||||||
func: 被检查的函数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
如果函数被 @not_action 装饰则返回 True,否则返回 False
|
|
||||||
"""
|
|
||||||
return getattr(func, "_is_not_action", False)
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ __all__ = [
|
|||||||
from ast import Constant
|
from ast import Constant
|
||||||
|
|
||||||
from unilabos.utils import logger
|
from unilabos.utils import logger
|
||||||
from unilabos.utils.decorator import is_not_action
|
|
||||||
|
|
||||||
|
|
||||||
class ImportManager:
|
class ImportManager:
|
||||||
@@ -276,9 +275,6 @@ class ImportManager:
|
|||||||
method_info = self._analyze_method_signature(method)
|
method_info = self._analyze_method_signature(method)
|
||||||
result["status_methods"][actual_name] = method_info
|
result["status_methods"][actual_name] = method_info
|
||||||
elif not name.startswith("_"):
|
elif not name.startswith("_"):
|
||||||
# 检查是否被 @not_action 装饰器标记
|
|
||||||
if is_not_action(method):
|
|
||||||
continue
|
|
||||||
# 其他非_开头的方法归类为action
|
# 其他非_开头的方法归类为action
|
||||||
method_info = self._analyze_method_signature(method)
|
method_info = self._analyze_method_signature(method)
|
||||||
result["action_methods"][name] = method_info
|
result["action_methods"][name] = method_info
|
||||||
@@ -334,9 +330,6 @@ class ImportManager:
|
|||||||
if actual_name not in result["status_methods"]:
|
if actual_name not in result["status_methods"]:
|
||||||
result["status_methods"][actual_name] = method_info
|
result["status_methods"][actual_name] = method_info
|
||||||
else:
|
else:
|
||||||
# 检查是否被 @not_action 装饰器标记
|
|
||||||
if self._is_not_action_method(node):
|
|
||||||
continue
|
|
||||||
# 其他非_开头的方法归类为action
|
# 其他非_开头的方法归类为action
|
||||||
result["action_methods"][method_name] = method_info
|
result["action_methods"][method_name] = method_info
|
||||||
return result
|
return result
|
||||||
@@ -457,13 +450,6 @@ class ImportManager:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _is_not_action_method(self, node: ast.FunctionDef) -> bool:
|
|
||||||
"""检查是否是@not_action装饰的方法"""
|
|
||||||
for decorator in node.decorator_list:
|
|
||||||
if isinstance(decorator, ast.Name) and decorator.id == "not_action":
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _get_property_name_from_setter(self, node: ast.FunctionDef) -> str:
|
def _get_property_name_from_setter(self, node: ast.FunctionDef) -> str:
|
||||||
"""从setter装饰器中获取属性名"""
|
"""从setter装饰器中获取属性名"""
|
||||||
for decorator in node.decorator_list:
|
for decorator in node.decorator_list:
|
||||||
|
|||||||
@@ -191,6 +191,21 @@ def configure_logger(loglevel=None, working_dir=None):
|
|||||||
|
|
||||||
# 添加处理器到根日志记录器
|
# 添加处理器到根日志记录器
|
||||||
root_logger.addHandler(console_handler)
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# 降低第三方库的日志级别,避免过多输出
|
||||||
|
# pymodbus 库的日志太详细,设置为 WARNING
|
||||||
|
logging.getLogger('pymodbus').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('pymodbus.logging').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('pymodbus.logging.base').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('pymodbus.logging.decoders').setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# websockets 库的日志输出较多,设置为 WARNING
|
||||||
|
logging.getLogger('websockets').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('websockets.client').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('websockets.server').setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# ROS 节点的状态更新日志过于频繁,设置为 INFO
|
||||||
|
logging.getLogger('unilabos.ros.nodes.presets.host_node').setLevel(logging.INFO)
|
||||||
|
|
||||||
# 如果指定了工作目录,添加文件处理器
|
# 如果指定了工作目录,添加文件处理器
|
||||||
if working_dir is not None:
|
if working_dir is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user