Merge branch 'dev' into prcix9320

This commit is contained in:
q434343
2026-04-09 18:12:25 +08:00
19 changed files with 3853 additions and 200 deletions

View File

@@ -1,4 +1,5 @@
import json
import os
# from nt import device_encoding
import threading
@@ -62,7 +63,7 @@ def main(
rclpy.init(args=rclpy_init_args)
else:
logger.info("[ROS] rclpy already initialized, reusing context")
executor = rclpy.__executor = MultiThreadedExecutor()
executor = rclpy.__executor = MultiThreadedExecutor(num_threads=max(os.cpu_count() * 4, 48))
# 创建主机节点
host_node = HostNode(
"host_node",
@@ -124,7 +125,7 @@ def slave(
rclpy.init(args=rclpy_init_args)
executor = rclpy.__executor
if not executor:
executor = rclpy.__executor = MultiThreadedExecutor()
executor = rclpy.__executor = MultiThreadedExecutor(num_threads=max(os.cpu_count() * 4, 48))
# 1.5 启动 executor 线程
thread = threading.Thread(target=executor.spin, daemon=True, name="slave_executor_thread")

View File

@@ -486,18 +486,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
if len(rts.root_nodes) == 1 and parent_resource is not None:
plr_instance = plr_instances[0]
if isinstance(plr_instance, Plate):
empty_liquid_info_in: List[Tuple[Optional[str], float]] = [(None, 0)] * plr_instance.num_items
if len(ADD_LIQUID_TYPE) == 1 and len(LIQUID_VOLUME) == 1 and len(LIQUID_INPUT_SLOT) > 1:
ADD_LIQUID_TYPE = ADD_LIQUID_TYPE * len(LIQUID_INPUT_SLOT)
LIQUID_VOLUME = LIQUID_VOLUME * len(LIQUID_INPUT_SLOT)
self.lab_logger().warning(
f"增加液体资源时数量为1自动补全为 {len(LIQUID_INPUT_SLOT)}"
)
for liquid_type, liquid_volume, liquid_input_slot in zip(
ADD_LIQUID_TYPE, LIQUID_VOLUME, LIQUID_INPUT_SLOT
):
empty_liquid_info_in[liquid_input_slot] = (liquid_type, liquid_volume)
plr_instance.set_well_liquids(empty_liquid_info_in)
try:
# noinspection PyProtectedMember
keys = list(plr_instance._ordering.keys())
@@ -511,6 +505,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
input_wells = []
for r in LIQUID_INPUT_SLOT:
input_wells.append(plr_instance.children[r])
for input_well, liquid_type, liquid_volume, liquid_input_slot in zip(
input_wells, ADD_LIQUID_TYPE, LIQUID_VOLUME, LIQUID_INPUT_SLOT
):
input_well.set_liquids([(liquid_type, liquid_volume, "uL")])
final_response["liquid_input_resource_tree"] = ResourceTreeSet.from_plr_resources(
input_wells
).dump()
@@ -1256,9 +1254,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
return self._lab_logger
def create_ros_publisher(self, attr_name, msg_type, initial_period=5.0):
"""创建ROS发布者,仅当方法/属性有 @topic_config 装饰器时才创建"""
# 检测 @topic_config 装饰器配置
topic_config = {}
"""创建ROS发布者。已在 status_types 中声明的属性直接创建;@topic_config 用于覆盖默认参数"""
topic_cfg = {}
driver_class = type(self.driver_instance)
# 区分 @property 和普通方法两种情况
@@ -1267,23 +1264,17 @@ class BaseROS2DeviceNode(Node, Generic[T]):
)
if is_prop:
# @property: 检测 fget 上的 @topic_config
class_attr = getattr(driver_class, attr_name)
if class_attr.fget is not None:
topic_config = get_topic_config(class_attr.fget)
topic_cfg = get_topic_config(class_attr.fget)
else:
# 普通方法: 直接检测 attr_name 方法上的 @topic_config
if hasattr(self.driver_instance, attr_name):
method = getattr(self.driver_instance, attr_name)
if callable(method):
topic_config = get_topic_config(method)
# 没有 @topic_config 装饰器则跳过发布
if not topic_config:
return
topic_cfg = get_topic_config(method)
# 发布名称优先级: @topic_config(name=...) > get_ 前缀去除 > attr_name
cfg_name = topic_config.get("name")
cfg_name = topic_cfg.get("name")
if cfg_name:
publish_name = cfg_name
elif attr_name.startswith("get_"):
@@ -1291,10 +1282,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
else:
publish_name = attr_name
# 使用装饰器配置或默认值
cfg_period = topic_config.get("period")
cfg_print = topic_config.get("print_publish")
cfg_qos = topic_config.get("qos")
# @topic_config 参数覆盖默认值
cfg_period = topic_cfg.get("period")
cfg_print = topic_cfg.get("print_publish")
cfg_qos = topic_cfg.get("qos")
period: float = cfg_period if cfg_period is not None else initial_period
print_publish: bool = cfg_print if cfg_print is not None else self._print_publish
qos: int = cfg_qos if cfg_qos is not None else 10
@@ -1486,13 +1477,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
if uuid_indices:
uuids = [item[1] for item in uuid_indices]
resource_tree = await self.get_resource(uuids)
plr_resources = resource_tree.to_plr_resources(requested_uuids=uuids)
plr_resources = resource_tree.to_plr_resources()
for i, (idx, _, resource_data) in enumerate(uuid_indices):
try:
plr_resource = plr_resources[i]
except Exception as e:
self.lab_logger().error(f"资源查询结果: 共 {len(queried_resources)} 个资源,但查询结果只有 {len(plr_resources)} 个资源,索引为 {i} 的资源不存在")
raise e
plr_resource = plr_resources[i]
if "sample_id" in resource_data:
plr_resource.unilabos_extra[EXTRA_SAMPLE_UUID] = resource_data["sample_id"]
queried_resources[idx] = plr_resource
@@ -1739,19 +1726,13 @@ class BaseROS2DeviceNode(Node, Generic[T]):
if arg_type == "unilabos.registry.placeholder_type:ResourceSlot":
resource_data = function_args[arg_name]
if isinstance(resource_data, dict) and "id" in resource_data:
uid = resource_data.get("uuid", "")
# 优先从本地追踪器直接取(避免服务端未同步导致的空返回)
local_fast = self.resource_tracker.uuid_to_resources.get(uid) if uid else None
if local_fast is not None:
function_args[arg_name] = local_fast
else:
try:
function_args[arg_name] = self._convert_resources_sync(uid)[0]
except Exception as e:
self.lab_logger().error(
f"转换ResourceSlot参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
)
raise JsonCommandInitError(f"ResourceSlot参数转换失败: {arg_name}")
try:
function_args[arg_name] = self._convert_resources_sync(resource_data["uuid"])[0]
except Exception as e:
self.lab_logger().error(
f"转换ResourceSlot参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
)
raise JsonCommandInitError(f"ResourceSlot参数转换失败: {arg_name}")
# 处理 ResourceSlot 列表
elif isinstance(arg_type, tuple) and len(arg_type) == 2:
@@ -1759,23 +1740,14 @@ class BaseROS2DeviceNode(Node, Generic[T]):
if arg_type[0] == "list" and arg_type[1] == resource_slot_type:
resource_list = function_args[arg_name]
if isinstance(resource_list, list):
uuids = [r["uuid"] for r in resource_list if isinstance(r, dict) and "id" in r]
# 先尝试本地追踪器批量取
local_hits = [
self.resource_tracker.uuid_to_resources[u]
for u in uuids
if u in self.resource_tracker.uuid_to_resources
]
if len(local_hits) == len(uuids):
function_args[arg_name] = local_hits
else:
try:
function_args[arg_name] = self._convert_resources_sync(*uuids) if uuids else []
except Exception as e:
self.lab_logger().error(
f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
)
raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}")
try:
uuids = [r["uuid"] for r in resource_list if isinstance(r, dict) and "id" in r]
function_args[arg_name] = self._convert_resources_sync(*uuids) if uuids else []
except Exception as e:
self.lab_logger().error(
f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
)
raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}")
# todo: 默认反报送
return function(**function_args)
@@ -1827,18 +1799,6 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 转换为 PLR 资源
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data)
if not len(tree_set.trees):
# 服务端未找到时尝试从本地追踪器兜底create_resource 刚完成但服务端未及时同步)
local_hits = [
self.resource_tracker.uuid_to_resources[uid]
for uid in uuids_list
if uid in self.resource_tracker.uuid_to_resources
]
if local_hits:
self.lab_logger().warning(
f"资源查询服务端返回空树,已从本地追踪器找到 "
f"{len(local_hits)}/{len(uuids_list)} 个资源: {uuids_list}"
)
return local_hits
raise Exception(f"资源查询返回空树: {raw_data}")
plr_resources = tree_set.to_plr_resources()