diff --git a/.conda/base/recipe.yaml b/.conda/base/recipe.yaml index 2916af79..57318aae 100644 --- a/.conda/base/recipe.yaml +++ b/.conda/base/recipe.yaml @@ -3,7 +3,7 @@ package: name: unilabos - version: 0.11.2 + version: 0.11.3 source: path: ../../unilabos @@ -54,7 +54,7 @@ requirements: - pymodbus - matplotlib - pylibftdi - - uni-lab::unilabos-env ==0.11.2 + - uni-lab::unilabos-env ==0.11.3 about: repository: https://github.com/deepmodeling/Uni-Lab-OS diff --git a/.conda/environment/recipe.yaml b/.conda/environment/recipe.yaml index 4ee6b75c..c41dac2b 100644 --- a/.conda/environment/recipe.yaml +++ b/.conda/environment/recipe.yaml @@ -2,7 +2,7 @@ package: name: unilabos-env - version: 0.11.2 + version: 0.11.3 build: noarch: generic diff --git a/.conda/full/recipe.yaml b/.conda/full/recipe.yaml index 6db0d77a..b3a83990 100644 --- a/.conda/full/recipe.yaml +++ b/.conda/full/recipe.yaml @@ -3,7 +3,7 @@ package: name: unilabos-full - version: 0.11.2 + version: 0.11.3 build: noarch: generic @@ -11,7 +11,7 @@ build: requirements: run: # Base unilabos package (includes unilabos-env) - - uni-lab::unilabos ==0.11.2 + - uni-lab::unilabos ==0.11.3 # Documentation tools - sphinx - sphinx_rtd_theme diff --git a/.cursor/skills/add-device/SKILL.md b/.cursor/skills/add-device/SKILL.md index 522c05bf..dc77c6b7 100644 --- a/.cursor/skills/add-device/SKILL.md +++ b/.cursor/skills/add-device/SKILL.md @@ -5,9 +5,98 @@ description: Guide for adding new devices to Uni-Lab-OS (接入新设备). Uses # 添加新设备到 Uni-Lab-OS -**第一步:** 使用 Read 工具读取 `docs/ai_guides/add_device.md`,获取完整的设备接入指南。 +本 Skill 是自包含的设备接入指南,不依赖外部文档。迁移给别人时,只复制 `.cursor/skills/add-device/SKILL.md` 即可获得核心规则、模板、验证方式和常见错误清单。 -该指南包含设备类别(物模型)列表、通信协议模板、常见错误检查清单等。搜索 `unilabos/devices/` 获取已有设备的实现参考。 +开始实现前,仍应搜索 `unilabos/devices/` 获取同类别已有设备的接口、参数名、状态字符串和返回值风格作为参考。 + +--- + +## 接入工作流 + +按下面顺序推进,并在工作中维护进度: + +```text +设备接入进度: +- [ ] 1. 确定设备类别(物模型)和对外单位 +- [ ] 2. 确定通信协议 +- [ ] 3. 收集指令协议(SDK、厂商文档、寄存器表、HTTP API、用户口述) +- [ ] 4. 对齐同类设备接口(搜索 unilabos/devices/) +- [ ] 5. 创建驱动 unilabos/devices//.py +- [ ] 6. 验证可导入、注册表扫描、启动测试 +- [ ] 7. 如需要,配置实验图文件 +``` + +## 设备类别(物模型) + +优先使用已有类别。只有确实无法归类时才使用 `custom`。 + +| 类别 ID | 说明 | 标准属性 | 标准动作 | +|---|---|---|---| +| `temperature` | 加热、冷却、温控 | `temp`, `temp_target`, `status` | `set_temperature`, `stop` | +| `pump_and_valve` | 泵、阀门、注射器 | 见子类型表 | 见子类型表 | +| `motor` | 电机、步进马达 | `position`, `status` | `enable`, `move_position`, `move_speed`, `stop` | +| `heaterstirrer` | 加热搅拌一体机 | `temp`, `stir_speed`, `status` | `set_temperature`, `stir`, `stop` | +| `balance` | 天平、称重 | `weight`, `unit`, `status` | `tare`, `read_weight` | +| `sensor` | 传感器(液位、温度等) | `value`, `level`, `status` | `read_value`, `set_threshold` | +| `liquid_handling` | 液体处理机器人 | `status`, `deck_state` | `transfer_liquid`, `aspirate`, `dispense` | +| `robot_arm` | 机械臂 | `arm_pose`, `arm_status` | `moveit_task`, `pick_and_place` | +| `workstation` | 工作站、组合设备 | `workflow_sequence`, `material_info` | `create_order`, `scheduler_start`, `scheduler_stop` | +| `virtual` | 虚拟、模拟设备 | 按模拟的真实设备定义 | 按模拟的真实设备定义 | +| `custom` | 不属于以上类别 | 用户自定义 | 用户自定义 | + +`pump_and_valve` 子类型: + +| 子类型 | 最小通用属性 | 最小通用动作 | 单位约定 | +|---|---|---|---| +| 注射泵(syringe pump) | `status`, `valve_position`, `position` | `initialize`, `set_valve_position`, `set_position`, `pull_plunger`, `push_plunger`, `stop_operation` | 体积=mL, 速度=mL/s | +| 电磁阀(solenoid valve) | `status`, `valve_position` | `open`, `close`, `set_valve_position` | 无 | +| 蠕动泵(peristaltic pump) | `status`, `speed` | `start`, `stop`, `set_speed` | 流速=mL/min | + +对外暴露的属性和动作参数必须使用用户友好的物理单位(mL、ul、degC、RPM 等),硬件原始值转换放在驱动内部。 + +## 通信协议和指令来源 + +先确认通信方式,再确认具体指令协议。物模型只定义设备“应该做什么”,不会告诉你硬件“具体发什么字节/请求”。 + +| 协议 | 常用 config 参数 | 常用依赖 | 现有抽象 | +|---|---|---|---| +| Serial (RS232/RS485) | `port`, `baudrate`, `timeout` | `pyserial` | 直接使用 `serial.Serial` | +| Modbus RTU | `port`, `baudrate`, `slave_id` | `pymodbus` | `device_comms/modbus_plc/` | +| Modbus TCP | `host`, `port`, `slave_id` | `pymodbus` | `device_comms/modbus_plc/` | +| TCP Socket | `host`, `port`, `timeout` | stdlib | 直接使用 `socket` | +| HTTP API | `url`, `token`, `timeout` | `requests` | `device_comms/rpc.py` | +| OPC UA | `url` | `opcua` | `device_comms/opcua_client/` | +| 无通信(虚拟) | 无 | 无 | 在动作中模拟行为 | + +必须从以下来源之一获得指令细节: + +| 来源 | 处理方式 | +|---|---| +| 现成 SDK/驱动代码 | 读取代码,提取指令逻辑,包装进 Uni-Lab-OS 类 | +| 协议文档/手册 | 解析命令、响应、校验、寄存器、错误码 | +| 用户口述 | 按描述实现指令编解码,标出不确定点 | +| 标准协议 | 使用标准实现,例如 Modbus 寄存器表、SCPI | +| 虚拟设备 | 跳过硬件通信,在动作方法中维护模拟状态 | + +## 对齐已有实现(强制) + +实现前必须搜索 `unilabos/devices/` 中同类别设备: + +- 参数名必须与已有设备保持一致;动作方法参数名是接口契约,不要随意改成 `volume_ml`、`target_temp_c` 这类新名字。 +- `status` 字符串值要和同类设备一致,优先使用英文稳定值,例如 `Idle`、`Running`、`Error`。 +- 状态属性用 `@property` + `@topic_config()` 明确声明。 +- 返回值使用结构化 dict,至少包含 `success`,需要给前端展示的信息放在 `message`、`data`、`error` 等字段。 + +## 架构选择 + +| 场景 | 推荐方式 | +|---|---| +| 简单设备 | 纯 Python 类 + `@device` | +| 工作站/组合设备 | `WorkstationBase` 或项目内已有工作站模式 | +| 液体处理 | `LiquidHandlerAbstract` / PyLabRobot 相关模式 | +| Modbus 设备 | 复用 `device_comms/modbus_plc/` 或项目内 Modbus 示例 | +| OPC UA 设备 | 复用 `device_comms/opcua_client/` | +| 外部独立包 | 使用 `create-device-package` skill | --- @@ -87,6 +176,29 @@ Args: - 如果只写 `param: 参数说明`,`title` 会兜底为字段名,`description` 使用参数说明。 - 如果没有写参数文档,生成器也会兜底补齐 `title=<字段名>` 和 `description=""`,但新设备应优先写清楚显示名和说明。 +### 特殊参数类型:ResourceSlot / DeviceSlot + +需要前端选择资源或设备时,用特殊类型注解,registry 会自动生成 `placeholder_keys`: + +```python +from typing import List +from unilabos.registry.placeholder_type import DeviceSlot, ResourceSlot + +@action(description="转移液体") +def transfer(self, source: ResourceSlot, target: ResourceSlot, volume_ul: float) -> dict: + """ + Args: + source[源资源]: 源容器或孔位。 + target[目标资源]: 目标容器或孔位。 + volume_ul[体积(ul)]: 转移体积。 + """ + return {"success": True} + +@action(description="同步设备") +def sync_devices(self, devices: List[DeviceSlot]) -> dict: + return {"success": True, "count": len(devices)} +``` + ### @topic_config — 状态属性配置 ```python @@ -194,3 +306,154 @@ class MyDevice: - `post_init` 用 `@not_action` 标记,参数类型标注为 `BaseROS2DeviceNode` - 运行时状态存储在 `self.data` 字典中 - 设备文件放在 `unilabos/devices//` 目录下 + +--- + +## 通信实现片段 + +Serial 文本指令: + +```python +def _send_command(self, cmd: str) -> str: + self.ser.write(f"{cmd}\r\n".encode()) + return self.ser.readline().decode().strip() +``` + +RS-485 响应解析要先定位帧头,不要用硬编码索引直接解析原始响应: + +```python +def _normalize_response(self, raw: str, start_marker: str = "/") -> str: + pos = raw.find(start_marker) + return raw[pos:] if pos >= 0 else raw +``` + +自定义二进制帧: + +```python +def _build_frame(self, func_code: int, data: bytes) -> bytes: + frame = bytearray([0xFE, func_code]) + bytearray(data) + checksum = sum(frame[1:]) % 256 + frame.append(checksum) + return bytes(frame) +``` + +Modbus 寄存器映射: + +```python +REGISTER_MAP = { + "temp_target": {"addr": 0x000B, "scale": 10}, +} + +def set_temperature(self, temp: float, **kwargs) -> bool: + reg = REGISTER_MAP["temp_target"] + value = int(float(temp) * reg["scale"]) & 0xFFFF + self.client.write_register(reg["addr"], value, slave=self.slave_id) + self.data["temp_target"] = temp + return True +``` + +HTTP API 映射: + +```python +API_MAP = { + "set_temperature": { + "method": "POST", + "endpoint": "/api/temperature", + "body_key": "target", + }, +} +``` + +SDK 封装: + +```python +from my_device_sdk import DeviceController + +class MyDevice: + def __init__(self, device_id=None, config=None, **kwargs): + self.config = config or {} + self.controller = DeviceController(port=self.config.get("port", "COM1")) +``` + +--- + +## 验证 + +无需手写注册表 YAML。`@device` 装饰器 + AST 扫描会在启动或检查时生成注册表条目。 + +```bash +# 1. 模块可导入 +python -c "from unilabos.devices.. import " + +# 2. 启动测试 +unilab -g .json + +# 3. 仅检查注册表 +unilab --check_mode --skip_env_check +``` + +仅在旧代码无 `@device`、需要覆盖特殊字段、或做 `--complete_registry` 旧设备补全时,才考虑 YAML。新设备默认不要手写 YAML。 + +## 图文件节点模板 + +实验图 JSON 中的 `class` 对应 `@device(id=...)`,`config` 会传入 `__init__` 的 `config` 字典: + +```json +{ + "id": "my_device_1", + "name": "我的设备", + "children": [], + "parent": null, + "type": "device", + "class": "my_device", + "position": {"x": 0, "y": 0, "z": 0}, + "config": { + "port": "/dev/ttyUSB0", + "baudrate": 9600 + }, + "data": {} +} +``` + +工作站需要同时配置 `deck` 和 `children`: + +```json +{ + "nodes": [ + { + "id": "my_station", + "type": "device", + "class": "my_workstation", + "children": ["my_deck"], + "config": {}, + "deck": { + "data": { + "_resource_child_name": "my_deck", + "_resource_type": "unilabos.resources.my_module:MyDeck" + } + } + }, + { + "id": "my_deck", + "type": "deck", + "class": "MyDeckClass", + "parent": "my_station", + "config": {"type": "MyDeckClass", "setup": true} + } + ] +} +``` + +--- + +## 常见错误清单 + +- 缺少 `@device`:设备不会被 AST 扫描发现。 +- 只有 `@property` 没有 `@topic_config()`:属性不会稳定广播到 `status_types`。 +- `post_init` 没有 `@not_action`:会被误暴露为动作。 +- `self.data = {}`:空字典会导致属性读取和 schema 初始数据不稳定,必须预填充每个状态键。 +- 动作参数重命名:不要把同类设备已有的 `volume` 改成 `volume_ml`,参数名是接口契约。 +- `status` 使用中文或临时文本:前端和工作流依赖稳定英文状态值。 +- async 方法中使用 `time.sleep()`:应使用 `await self._ros_node.sleep(seconds)`。 +- 硬编码串口响应索引:RS-485 响应前可能有噪声字节,应先定位帧头。 +- 把硬件寄存器单位暴露给用户:对外使用物理单位,驱动内部做 scale 转换。 diff --git a/.cursor/skills/virtual-workbench/SKILL.md b/.cursor/skills/virtual-workbench/SKILL.md index 8f7aa0fe..1c295ffd 100644 --- a/.cursor/skills/virtual-workbench/SKILL.md +++ b/.cursor/skills/virtual-workbench/SKILL.md @@ -10,7 +10,8 @@ description: Operate Virtual Workbench via REST API — prepare materials, move - **device_id**: `virtual_workbench` - **Python 源码**: `unilabos/devices/virtual/workbench.py` - **设备类**: `VirtualWorkbench` -- **动作数**: 6(`auto-prepare_materials`, `auto-move_to_heating_station`, `auto-start_heating`, `auto-move_to_output`, `transfer`, `manual_confirm`) +- **当前纳入动作**: 5 个(`auto-prepare_materials`, `auto-move_to_heating_station`, `auto-start_heating`, `auto-move_to_output`, `transfer`) +- **暂跳过动作**: `manual_confirm`、扣电测试 `test`(需要启用时先从最新注册表重新提取 schema) - **设备描述**: 模拟工作台,包含 1 个机械臂(每次操作 2s,独占锁)和 3 个加热台(每次加热 60s,可并行) ### 典型工作流程 @@ -151,7 +152,8 @@ curl -s -X POST "$BASE/api/v1/lab/mcp/run/action" \ | `auto-start_heating` | `UniLabJsonCommand` | | `auto-move_to_output` | `UniLabJsonCommand` | | `transfer` | `UniLabJsonCommandAsync` | -| `manual_confirm` | `UniLabJsonCommand` | + +> `manual_confirm` 和扣电测试 `test` 当前不纳入本 skill 的推荐操作范围;不要基于历史 JSON 直接调用,需先重新生成并校验 schema。 ### 10. 查询任务状态 @@ -225,11 +227,9 @@ curl -s -X PUT "$BASE/api/v1/edge/material/node" \ | `transfer` | `resource` | ResourceSlot | 待转移物料数组 | | `transfer` | `target_device` | DeviceSlot | 目标设备路径 | | `transfer` | `mount_resource` | ResourceSlot | 目标孔位数组 | -| `manual_confirm` | `resource` | ResourceSlot | 确认用物料数组 | -| `manual_confirm` | `target_device` | DeviceSlot | 确认用目标设备 | -| `manual_confirm` | `mount_resource` | ResourceSlot | 确认用目标孔位数组 | > `prepare_materials`、`move_to_heating_station`、`start_heating`、`move_to_output` 这 4 个动作**无 Slot 字段**,参数为纯数值/整数。 +> `manual_confirm` 先跳过,不维护其 Slot 字段表。 --- @@ -270,3 +270,13 @@ prepare_materials (count=5) ``` 创建节点时,`prepare_materials` 的 5 个 output handle(`channel_1` ~ `channel_5`)分别连接到 5 个 `move_to_heating_station` 节点的 `material_input` handle。每个 `move_to_heating_station` 的 `heating_station_output` 和 `material_number_output` 连接到对应 `start_heating` 的 `station_id_input` 和 `material_number_input`。 + +`start_heating` 完成后还需要继续连接到 `move_to_output`,否则加热完成的物料不会移出加热台: + +| source action | source handle | target action | target handle | 传递参数 | +| ------------- | ------------- | ------------- | ------------- | -------- | +| `auto-prepare_materials` | `channel_N` | `auto-move_to_heating_station` | `material_input` | `material_number` | +| `auto-move_to_heating_station` | `heating_station_output` | `auto-start_heating` | `station_id_input` | `station_id` | +| `auto-move_to_heating_station` | `material_number_output` | `auto-start_heating` | `material_number_input` | `material_number` | +| `auto-start_heating` | `heating_done_station` | `auto-move_to_output` | `output_station_input` | `station_id` | +| `auto-start_heating` | `heating_done_material` | `auto-move_to_output` | `output_material_input` | `material_number` | diff --git a/.cursor/skills/virtual-workbench/action-index.md b/.cursor/skills/virtual-workbench/action-index.md index f67d9a91..7b3401fa 100644 --- a/.cursor/skills/virtual-workbench/action-index.md +++ b/.cursor/skills/virtual-workbench/action-index.md @@ -1,6 +1,8 @@ # Action Index — virtual_workbench -6 个动作,按功能分类。每个动作的完整 JSON Schema 在 `actions/.json`。 +当前纳入 5 个动作,按功能分类。每个动作的完整 JSON Schema 在 `actions/.json`。 + +暂跳过:`manual_confirm`、扣电测试 `test`。这两个动作需要启用时,先从最新 `req_device_registry_upload.json` 重新提取 schema 并校验参数。 --- @@ -60,17 +62,18 @@ --- -## 人工确认 +## 暂跳过动作 ### `manual_confirm` -创建人工确认节点,等待用户手动确认后继续(含物料转移上下文) +创建人工确认节点,等待用户手动确认后继续(含物料转移上下文)。当前先不纳入推荐操作范围。 - **action_type**: `UniLabJsonCommand` - **Schema**: [`actions/manual_confirm.json`](actions/manual_confirm.json) -- **核心参数**: `resource`, `target_device`, `mount_resource`, `timeout_seconds`, `assignee_user_ids` -- **占位符字段**: - - `resource` — **ResourceSlot**,物料数组 - - `target_device` — **DeviceSlot**,目标设备路径 - - `mount_resource` — **ResourceSlot**,目标孔位数组 - - `assignee_user_ids` — `unilabos_manual_confirm` 类型 +- **状态**: 暂跳过。源码参数已包含扣电测试相关字段,历史 JSON 可能过期;需要启用时重新提取 schema。 + +### `test` + +启动扣电测试。当前先不纳入本 skill。 + +- **状态**: 暂跳过。需要启用时从注册表生成 `actions/test.json` 后再补充索引。 diff --git a/recipes/msgs/recipe.yaml b/recipes/msgs/recipe.yaml index 5e6bbc85..f821c118 100644 --- a/recipes/msgs/recipe.yaml +++ b/recipes/msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.11.2 + version: 0.11.3 source: path: ../../unilabos_msgs target_directory: src diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index 3a7e50dc..18c724f1 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.11.2" + version: "0.11.3" source: path: ../.. diff --git a/setup.py b/setup.py index 201db6f4..8ada8c20 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.11.2', + version='0.11.3', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/unilabos/__init__.py b/unilabos/__init__.py index e2bd0728..1bebb74e 100644 --- a/unilabos/__init__.py +++ b/unilabos/__init__.py @@ -1 +1 @@ -__version__ = "0.11.2" +__version__ = "0.11.3" diff --git a/unilabos/app/ws_client.py b/unilabos/app/ws_client.py index fbe19b43..6e9b50f6 100644 --- a/unilabos/app/ws_client.py +++ b/unilabos/app/ws_client.py @@ -1034,11 +1034,16 @@ class MessageProcessor: success = host_node.notify_resource_tree_update(dev_id, act, item_list) - if success: + if success is True: logger.info( f"[MessageProcessor] Resource tree {act} completed for device {dev_id}, " f"items: {len(item_list)}" ) + elif success is None: + logger.info( + f"[MessageProcessor] Resource tree {act} skipped for device {dev_id}: " + "在线增加设备暂不支持" + ) else: logger.warning(f"[MessageProcessor] Resource tree {act} failed for device {dev_id}") @@ -1062,6 +1067,11 @@ class MessageProcessor: for item in device_list: target_node_id = item.get("target_node_id", "host_node") + if action == "add": + logger.info( + f"[DeviceManage] 在线增加设备暂不支持,跳过 add_device: {item.get('id', '')}" + ) + continue def _notify(target_id: str, act: str, cfg: ResourceDictType): try: diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 72514b99..8a732c80 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -45,6 +45,7 @@ from unilabos.resources.graphio import ( ) from unilabos.resources.plr_additional_res_reg import register from unilabos.ros.msgs.message_converter import ( + String, convert_to_ros_msg, convert_from_ros_msg_with_mapping, convert_to_ros_msg_with_mapping, @@ -250,7 +251,8 @@ class PropertyPublisher: ): self.node = node self.name = name - self.msg_type = msg_type + self.msg_type = self._normalize_msg_type(msg_type) + self.original_msg_type = msg_type self.get_method = get_method self.timer_period = initial_period self.print_publish = print_publish @@ -258,16 +260,36 @@ class PropertyPublisher: self._value = None try: - self.publisher_ = node.create_publisher(msg_type, f"{name}", qos) + self.publisher_ = node.create_publisher(self.msg_type, f"{name}", qos) except Exception as e: self.node.lab_logger().error( - f"StatusError, DeviceId: {self.node.device_id} 创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {e}" + f"StatusError, DeviceId: {self.node.device_id} 创建发布者 {name} 失败," + f"可能由于注册表有误,类型: {msg_type},错误: {e}" ) + self.msg_type = String + try: + self.publisher_ = node.create_publisher(self.msg_type, f"{name}", qos) + self.node.lab_logger().warning( + f"属性 {name} 的发布类型已降级为 String,原始类型: {msg_type}" + ) + except Exception: + self.publisher_ = None self.timer = node.create_timer(self.timer_period, self.publish_property) self.__loop = ROS2DeviceNode.get_asyncio_loop() - str_msg_type = str(msg_type)[8:-2] + str_msg_type = str(self.msg_type)[8:-2] self.node.lab_logger().trace(f"发布属性: {name}, 类型: {str_msg_type}, 周期: {initial_period}秒, QoS: {qos}") + @staticmethod + def _normalize_msg_type(msg_type): + if msg_type in (dict, list, tuple, set) or msg_type in ("dict", "list", "tuple", "set"): + return String + return msg_type + + def _normalize_value(self, value): + if self.msg_type is String and isinstance(value, (dict, list, tuple, set)): + return json.dumps(value, ensure_ascii=False, cls=TypeEncoder) + return value + def get_property(self): if asyncio.iscoroutinefunction(self.get_method): # 如果是异步函数,运行事件循环并等待结果 @@ -302,12 +324,16 @@ class PropertyPublisher: pass # self.node.lab_logger().trace(f"【.publish_property】发布 {self.msg_type}: {value}") if value is not None: + if self.publisher_ is None: + return + value = self._normalize_value(value) msg = convert_to_ros_msg(self.msg_type, value) self.publisher_.publish(msg) # self.node.lab_logger().trace(f"【.publish_property】属性 {self.name} 发布成功") except Exception as e: + topic = getattr(self.publisher_, "topic", self.name) self.node.lab_logger().error( - f"【.publish_property】发布属性 {self.publisher_.topic} 出错: {str(e)}\n{traceback.format_exc()}" + f"【.publish_property】发布属性 {topic} 出错: {str(e)}\n{traceback.format_exc()}" ) def change_frequency(self, period): diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 26b925bb..9e34c16b 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -1691,7 +1691,9 @@ class HostNode(BaseROS2DeviceNode): else: self.lab_logger().warning("⚠️ 收到无效的Pong响应(缺少ping_id)") - def notify_resource_tree_update(self, device_id: str, action: str, resource_uuid_list: List[str]) -> bool: + def notify_resource_tree_update( + self, device_id: str, action: str, resource_uuid_list: List[str] + ) -> Optional[bool]: """ 通知设备节点更新资源树 @@ -1701,13 +1703,14 @@ class HostNode(BaseROS2DeviceNode): resource_uuid_list: 资源UUIDs Returns: - bool: 操作是否成功 + True if the update completed, False if it failed, None if it was intentionally skipped. """ try: - # 检查设备是否存在 if device_id not in self.devices_names: - self.lab_logger().error(f"[Host Node-Resource] Device {device_id} not found in devices_names") - return False + self.lab_logger().info( + f"[Host Node-Resource] 在线增加设备暂不支持,跳过设备 {device_id} 的资源树 {action} 更新" + ) + return None namespace = self.devices_names[device_id] device_key = f"{namespace}/{device_id}" diff --git a/unilabos_msgs/package.xml b/unilabos_msgs/package.xml index a1ab4e85..3e49ed04 100644 --- a/unilabos_msgs/package.xml +++ b/unilabos_msgs/package.xml @@ -2,7 +2,7 @@ unilabos_msgs - 0.11.2 + 0.11.3 ROS2 Messages package for unilabos devices Junhan Chang Xuwznln