Merge remote-tracking branch 'origin/dev' into feature/organic-extraction

This commit is contained in:
ZiWei
2026-04-27 11:13:13 +08:00
60 changed files with 3222 additions and 476 deletions

View File

@@ -2,6 +2,8 @@ import time
import logging
from typing import Union, Dict, Optional
from unilabos.registry.decorators import topic_config
class VirtualMultiwayValve:
"""
@@ -41,13 +43,11 @@ class VirtualMultiwayValve:
def target_position(self) -> int:
return self._target_position
def get_current_position(self) -> int:
"""获取当前阀门位置 📍"""
return self._current_position
def get_current_port(self) -> str:
"""获取当前连接的端口名称 🔌"""
return self._current_position
@property
@topic_config()
def current_port(self) -> str:
"""当前连接的端口名称 🔌"""
return self.port
def set_position(self, command: Union[int, str]):
"""
@@ -169,12 +169,14 @@ class VirtualMultiwayValve:
self._status = "Idle"
self._valve_state = "Closed"
close_msg = f"🔒 阀门已关闭,保持在位置 {self._current_position} ({self.get_current_port()})"
close_msg = f"🔒 阀门已关闭,保持在位置 {self._current_position} ({self.port})"
self.logger.info(close_msg)
return close_msg
def get_valve_position(self) -> int:
"""获取阀门位置 - 兼容性方法 📍"""
@property
@topic_config()
def valve_position(self) -> int:
"""阀门位置 📍"""
return self._current_position
def set_valve_position(self, command: Union[int, str]):
@@ -229,19 +231,16 @@ class VirtualMultiwayValve:
self.logger.info(f"🔄 从端口 {self._current_position} 切换到泵位置...")
return self.set_to_pump_position()
def get_flow_path(self) -> str:
"""获取当前流路路径描述 🌊"""
current_port = self.get_current_port()
@property
@topic_config()
def flow_path(self) -> str:
"""当前流路路径描述 🌊"""
if self._current_position == 0:
flow_path = f"🚰 转移泵已连接 (位置 {self._current_position})"
else:
flow_path = f"🔌 端口 {self._current_position} 已连接 ({current_port})"
# 删除debug日志self.logger.debug(f"🌊 当前流路: {flow_path}")
return flow_path
return f"🚰 转移泵已连接 (位置 {self._current_position})"
return f"🔌 端口 {self._current_position} 已连接 ({self.current_port})"
def __str__(self):
current_port = self.get_current_port()
current_port = self.current_port
status_emoji = "" if self._status == "Idle" else "🔄" if self._status == "Busy" else ""
return f"🔄 VirtualMultiwayValve({status_emoji} 位置: {self._current_position}/{self.max_positions}, 端口: {current_port}, 状态: {self._status})"
@@ -253,7 +252,7 @@ if __name__ == "__main__":
print("🔄 === 虚拟九通阀门测试 === ✨")
print(f"🏠 初始状态: {valve}")
print(f"🌊 当前流路: {valve.get_flow_path()}")
print(f"🌊 当前流路: {valve.flow_path}")
# 切换到试剂瓶11号位
print(f"\n🔌 切换到1号位: {valve.set_position(1)}")

View File

@@ -3,6 +3,7 @@ import logging
import time as time_module
from typing import Dict, Any
from unilabos.registry.decorators import topic_config
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class VirtualStirrer:
@@ -314,9 +315,11 @@ class VirtualStirrer:
def min_speed(self) -> float:
return self._min_speed
def get_device_info(self) -> Dict[str, Any]:
"""获取设备状态信息 📊"""
info = {
@property
@topic_config()
def device_info(self) -> Dict[str, Any]:
"""设备状态快照信息 📊"""
return {
"device_id": self.device_id,
"status": self.status,
"operation_mode": self.operation_mode,
@@ -325,12 +328,9 @@ class VirtualStirrer:
"is_stirring": self.is_stirring,
"remaining_time": self.remaining_time,
"max_speed": self._max_speed,
"min_speed": self._min_speed
"min_speed": self._min_speed,
}
# self.logger.debug(f"📊 设备信息: 模式={self.operation_mode}, 速度={self.current_speed} RPM, 搅拌={self.is_stirring}")
return info
def __str__(self):
status_emoji = "" if self.operation_mode == "Idle" else "🌪️" if self.operation_mode == "Stirring" else "🛑" if self.operation_mode == "Settling" else ""
return f"🌪️ VirtualStirrer({status_emoji} {self.device_id}: {self.operation_mode}, {self.current_speed} RPM)"

View File

@@ -4,6 +4,7 @@ from enum import Enum
from typing import Union, Optional
import logging
from unilabos.registry.decorators import topic_config
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
@@ -385,8 +386,10 @@ class VirtualTransferPump:
"""获取当前体积"""
return self._current_volume
def get_remaining_capacity(self) -> float:
"""获取剩余容量"""
@property
@topic_config()
def remaining_capacity(self) -> float:
"""剩余容量 (ml)"""
return self.max_volume - self._current_volume
def is_empty(self) -> bool:

View File

@@ -22,10 +22,11 @@ from threading import Lock, RLock
from typing_extensions import TypedDict
from unilabos.registry.decorators import (
device, action, ActionInputHandle, ActionOutputHandle, DataSource, topic_config, not_action
device, action, ActionInputHandle, ActionOutputHandle, DataSource, topic_config, not_action, NodeType
)
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, ResourceTreeSet
# ============ TypedDict 返回类型定义 ============
@@ -290,6 +291,126 @@ class VirtualWorkbench:
self._update_data_status(f"机械臂已释放 (完成: {task})")
self.logger.info(f"机械臂已释放 (完成: {task})")
@action(
always_free=True, node_type=NodeType.MANUAL_CONFIRM, placeholder_keys={
"assignee_user_ids": "unilabos_manual_confirm"
}, goal_default={
"timeout_seconds": 3600,
"assignee_user_ids": []
}, feedback_interval=300,
handles=[
ActionInputHandle(key="target_device", data_type="device_id",
label="目标设备", data_key="target_device", data_source=DataSource.HANDLE),
ActionInputHandle(key="resource", data_type="resource",
label="待转移资源", data_key="resource", data_source=DataSource.HANDLE),
ActionInputHandle(key="mount_resource", data_type="resource",
label="目标孔位", data_key="mount_resource", data_source=DataSource.HANDLE),
ActionInputHandle(key="collector_mass", data_type="collector_mass",
label="极流体质量", data_key="collector_mass", data_source=DataSource.HANDLE),
ActionInputHandle(key="active_material", data_type="active_material",
label="活性物质含量", data_key="active_material", data_source=DataSource.HANDLE),
ActionInputHandle(key="capacity", data_type="capacity",
label="克容量", data_key="capacity", data_source=DataSource.HANDLE),
ActionInputHandle(key="battery_system", data_type="battery_system",
label="电池体系", data_key="battery_system", data_source=DataSource.HANDLE),
# transfer使用
ActionOutputHandle(key="target_device", data_type="device_id",
label="目标设备", data_key="target_device", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="resource", data_type="resource",
label="待转移资源", data_key="resource.@flatten", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="mount_resource", data_type="resource",
label="目标孔位", data_key="mount_resource.@flatten", data_source=DataSource.EXECUTOR),
# test使用
ActionOutputHandle(key="collector_mass", data_type="collector_mass",
label="极流体质量", data_key="collector_mass", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="active_material", data_type="active_material",
label="活性物质含量", data_key="active_material", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="capacity", data_type="capacity",
label="克容量", data_key="capacity", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="battery_system", data_type="battery_system",
label="电池体系", data_key="battery_system", data_source=DataSource.EXECUTOR),
]
)
def manual_confirm(
self,
resource: List[ResourceSlot],
target_device: DeviceSlot,
mount_resource: List[ResourceSlot],
collector_mass: List[float],
active_material: List[float],
capacity: List[float],
battery_system: List[str],
timeout_seconds: int,
assignee_user_ids: list[str],
**kwargs
) -> dict:
"""
timeout_seconds: 超时时间默认3600秒
collector_mass: 极流体质量
active_material: 活性物质含量
capacity: 克容量mAh/g
battery_system: 电池体系
修改的结果无效,是只读的
"""
resource = ResourceTreeSet.from_plr_resources(resource).dump()
mount_resource = ResourceTreeSet.from_plr_resources(mount_resource).dump()
kwargs.update(locals())
kwargs.pop("kwargs")
kwargs.pop("self")
return kwargs
@action(
description="转移物料",
handles=[
ActionInputHandle(key="target_device", data_type="device_id",
label="目标设备", data_key="target_device", data_source=DataSource.HANDLE),
ActionInputHandle(key="resource", data_type="resource",
label="待转移资源", data_key="resource", data_source=DataSource.HANDLE),
ActionInputHandle(key="mount_resource", data_type="resource",
label="目标孔位", data_key="mount_resource", data_source=DataSource.HANDLE),
]
)
async def transfer(self, resource: List[ResourceSlot], target_device: DeviceSlot, mount_resource: List[ResourceSlot]):
future = ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True,
**{
"plr_resources": resource,
"target_device_id": target_device,
"target_resources": mount_resource,
"sites": [None] * len(mount_resource),
})
result = await future
return result
@action(
description="扣电测试启动",
handles=[
ActionInputHandle(key="resource", data_type="resource",
label="待转移资源", data_key="resource", data_source=DataSource.HANDLE),
ActionInputHandle(key="mount_resource", data_type="resource",
label="目标孔位", data_key="mount_resource", data_source=DataSource.HANDLE),
ActionInputHandle(key="collector_mass", data_type="collector_mass",
label="极流体质量", data_key="collector_mass", data_source=DataSource.HANDLE),
ActionInputHandle(key="active_material", data_type="active_material",
label="活性物质含量", data_key="active_material", data_source=DataSource.HANDLE),
ActionInputHandle(key="capacity", data_type="capacity",
label="克容量", data_key="capacity", data_source=DataSource.HANDLE),
ActionInputHandle(key="battery_system", data_type="battery_system",
label="电池体系", data_key="battery_system", data_source=DataSource.HANDLE),
]
)
async def test(
self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], collector_mass: List[float], active_material: List[float], capacity: List[float], battery_system: list[str]
):
print(resource)
print(mount_resource)
print(collector_mass)
print(active_material)
print(capacity)
print(battery_system)
@action(
auto_prefix=True,
description="批量准备物料 - 虚拟起始节点, 生成A1-A5物料, 输出5个handle供后续节点使用",