mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-07 15:35:10 +00:00
add registry name & add always free
This commit is contained in:
@@ -76,6 +76,7 @@ class JobInfo:
|
|||||||
start_time: float
|
start_time: float
|
||||||
last_update_time: float = field(default_factory=time.time)
|
last_update_time: float = field(default_factory=time.time)
|
||||||
ready_timeout: Optional[float] = None # READY状态的超时时间
|
ready_timeout: Optional[float] = None # READY状态的超时时间
|
||||||
|
always_free: bool = False # 是否为永久闲置动作(不受排队限制)
|
||||||
|
|
||||||
def update_timestamp(self):
|
def update_timestamp(self):
|
||||||
"""更新最后更新时间"""
|
"""更新最后更新时间"""
|
||||||
@@ -127,6 +128,15 @@ class DeviceActionManager:
|
|||||||
# 总是将job添加到all_jobs中
|
# 总是将job添加到all_jobs中
|
||||||
self.all_jobs[job_info.job_id] = job_info
|
self.all_jobs[job_info.job_id] = job_info
|
||||||
|
|
||||||
|
# always_free的动作不受排队限制,直接设为READY
|
||||||
|
if job_info.always_free:
|
||||||
|
job_info.status = JobStatus.READY
|
||||||
|
job_info.update_timestamp()
|
||||||
|
job_info.set_ready_timeout(10)
|
||||||
|
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} always_free, start immediately")
|
||||||
|
return True
|
||||||
|
|
||||||
# 检查是否有正在执行或准备执行的任务
|
# 检查是否有正在执行或准备执行的任务
|
||||||
if device_key in self.active_jobs:
|
if device_key in self.active_jobs:
|
||||||
# 有正在执行或准备执行的任务,加入队列
|
# 有正在执行或准备执行的任务,加入队列
|
||||||
@@ -176,11 +186,15 @@ class DeviceActionManager:
|
|||||||
logger.error(f"[DeviceActionManager] Job {job_log} is not in READY status, current: {job_info.status}")
|
logger.error(f"[DeviceActionManager] Job {job_log} is not in READY status, current: {job_info.status}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 检查设备上是否是这个job
|
# always_free的job不需要检查active_jobs
|
||||||
if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id:
|
if not job_info.always_free:
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
# 检查设备上是否是这个job
|
||||||
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}")
|
if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id:
|
||||||
return False
|
job_log = format_job_log(
|
||||||
|
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
||||||
|
)
|
||||||
|
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}")
|
||||||
|
return False
|
||||||
|
|
||||||
# 开始执行任务,将状态从READY转换为STARTED
|
# 开始执行任务,将状态从READY转换为STARTED
|
||||||
job_info.status = JobStatus.STARTED
|
job_info.status = JobStatus.STARTED
|
||||||
@@ -203,6 +217,13 @@ class DeviceActionManager:
|
|||||||
job_info = self.all_jobs[job_id]
|
job_info = self.all_jobs[job_id]
|
||||||
device_key = job_info.device_action_key
|
device_key = job_info.device_action_key
|
||||||
|
|
||||||
|
# always_free的job直接清理,不影响队列
|
||||||
|
if job_info.always_free:
|
||||||
|
job_info.status = JobStatus.ENDED
|
||||||
|
job_info.update_timestamp()
|
||||||
|
del self.all_jobs[job_id]
|
||||||
|
return None
|
||||||
|
|
||||||
# 移除活跃任务
|
# 移除活跃任务
|
||||||
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
||||||
del self.active_jobs[device_key]
|
del self.active_jobs[device_key]
|
||||||
@@ -234,9 +255,14 @@ class DeviceActionManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_active_jobs(self) -> List[JobInfo]:
|
def get_active_jobs(self) -> List[JobInfo]:
|
||||||
"""获取所有正在执行的任务"""
|
"""获取所有正在执行的任务(含active_jobs和always_free的STARTED job)"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return list(self.active_jobs.values())
|
jobs = list(self.active_jobs.values())
|
||||||
|
# 补充 always_free 的 STARTED job(它们不在 active_jobs 中)
|
||||||
|
for job in self.all_jobs.values():
|
||||||
|
if job.always_free and job.status == JobStatus.STARTED and job not in jobs:
|
||||||
|
jobs.append(job)
|
||||||
|
return jobs
|
||||||
|
|
||||||
def get_queued_jobs(self) -> List[JobInfo]:
|
def get_queued_jobs(self) -> List[JobInfo]:
|
||||||
"""获取所有排队中的任务"""
|
"""获取所有排队中的任务"""
|
||||||
@@ -261,6 +287,14 @@ class DeviceActionManager:
|
|||||||
job_info = self.all_jobs[job_id]
|
job_info = self.all_jobs[job_id]
|
||||||
device_key = job_info.device_action_key
|
device_key = job_info.device_action_key
|
||||||
|
|
||||||
|
# always_free的job直接清理
|
||||||
|
if job_info.always_free:
|
||||||
|
job_info.status = JobStatus.ENDED
|
||||||
|
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)
|
||||||
|
logger.trace(f"[DeviceActionManager] Always-free job {job_log} cancelled")
|
||||||
|
return True
|
||||||
|
|
||||||
# 如果是正在执行的任务
|
# 如果是正在执行的任务
|
||||||
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
||||||
# 清理active job状态
|
# 清理active job状态
|
||||||
@@ -334,13 +368,18 @@ class DeviceActionManager:
|
|||||||
timeout_jobs = []
|
timeout_jobs = []
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# 统计READY状态的任务数量
|
# 收集所有需要检查的 READY 任务(active_jobs + always_free READY jobs)
|
||||||
ready_jobs_count = sum(1 for job in self.active_jobs.values() if job.status == JobStatus.READY)
|
ready_candidates = list(self.active_jobs.values())
|
||||||
|
for job in self.all_jobs.values():
|
||||||
|
if job.always_free and job.status == JobStatus.READY and job not in ready_candidates:
|
||||||
|
ready_candidates.append(job)
|
||||||
|
|
||||||
|
ready_jobs_count = sum(1 for job in ready_candidates if job.status == JobStatus.READY)
|
||||||
if ready_jobs_count > 0:
|
if ready_jobs_count > 0:
|
||||||
logger.trace(f"[DeviceActionManager] Checking {ready_jobs_count} READY jobs for timeout") # type: ignore # noqa: E501
|
logger.trace(f"[DeviceActionManager] Checking {ready_jobs_count} READY jobs for timeout") # type: ignore # noqa: E501
|
||||||
|
|
||||||
# 找到所有超时的READY任务(只检测,不处理)
|
# 找到所有超时的READY任务(只检测,不处理)
|
||||||
for job_info in self.active_jobs.values():
|
for job_info in ready_candidates:
|
||||||
if job_info.is_ready_timeout():
|
if job_info.is_ready_timeout():
|
||||||
timeout_jobs.append(job_info)
|
timeout_jobs.append(job_info)
|
||||||
job_log = format_job_log(
|
job_log = format_job_log(
|
||||||
@@ -608,6 +647,24 @@ class MessageProcessor:
|
|||||||
if host_node:
|
if host_node:
|
||||||
host_node.handle_pong_response(pong_data)
|
host_node.handle_pong_response(pong_data)
|
||||||
|
|
||||||
|
def _check_action_always_free(self, device_id: str, action_name: str) -> bool:
|
||||||
|
"""检查该action是否标记为always_free,通过HostNode统一的_action_value_mappings查找"""
|
||||||
|
try:
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if not host_node:
|
||||||
|
return False
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
action_mappings = host_node._action_value_mappings.get(device_id)
|
||||||
|
if not action_mappings:
|
||||||
|
return False
|
||||||
|
# 尝试直接匹配或 auto- 前缀匹配
|
||||||
|
for key in [action_name, f"auto-{action_name}"]:
|
||||||
|
if key in action_mappings:
|
||||||
|
return action_mappings[key].get("always_free", False)
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
async def _handle_query_action_state(self, data: Dict[str, Any]):
|
async def _handle_query_action_state(self, data: Dict[str, Any]):
|
||||||
"""处理query_action_state消息"""
|
"""处理query_action_state消息"""
|
||||||
device_id = data.get("device_id", "")
|
device_id = data.get("device_id", "")
|
||||||
@@ -622,6 +679,9 @@ class MessageProcessor:
|
|||||||
|
|
||||||
device_action_key = f"/devices/{device_id}/{action_name}"
|
device_action_key = f"/devices/{device_id}/{action_name}"
|
||||||
|
|
||||||
|
# 检查action是否为always_free
|
||||||
|
action_always_free = self._check_action_always_free(device_id, action_name)
|
||||||
|
|
||||||
# 创建任务信息
|
# 创建任务信息
|
||||||
job_info = JobInfo(
|
job_info = JobInfo(
|
||||||
job_id=job_id,
|
job_id=job_id,
|
||||||
@@ -631,6 +691,7 @@ class MessageProcessor:
|
|||||||
device_action_key=device_action_key,
|
device_action_key=device_action_key,
|
||||||
status=JobStatus.QUEUE,
|
status=JobStatus.QUEUE,
|
||||||
start_time=time.time(),
|
start_time=time.time(),
|
||||||
|
always_free=action_always_free,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加到设备管理器
|
# 添加到设备管理器
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ from rclpy.node import Node
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
||||||
def __init__(self,resources_config:list, resource_tracker, rate=50, device_id:str = "lh_joint_publisher", **kwargs):
|
def __init__(self,resources_config:list, resource_tracker, rate=50, device_id:str = "lh_joint_publisher", registry_name: str = "lh_joint_publisher", **kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
hardware_interface={},
|
hardware_interface={},
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from threading import Lock, RLock
|
|||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
from unilabos.utils.decorator import not_action
|
from unilabos.utils.decorator import not_action, always_free
|
||||||
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES
|
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES
|
||||||
|
|
||||||
|
|
||||||
@@ -123,8 +123,8 @@ class VirtualWorkbench:
|
|||||||
_ros_node: BaseROS2DeviceNode
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
# 配置常量
|
# 配置常量
|
||||||
ARM_OPERATION_TIME: float = 3.0 # 机械臂操作时间(秒)
|
ARM_OPERATION_TIME: float = 2 # 机械臂操作时间(秒)
|
||||||
HEATING_TIME: float = 10.0 # 加热时间(秒)
|
HEATING_TIME: float = 60.0 # 加热时间(秒)
|
||||||
NUM_HEATING_STATIONS: int = 3 # 加热台数量
|
NUM_HEATING_STATIONS: int = 3 # 加热台数量
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
@@ -141,9 +141,9 @@ class VirtualWorkbench:
|
|||||||
self.data: Dict[str, Any] = {}
|
self.data: Dict[str, Any] = {}
|
||||||
|
|
||||||
# 从config中获取可配置参数
|
# 从config中获取可配置参数
|
||||||
self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", 3.0))
|
self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", self.ARM_OPERATION_TIME))
|
||||||
self.HEATING_TIME = float(self.config.get("heating_time", 10.0))
|
self.HEATING_TIME = float(self.config.get("heating_time", self.HEATING_TIME))
|
||||||
self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", 3))
|
self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", self.NUM_HEATING_STATIONS))
|
||||||
|
|
||||||
# 机械臂状态和锁 (使用threading.Lock)
|
# 机械臂状态和锁 (使用threading.Lock)
|
||||||
self._arm_lock = Lock()
|
self._arm_lock = Lock()
|
||||||
@@ -431,6 +431,7 @@ class VirtualWorkbench:
|
|||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid, content in sample_uuids.items()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@always_free
|
||||||
def start_heating(
|
def start_heating(
|
||||||
self,
|
self,
|
||||||
sample_uuids: SampleUUIDsType,
|
sample_uuids: SampleUUIDsType,
|
||||||
@@ -501,10 +502,21 @@ class VirtualWorkbench:
|
|||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}开始加热{material_id}")
|
self._update_data_status(f"加热台{station_id}开始加热{material_id}")
|
||||||
|
|
||||||
# 模拟加热过程 (10秒)
|
# 打印当前所有正在加热的台位
|
||||||
|
with self._stations_lock:
|
||||||
|
heating_list = [
|
||||||
|
f"加热台{sid}:{s.current_material}"
|
||||||
|
for sid, s in self._heating_stations.items()
|
||||||
|
if s.state == HeatingStationState.HEATING and s.current_material
|
||||||
|
]
|
||||||
|
self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}")
|
||||||
|
|
||||||
|
# 模拟加热过程
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
last_countdown_log = start_time
|
||||||
while True:
|
while True:
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
|
remaining = max(0.0, self.HEATING_TIME - elapsed)
|
||||||
progress = min(100.0, (elapsed / self.HEATING_TIME) * 100)
|
progress = min(100.0, (elapsed / self.HEATING_TIME) * 100)
|
||||||
|
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
@@ -512,6 +524,11 @@ class VirtualWorkbench:
|
|||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
|
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
|
||||||
|
|
||||||
|
# 每5秒打印一次倒计时
|
||||||
|
if time.time() - last_countdown_log >= 5.0:
|
||||||
|
self.logger.info(f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s")
|
||||||
|
last_countdown_log = time.time()
|
||||||
|
|
||||||
if elapsed >= self.HEATING_TIME:
|
if elapsed >= self.HEATING_TIME:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -6090,6 +6090,7 @@ virtual_workbench:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-start_heating:
|
auto-start_heating:
|
||||||
|
always_free: true
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
goal_default:
|
goal_default:
|
||||||
|
|||||||
@@ -838,6 +838,7 @@ class Registry:
|
|||||||
("list", "unilabos.registry.placeholder_type:DeviceSlot"),
|
("list", "unilabos.registry.placeholder_type:DeviceSlot"),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
**({"always_free": True} if v.get("always_free") else {}),
|
||||||
}
|
}
|
||||||
for k, v in enhanced_info["action_methods"].items()
|
for k, v in enhanced_info["action_methods"].items()
|
||||||
if k not in device_config["class"]["action_value_mappings"]
|
if k not in device_config["class"]["action_value_mappings"]
|
||||||
|
|||||||
@@ -44,8 +44,7 @@ def ros2_device_node(
|
|||||||
# 从属性中自动发现可发布状态
|
# 从属性中自动发现可发布状态
|
||||||
if status_types is None:
|
if status_types is None:
|
||||||
status_types = {}
|
status_types = {}
|
||||||
if device_config is None:
|
assert device_config is not None, "device_config cannot be None"
|
||||||
raise ValueError("device_config cannot be None")
|
|
||||||
if action_value_mappings is None:
|
if action_value_mappings is None:
|
||||||
action_value_mappings = {}
|
action_value_mappings = {}
|
||||||
if hardware_interface is None:
|
if hardware_interface is None:
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ def init_wrapper(
|
|||||||
device_id: str,
|
device_id: str,
|
||||||
device_uuid: str,
|
device_uuid: str,
|
||||||
driver_class: type[T],
|
driver_class: type[T],
|
||||||
device_config: ResourceTreeInstance,
|
device_config: ResourceDictInstance,
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
action_value_mappings: Dict[str, Any],
|
action_value_mappings: Dict[str, Any],
|
||||||
hardware_interface: Dict[str, Any],
|
hardware_interface: Dict[str, Any],
|
||||||
@@ -279,6 +279,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
self,
|
self,
|
||||||
driver_instance: T,
|
driver_instance: T,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
|
registry_name: str,
|
||||||
device_uuid: str,
|
device_uuid: str,
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
action_value_mappings: Dict[str, Any],
|
action_value_mappings: Dict[str, Any],
|
||||||
@@ -300,6 +301,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
"""
|
"""
|
||||||
self.driver_instance = driver_instance
|
self.driver_instance = driver_instance
|
||||||
self.device_id = device_id
|
self.device_id = device_id
|
||||||
|
self.registry_name = registry_name
|
||||||
self.uuid = device_uuid
|
self.uuid = device_uuid
|
||||||
self.publish_high_frequency = False
|
self.publish_high_frequency = False
|
||||||
self.callback_group = ReentrantCallbackGroup()
|
self.callback_group = ReentrantCallbackGroup()
|
||||||
@@ -416,7 +418,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
if len(rts.root_nodes) == 1 and isinstance(rts_plr_instances[0], RegularContainer):
|
if len(rts.root_nodes) == 1 and isinstance(rts_plr_instances[0], RegularContainer):
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
container_instance: RegularContainer = rts_plr_instances[0]
|
container_instance: RegularContainer = rts_plr_instances[0]
|
||||||
found_resources = self.resource_tracker.figure_resource({"name": container_instance.name}, try_mode=True)
|
found_resources = self.resource_tracker.figure_resource(
|
||||||
|
{"name": container_instance.name}, try_mode=True
|
||||||
|
)
|
||||||
if not len(found_resources):
|
if not len(found_resources):
|
||||||
self.resource_tracker.add_resource(container_instance)
|
self.resource_tracker.add_resource(container_instance)
|
||||||
logger.info(f"添加物料{container_instance.name}到资源跟踪器")
|
logger.info(f"添加物料{container_instance.name}到资源跟踪器")
|
||||||
@@ -1152,6 +1156,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
"machine_name": BasicConfig.machine_name,
|
"machine_name": BasicConfig.machine_name,
|
||||||
"type": "slave",
|
"type": "slave",
|
||||||
"edge_device_id": self.device_id,
|
"edge_device_id": self.device_id,
|
||||||
|
"registry_name": self.registry_name,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
@@ -1626,9 +1631,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
else:
|
else:
|
||||||
resolved_sample_uuids[sample_uuid] = material_uuid
|
resolved_sample_uuids[sample_uuid] = material_uuid
|
||||||
function_args[PARAM_SAMPLE_UUIDS] = resolved_sample_uuids
|
function_args[PARAM_SAMPLE_UUIDS] = resolved_sample_uuids
|
||||||
self.lab_logger().debug(
|
self.lab_logger().debug(f"[JsonCommand] 注入 {PARAM_SAMPLE_UUIDS}: {resolved_sample_uuids}")
|
||||||
f"[JsonCommand] 注入 {PARAM_SAMPLE_UUIDS}: {resolved_sample_uuids}"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 处理单个 ResourceSlot
|
# 处理单个 ResourceSlot
|
||||||
@@ -2005,6 +2008,7 @@ class ROS2DeviceNode:
|
|||||||
|
|
||||||
if driver_is_ros:
|
if driver_is_ros:
|
||||||
driver_params["device_id"] = device_id
|
driver_params["device_id"] = device_id
|
||||||
|
driver_params["registry_name"] = device_config.res_content.klass
|
||||||
driver_params["resource_tracker"] = self.resource_tracker
|
driver_params["resource_tracker"] = self.resource_tracker
|
||||||
self._driver_instance = self._driver_creator.create_instance(driver_params)
|
self._driver_instance = self._driver_creator.create_instance(driver_params)
|
||||||
if self._driver_instance is None:
|
if self._driver_instance is None:
|
||||||
@@ -2022,6 +2026,7 @@ class ROS2DeviceNode:
|
|||||||
children=children,
|
children=children,
|
||||||
driver_instance=self._driver_instance, # type: ignore
|
driver_instance=self._driver_instance, # type: ignore
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=device_config.res_content.klass,
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings=action_value_mappings,
|
action_value_mappings=action_value_mappings,
|
||||||
@@ -2033,6 +2038,7 @@ class ROS2DeviceNode:
|
|||||||
self._ros_node = BaseROS2DeviceNode(
|
self._ros_node = BaseROS2DeviceNode(
|
||||||
driver_instance=self._driver_instance,
|
driver_instance=self._driver_instance,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=device_config.res_content.klass,
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings=action_value_mappings,
|
action_value_mappings=action_value_mappings,
|
||||||
@@ -2041,6 +2047,7 @@ class ROS2DeviceNode:
|
|||||||
resource_tracker=self.resource_tracker,
|
resource_tracker=self.resource_tracker,
|
||||||
)
|
)
|
||||||
self._ros_node: BaseROS2DeviceNode
|
self._ros_node: BaseROS2DeviceNode
|
||||||
|
# 将注册表类型名传递给BaseROS2DeviceNode,用于slave上报
|
||||||
self._ros_node.lab_logger().info(f"初始化完成 {self._ros_node.uuid} {self.driver_is_ros}")
|
self._ros_node.lab_logger().info(f"初始化完成 {self._ros_node.uuid} {self.driver_is_ros}")
|
||||||
self.driver_instance._ros_node = self._ros_node # type: ignore
|
self.driver_instance._ros_node = self._ros_node # type: ignore
|
||||||
self.driver_instance._execute_driver_command = self._ros_node._execute_driver_command # type: ignore
|
self.driver_instance._execute_driver_command = self._ros_node._execute_driver_command # type: ignore
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ from cv_bridge import CvBridge
|
|||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker
|
||||||
|
|
||||||
class VideoPublisher(BaseROS2DeviceNode):
|
class VideoPublisher(BaseROS2DeviceNode):
|
||||||
def __init__(self, device_id='video_publisher', device_uuid='', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None):
|
def __init__(self, device_id='video_publisher', registry_name="", device_uuid='', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None):
|
||||||
# 初始化BaseROS2DeviceNode,使用自身作为driver_instance
|
# 初始化BaseROS2DeviceNode,使用自身作为driver_instance
|
||||||
BaseROS2DeviceNode.__init__(
|
BaseROS2DeviceNode.__init__(
|
||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class ControllerNode(BaseROS2DeviceNode):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
|
registry_name: str,
|
||||||
controller_func: Callable,
|
controller_func: Callable,
|
||||||
update_rate: float,
|
update_rate: float,
|
||||||
inputs: Dict[str, Dict[str, type | str]],
|
inputs: Dict[str, Dict[str, type | str]],
|
||||||
@@ -51,6 +52,7 @@ class ControllerNode(BaseROS2DeviceNode):
|
|||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings=action_value_mappings,
|
action_value_mappings=action_value_mappings,
|
||||||
hardware_interface=hardware_interface,
|
hardware_interface=hardware_interface,
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name="host_node",
|
||||||
device_uuid=host_node_dict["uuid"],
|
device_uuid=host_node_dict["uuid"],
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
|
action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
|
||||||
@@ -302,7 +303,8 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
} # 用来存储多个ActionClient实例
|
} # 用来存储多个ActionClient实例
|
||||||
self._action_value_mappings: Dict[str, Dict] = (
|
self._action_value_mappings: Dict[str, Dict] = (
|
||||||
{}
|
{}
|
||||||
) # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系
|
) # device_id -> action_value_mappings(本地+远程设备统一存储)
|
||||||
|
self._slave_registry_configs: Dict[str, Dict] = {} # registry_name -> registry_config(含action_value_mappings)
|
||||||
self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
|
self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
|
||||||
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
|
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
|
||||||
self._last_discovery_time = 0.0 # 上次设备发现的时间
|
self._last_discovery_time = 0.0 # 上次设备发现的时间
|
||||||
@@ -636,6 +638,8 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self.device_machine_names[device_id] = "本地"
|
self.device_machine_names[device_id] = "本地"
|
||||||
self.devices_instances[device_id] = d
|
self.devices_instances[device_id] = d
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
|
self._action_value_mappings[device_id] = d._ros_node._action_value_mappings
|
||||||
|
# noinspection PyProtectedMember
|
||||||
for action_name, action_value_mapping in d._ros_node._action_value_mappings.items():
|
for action_name, action_value_mapping in d._ros_node._action_value_mappings.items():
|
||||||
if action_name.startswith("auto-") or str(action_value_mapping.get("type", "")).startswith(
|
if action_name.startswith("auto-") or str(action_value_mapping.get("type", "")).startswith(
|
||||||
"UniLabJsonCommand"
|
"UniLabJsonCommand"
|
||||||
@@ -1168,6 +1172,10 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
def _node_info_update_callback(self, request, response):
|
def _node_info_update_callback(self, request, response):
|
||||||
"""
|
"""
|
||||||
更新节点信息回调
|
更新节点信息回调
|
||||||
|
|
||||||
|
处理两种消息:
|
||||||
|
1. 首次上报(main_slave_run): 带 devices_config + registry_config,存储 action_value_mappings
|
||||||
|
2. 设备重注册(SYNC_SLAVE_NODE_INFO): 带 edge_device_id + registry_name,用 registry_name 索引已存储的 mappings
|
||||||
"""
|
"""
|
||||||
self.lab_logger().trace(f"[Host Node] Node info update request received: {request}")
|
self.lab_logger().trace(f"[Host Node] Node info update request received: {request}")
|
||||||
try:
|
try:
|
||||||
@@ -1179,12 +1187,48 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
info = info["SYNC_SLAVE_NODE_INFO"]
|
info = info["SYNC_SLAVE_NODE_INFO"]
|
||||||
machine_name = info["machine_name"]
|
machine_name = info["machine_name"]
|
||||||
edge_device_id = info["edge_device_id"]
|
edge_device_id = info["edge_device_id"]
|
||||||
|
registry_name = info.get("registry_name", "")
|
||||||
self.device_machine_names[edge_device_id] = machine_name
|
self.device_machine_names[edge_device_id] = machine_name
|
||||||
|
|
||||||
|
# 用 registry_name 索引已存储的 registry_config,获取 action_value_mappings
|
||||||
|
if registry_name and registry_name in self._slave_registry_configs:
|
||||||
|
action_mappings = self._slave_registry_configs[registry_name].get(
|
||||||
|
"class", {}
|
||||||
|
).get("action_value_mappings", {})
|
||||||
|
if action_mappings:
|
||||||
|
self._action_value_mappings[edge_device_id] = action_mappings
|
||||||
|
self.lab_logger().info(
|
||||||
|
f"[Host Node] Loaded {len(action_mappings)} action mappings "
|
||||||
|
f"for remote device {edge_device_id} (registry: {registry_name})"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
devices_config = info.pop("devices_config")
|
devices_config = info.pop("devices_config")
|
||||||
registry_config = info.pop("registry_config")
|
registry_config = info.pop("registry_config")
|
||||||
if registry_config:
|
if registry_config:
|
||||||
http_client.resource_registry({"resources": registry_config})
|
http_client.resource_registry({"resources": registry_config})
|
||||||
|
|
||||||
|
# 存储 slave 的 registry_config,用于后续 SYNC_SLAVE_NODE_INFO 索引
|
||||||
|
for reg_name, reg_data in registry_config.items():
|
||||||
|
if isinstance(reg_data, dict) and "class" in reg_data:
|
||||||
|
self._slave_registry_configs[reg_name] = reg_data
|
||||||
|
|
||||||
|
# 解析 devices_config,建立 device_id -> action_value_mappings 映射
|
||||||
|
if devices_config:
|
||||||
|
for device_tree in devices_config:
|
||||||
|
for device_dict in device_tree:
|
||||||
|
device_id = device_dict.get("id", "")
|
||||||
|
class_name = device_dict.get("class", "")
|
||||||
|
if device_id and class_name and class_name in self._slave_registry_configs:
|
||||||
|
action_mappings = self._slave_registry_configs[class_name].get(
|
||||||
|
"class", {}
|
||||||
|
).get("action_value_mappings", {})
|
||||||
|
if action_mappings:
|
||||||
|
self._action_value_mappings[device_id] = action_mappings
|
||||||
|
self.lab_logger().info(
|
||||||
|
f"[Host Node] Stored {len(action_mappings)} action mappings "
|
||||||
|
f"for remote device {device_id} (class: {class_name})"
|
||||||
|
)
|
||||||
|
|
||||||
self.lab_logger().debug(f"[Host Node] Node info update: {info}")
|
self.lab_logger().debug(f"[Host Node] Node info update: {info}")
|
||||||
response.response = "OK"
|
response.response = "OK"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ from rclpy.callback_groups import ReentrantCallbackGroup
|
|||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
class JointRepublisher(BaseROS2DeviceNode):
|
class JointRepublisher(BaseROS2DeviceNode):
|
||||||
def __init__(self,device_id,resource_tracker, **kwargs):
|
def __init__(self,device_id, registry_name, resource_tracker, **kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
hardware_interface={},
|
hardware_interface={},
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from unilabos.resources.graphio import initialize_resources
|
|||||||
from unilabos.registry.registry import lab_registry
|
from unilabos.registry.registry import lab_registry
|
||||||
|
|
||||||
class ResourceMeshManager(BaseROS2DeviceNode):
|
class ResourceMeshManager(BaseROS2DeviceNode):
|
||||||
def __init__(self, resource_model: dict, resource_config: list,resource_tracker, device_id: str = "resource_mesh_manager", rate=50, **kwargs):
|
def __init__(self, resource_model: dict, resource_config: list,resource_tracker, device_id: str = "resource_mesh_manager", registry_name: str = "", rate=50, **kwargs):
|
||||||
"""初始化资源网格管理器节点
|
"""初始化资源网格管理器节点
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -37,6 +37,7 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
hardware_interface={},
|
hardware_interface={},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeRe
|
|||||||
|
|
||||||
|
|
||||||
class ROS2SerialNode(BaseROS2DeviceNode):
|
class ROS2SerialNode(BaseROS2DeviceNode):
|
||||||
def __init__(self, device_id, port: str, baudrate: int = 9600, resource_tracker: DeviceNodeResourceTracker=None):
|
def __init__(self, device_id, registry_name, port: str, baudrate: int = 9600, resource_tracker: DeviceNodeResourceTracker=None):
|
||||||
# 保存属性,以便在调用父类初始化前使用
|
# 保存属性,以便在调用父类初始化前使用
|
||||||
self.port = port
|
self.port = port
|
||||||
self.baudrate = baudrate
|
self.baudrate = baudrate
|
||||||
@@ -28,6 +28,7 @@ class ROS2SerialNode(BaseROS2DeviceNode):
|
|||||||
BaseROS2DeviceNode.__init__(
|
BaseROS2DeviceNode.__init__(
|
||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
|
registry_name=registry_name,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
*,
|
*,
|
||||||
driver_instance: "WorkstationBase",
|
driver_instance: "WorkstationBase",
|
||||||
device_id: str,
|
device_id: str,
|
||||||
|
registry_name: str,
|
||||||
device_uuid: str,
|
device_uuid: str,
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
action_value_mappings: Dict[str, Any],
|
action_value_mappings: Dict[str, Any],
|
||||||
@@ -62,6 +63,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=driver_instance,
|
driver_instance=driver_instance,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
|
action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
|
||||||
|
|||||||
@@ -184,6 +184,51 @@ def get_all_subscriptions(instance) -> list:
|
|||||||
return subscriptions
|
return subscriptions
|
||||||
|
|
||||||
|
|
||||||
|
def always_free(func: F) -> F:
|
||||||
|
"""
|
||||||
|
标记动作为永久闲置(不受busy队列限制)的装饰器
|
||||||
|
|
||||||
|
被此装饰器标记的 action 方法,在执行时不会受到设备级别的排队限制,
|
||||||
|
任何时候请求都可以立即执行。适用于查询类、状态读取类等轻量级操作。
|
||||||
|
|
||||||
|
Example:
|
||||||
|
class MyDriver:
|
||||||
|
@always_free
|
||||||
|
def query_status(self, param: str):
|
||||||
|
# 这个动作可以随时执行,不需要排队
|
||||||
|
return self._status
|
||||||
|
|
||||||
|
def transfer(self, volume: float):
|
||||||
|
# 这个动作会按正常排队逻辑执行
|
||||||
|
pass
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- 可以与其他装饰器组合使用,@always_free 应放在最外层
|
||||||
|
- 仅影响 WebSocket 调度层的 busy/free 判断,不影响 ROS2 层
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
wrapper._is_always_free = True # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
return wrapper # type: ignore[return-value]
|
||||||
|
|
||||||
|
|
||||||
|
def is_always_free(func) -> bool:
|
||||||
|
"""
|
||||||
|
检查函数是否被标记为永久闲置
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: 被检查的函数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
如果函数被 @always_free 装饰则返回 True,否则返回 False
|
||||||
|
"""
|
||||||
|
return getattr(func, "_is_always_free", False)
|
||||||
|
|
||||||
|
|
||||||
def not_action(func: F) -> F:
|
def not_action(func: F) -> F:
|
||||||
"""
|
"""
|
||||||
标记方法为非动作的装饰器
|
标记方法为非动作的装饰器
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from ast import Constant
|
|||||||
|
|
||||||
from unilabos.resources.resource_tracker import PARAM_SAMPLE_UUIDS
|
from unilabos.resources.resource_tracker import PARAM_SAMPLE_UUIDS
|
||||||
from unilabos.utils import logger
|
from unilabos.utils import logger
|
||||||
from unilabos.utils.decorator import is_not_action
|
from unilabos.utils.decorator import is_not_action, is_always_free
|
||||||
|
|
||||||
|
|
||||||
class ImportManager:
|
class ImportManager:
|
||||||
@@ -282,6 +282,9 @@ class ImportManager:
|
|||||||
continue
|
continue
|
||||||
# 其他非_开头的方法归类为action
|
# 其他非_开头的方法归类为action
|
||||||
method_info = self._analyze_method_signature(method)
|
method_info = self._analyze_method_signature(method)
|
||||||
|
# 检查是否被 @always_free 装饰器标记
|
||||||
|
if is_always_free(method):
|
||||||
|
method_info["always_free"] = True
|
||||||
result["action_methods"][name] = method_info
|
result["action_methods"][name] = method_info
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -339,6 +342,9 @@ class ImportManager:
|
|||||||
if self._is_not_action_method(node):
|
if self._is_not_action_method(node):
|
||||||
continue
|
continue
|
||||||
# 其他非_开头的方法归类为action
|
# 其他非_开头的方法归类为action
|
||||||
|
# 检查是否被 @always_free 装饰器标记
|
||||||
|
if self._is_always_free_method(node):
|
||||||
|
method_info["always_free"] = True
|
||||||
result["action_methods"][method_name] = method_info
|
result["action_methods"][method_name] = method_info
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -474,6 +480,13 @@ class ImportManager:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _is_always_free_method(self, node: ast.FunctionDef) -> bool:
|
||||||
|
"""检查是否是@always_free装饰的方法"""
|
||||||
|
for decorator in node.decorator_list:
|
||||||
|
if isinstance(decorator, ast.Name) and decorator.id == "always_free":
|
||||||
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user