From 454b08537e4fd6b051f4038836192fe54531572f Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:25:07 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(networking):=20=E6=8F=90=E5=8F=96=20Ho?= =?UTF-8?q?stLink=20ROS2=20=E7=BB=84=E7=BD=91=E6=8E=A7=E5=88=B6=E9=80=9A?= =?UTF-8?q?=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced_usage/configuration.md | 16 ++ docs/developer_guide/networking_overview.md | 47 ++- tests/hostlink/__init__.py | 1 + tests/hostlink/test_cli.py | 52 ++++ tests/hostlink/test_link.py | 65 +++++ tests/hostlink/test_protocol.py | 27 ++ tests/hostlink/test_ros_assist.py | 64 +++++ tests/networking/__init__.py | 1 + tests/networking/test_hostlink_runtime.py | 77 +++++ tests/ros/test_domain_init.py | 37 +++ unilabos/app/main.py | 84 +++++- unilabos/config/config.py | 20 ++ unilabos/config/example_config.py | 9 + unilabos/hostlink/__init__.py | 11 + unilabos/hostlink/client.py | 304 ++++++++++++++++++++ unilabos/hostlink/protocol.py | 148 ++++++++++ unilabos/hostlink/ros_assist.py | 221 ++++++++++++++ unilabos/hostlink/runtime.py | 202 +++++++++++++ unilabos/hostlink/server.py | 295 +++++++++++++++++++ unilabos/ros/main_slave_run.py | 43 ++- 20 files changed, 1710 insertions(+), 14 deletions(-) create mode 100644 tests/hostlink/__init__.py create mode 100644 tests/hostlink/test_cli.py create mode 100644 tests/hostlink/test_link.py create mode 100644 tests/hostlink/test_protocol.py create mode 100644 tests/hostlink/test_ros_assist.py create mode 100644 tests/networking/__init__.py create mode 100644 tests/networking/test_hostlink_runtime.py create mode 100644 tests/ros/test_domain_init.py create mode 100644 unilabos/hostlink/__init__.py create mode 100644 unilabos/hostlink/client.py create mode 100644 unilabos/hostlink/protocol.py create mode 100644 unilabos/hostlink/ros_assist.py create mode 100644 unilabos/hostlink/runtime.py create mode 100644 unilabos/hostlink/server.py diff --git a/docs/advanced_usage/configuration.md b/docs/advanced_usage/configuration.md index a885e06d2..71ffa1413 100644 --- a/docs/advanced_usage/configuration.md +++ b/docs/advanced_usage/configuration.md @@ -71,6 +71,18 @@ class WSConfig: class HTTPConfig: remote_addr = "https://leap-lab.bohrium.com/api/v1" # 远程服务器地址 +# Host/Slave ROS2 组网控制通道 +class HostLinkConfig: + enable = True + host = "" # Slave 的 HostNode IP;推荐用 --host-node-ip 覆盖 + port = 7302 + bind = "0.0.0.0" + advertise_ip = "" # Host 多网卡时显式填写 Slave 可达 IP + ros_domain_id = "" # Host 下发的 ROS_DOMAIN_ID + ros_discovery_range = "" + ros_static_peers = "" + ros_discovery_server = "" # 外部 host:port;off 表示禁用 + # ROS配置 class ROSConfig: modules = [ @@ -107,6 +119,8 @@ class ROSConfig: | `ak` / `sk` | `--ak` / `--sk` | **安全考虑**:避免敏感信息泄露 | | `working_dir` | `--working_dir` | **灵活性**:不同环境可能使用不同目录 | | `is_host_mode` | `--is_slave` | **运行模式**:由启动场景决定,不固定 | +| HostNode 地址 | `--host-node-ip` | **组网目标**:Slave 按部署指定 Host IP | +| ROS2 domain | `--ros-domain-id` | **网络隔离**:由 Host 统一发布给 Slave | | `slave_no_host` | `--slave_no_host` | **运行模式**:从站特殊配置,按需使用 | | `upload_registry` | `--upload_registry` | **临时操作**:仅首次启动或更新时需要 | | `vis_2d_enable` | `--2d_vis` | **调试功能**:按需临时启用 | @@ -173,6 +187,8 @@ Uni-Lab 允许通过命令行参数覆盖配置文件中的设置,提供更灵 | `BasicConfig` | `upload_registry` | `--upload_registry` | 启动时上传注册表信息 | | `BasicConfig` | `vis_2d_enable` | `--2d_vis` | 启用 2D 可视化 | | `HTTPConfig` | `remote_addr` | `--addr` | 远程服务地址 | +| `HostLinkConfig` | `host` | `--host-node-ip` | Slave 连接的 HostNode IP/端口 | +| `HostLinkConfig` | `ros_domain_id`| `--ros-domain-id` | Host 发布或 Slave 本地兜底 domain | ### 特殊命令行参数 diff --git a/docs/developer_guide/networking_overview.md b/docs/developer_guide/networking_overview.md index dc7422350..9cd06ad21 100644 --- a/docs/developer_guide/networking_overview.md +++ b/docs/developer_guide/networking_overview.md @@ -75,7 +75,8 @@ unilab --ak your_ak --sk your_sk -g host_devices.json **启动命令**: ```bash -unilab --ak your_ak --sk your_sk -g slave_devices.json --is_slave +unilab --ak your_ak --sk your_sk -g slave_devices.json \ + --is_slave --host-node-ip 192.168.1.10 ``` --- @@ -105,6 +106,18 @@ ros2 topic list ros2 action list ``` +### HostLink 组网控制通道 + +Host 运行 ROS backend 时会在 TCP `7302` 监听 HostLink。Slave 通过 +`--host-node-ip [:port]` 建立控制连接,在 `rclpy.init` 前完成两件事: + +- 上报启动图中的设备 ID,供 Host 发现 Slave 及其设备归属; +- 接收并应用 Host 的 `ROS_DOMAIN_ID`、发现范围、静态对端和外部 Fast DDS + Discovery Server 地址。 + +HostLink 只辅助 ROS2 组网。设备 Action、节点注册和资源同步仍走现有 ROS2 +接口;本阶段没有通过 HostLink 提供物料查询或无 ROS backend。 + ### WebSocket 通信 **用途**: 主节点与云端通信 @@ -188,14 +201,16 @@ unilab --ak your_ak --sk your_sk -g all_devices.json **主节点**: ```bash -unilab --ak your_ak --sk your_sk -g host.json +unilab --ak your_ak --sk your_sk -g host.json --ros-domain-id 42 ``` **从节点**: ```bash -unilab --ak your_ak --sk your_sk -g slave1.json --is_slave -unilab --ak your_ak --sk your_sk -g slave2.json --is_slave --port 8003 +unilab --ak your_ak --sk your_sk -g slave1.json \ + --is-slave --host-node-ip 192.168.1.10 +unilab --ak your_ak --sk your_sk -g slave2.json \ + --is-slave --host-node-ip 192.168.1.10 --port 8003 ``` ### 云端集成模式 @@ -358,10 +373,34 @@ ping export ROS_DOMAIN_ID=42 ``` +推荐由 Host 启动参数统一 domain,Slave 不再重复维护: + +```bash +# Host:发布 domain 42 +unilab -g host.json --ros-domain-id 42 + +# Slave:通过 HostLink 获取 domain 42 和发现配置 +unilab -g slave.json --is-slave --host-node-ip 192.168.1.10 +``` + +如实验室使用已有 Fast DDS Discovery Server,可在 Host 指定并下发: + +```bash +unilab -g host.json --ros-domain-id 42 \ + --ros-discovery-server 192.168.1.10:11811 +``` + +此功能切片不会自动启动 Discovery Server 进程;未指定时仍沿用 ROS2/DDS +原有发现机制,并把 Host IP 加入 `ROS_STATIC_PEERS`。 + ### 防火墙配置 **建议做法**: +HostLink 需要 Slave 能访问 Host 的 TCP `7302`(若在 `--host-node-ip` 中指定 +其他端口,则开放对应端口)。该端口只承载组网握手、心跳和设备 ID,不承载设备 +动作或物料数据。 + 为了确保 ROS2 DDS 通信正常,建议直接关闭防火墙,而不是配置特定端口。ROS2 使用动态端口范围,配置特定端口可能导致通信问题。 **Linux**: diff --git a/tests/hostlink/__init__.py b/tests/hostlink/__init__.py new file mode 100644 index 000000000..446349053 --- /dev/null +++ b/tests/hostlink/__init__.py @@ -0,0 +1 @@ +"""HostLink unit tests.""" diff --git a/tests/hostlink/test_cli.py b/tests/hostlink/test_cli.py new file mode 100644 index 000000000..83e710c64 --- /dev/null +++ b/tests/hostlink/test_cli.py @@ -0,0 +1,52 @@ +import pytest + +from unilabos.app.main import _apply_hostlink_cli, parse_args +from unilabos.config.config import HostLinkConfig + + +def test_networking_cli_accepts_host_and_domain_aliases() -> None: + args = parse_args().parse_args( + [ + "--is_slave", + "--host-node-ip", + "10.0.0.9:7402", + "--ros-domain-id", + "52", + "--ros-discovery-range", + "OFF", + ] + ) + assert args.is_slave is True + assert args.host_node_ip == "10.0.0.9:7402" + assert args.ros_domain_id == 52 + assert args.ros_discovery_range == "OFF" + + +@pytest.mark.parametrize("domain_id", ["-1", "233"]) +def test_networking_cli_domain_range_is_validated_at_startup(domain_id) -> None: + args = parse_args().parse_args(["--ros-domain-id", domain_id]) + with pytest.raises(ValueError, match="between 0 and 232"): + _apply_hostlink_cli(vars(args), is_slave=False) + + +def test_networking_cli_is_applied_after_config(monkeypatch) -> None: + monkeypatch.setattr(HostLinkConfig, "host", "") + monkeypatch.setattr(HostLinkConfig, "port", 7302) + monkeypatch.setattr(HostLinkConfig, "ros_domain_id", "") + monkeypatch.setattr(HostLinkConfig, "ros_discovery_range", "") + monkeypatch.delenv("ROS_DOMAIN_ID", raising=False) + + _apply_hostlink_cli( + { + "host_node_ip": "10.0.0.9:7402", + "ros_domain_id": 52, + "ros_discovery_range": "OFF", + "ros_discovery_server": None, + }, + is_slave=True, + ) + + assert HostLinkConfig.host == "10.0.0.9" + assert HostLinkConfig.port == 7402 + assert HostLinkConfig.ros_domain_id == "52" + assert HostLinkConfig.ros_discovery_range == "OFF" diff --git a/tests/hostlink/test_link.py b/tests/hostlink/test_link.py new file mode 100644 index 000000000..4e5fb0d21 --- /dev/null +++ b/tests/hostlink/test_link.py @@ -0,0 +1,65 @@ +import time + +from unilabos.hostlink.client import HostLinkClient +from unilabos.hostlink.ros_assist import RosNetworkInfo +from unilabos.hostlink.server import HostLinkServer + + +def _wait_until(predicate, timeout: float = 2.0) -> bool: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if predicate(): + return True + time.sleep(0.01) + return bool(predicate()) + + +def test_hello_discovers_slave_devices_and_returns_ros_policy() -> None: + server = HostLinkServer("127.0.0.1", 0, heartbeat_timeout=1).start() + server.hello_payload = { + "host_id": "host-a", + "ros": RosNetworkInfo( + domain_id=42, + automatic_discovery_range="OFF", + static_peers=["127.0.0.1"], + ).to_dict(), + } + client = HostLinkClient( + "127.0.0.1", + server.port, + machine_name="slave-a", + device_ids=["pump-2", "pump-1", "pump-1"], + heartbeat_interval=0.05, + request_timeout=0.5, + ) + try: + assert client.connect_blocking(timeout=2) + assert client.hello_info["host_id"] == "host-a" + assert client.hello_ros_info().domain_id == 42 + assert client.ros_info().static_peers == ["127.0.0.1"] + assert _wait_until(lambda: server.has_device("pump-1")) + assert server.devices()["pump-2"]["machine_name"] == "slave-a" + peer = server.peers()[0] + assert peer["node_id"] == "device:pump-1" + assert peer["device_ids"] == ["pump-1", "pump-2"] + assert peer["online"] is True + finally: + client.close() + server.stop() + + +def test_same_device_set_keeps_logical_identity_after_reconnect() -> None: + server = HostLinkServer("127.0.0.1", 0).start() + first = HostLinkClient("127.0.0.1", server.port, device_ids=["robot-1"]) + second = HostLinkClient("127.0.0.1", server.port, device_ids=["robot-1"]) + try: + assert first.connect_blocking(timeout=2) + first.close() + assert second.connect_blocking(timeout=2) + assert _wait_until(lambda: len(server.peers()) == 1) + assert server.peers()[0]["node_id"] == "device:robot-1" + assert server.peers()[0]["online"] is True + finally: + first.close() + second.close() + server.stop() diff --git a/tests/hostlink/test_protocol.py b/tests/hostlink/test_protocol.py new file mode 100644 index 000000000..8a15a591e --- /dev/null +++ b/tests/hostlink/test_protocol.py @@ -0,0 +1,27 @@ +import io + +import pytest + +from unilabos.hostlink.protocol import ( + ActionType, + LinkError, + MAX_FRAME_BYTES, + encode_frame, + new_request, + read_message, +) + + +def test_request_round_trip() -> None: + request = new_request(ActionType.HELLO, {"device_ids": ["pump-1"]}) + assert read_message(io.BytesIO(encode_frame(request))) == request + + +def test_truncated_frame_is_rejected() -> None: + with pytest.raises(LinkError, match="truncated"): + read_message(io.BytesIO(b'{"kind":"req"}')) + + +def test_oversized_frame_is_rejected() -> None: + with pytest.raises(LinkError, match="too large"): + encode_frame({"kind": "req", "payload": "x" * MAX_FRAME_BYTES}) diff --git a/tests/hostlink/test_ros_assist.py b/tests/hostlink/test_ros_assist.py new file mode 100644 index 000000000..5670b871a --- /dev/null +++ b/tests/hostlink/test_ros_assist.py @@ -0,0 +1,64 @@ +import pytest + +from unilabos.hostlink.ros_assist import ( + RosNetworkInfo, + apply_ros_network_env, + build_host_ros_info, + parse_host_target, + validate_domain_id, +) + + +def test_host_policy_uses_domain_and_advertised_ip() -> None: + info = build_host_ros_info( + host_ip="10.20.0.5", + domain_id=37, + discovery_range="OFF", + environ={}, + ) + assert info.domain_id == 37 + assert info.static_peers == ["10.20.0.5"] + assert info.automatic_discovery_range == "OFF" + + +def test_slave_applies_host_policy_before_ros_init() -> None: + environ = {"ROS_DISCOVERY_SERVER": "stale:11811"} + applied = apply_ros_network_env( + RosNetworkInfo( + domain_id=12, + automatic_discovery_range="LOCALHOST", + static_peers=["10.0.0.8"], + discovery_server_disabled=True, + ), + environ, + ) + assert applied == { + "ROS_DOMAIN_ID": "12", + "ROS_AUTOMATIC_DISCOVERY_RANGE": "LOCALHOST", + "ROS_STATIC_PEERS": "10.0.0.8", + } + assert "ROS_DISCOVERY_SERVER" not in environ + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("10.0.0.8", ("10.0.0.8", 7302)), + ("host.local:7400", ("host.local", 7400)), + ("[fe80::1]:7500", ("fe80::1", 7500)), + ("fe80::1", ("fe80::1", 7302)), + ], +) +def test_parse_host_target(value, expected) -> None: + assert parse_host_target(value, 7302) == expected + + +def test_parse_host_target_rejects_non_numeric_port() -> None: + with pytest.raises(ValueError, match="invalid HostNode address"): + parse_host_target("host.local:not-a-port", 7302) + + +@pytest.mark.parametrize("value", [-1, 233]) +def test_domain_id_portable_range(value: int) -> None: + with pytest.raises(ValueError, match="between 0 and 232"): + validate_domain_id(value) diff --git a/tests/networking/__init__.py b/tests/networking/__init__.py new file mode 100644 index 000000000..e666ee974 --- /dev/null +++ b/tests/networking/__init__.py @@ -0,0 +1 @@ +"""Host/Slave networking integration tests.""" diff --git a/tests/networking/test_hostlink_runtime.py b/tests/networking/test_hostlink_runtime.py new file mode 100644 index 000000000..dc0e64281 --- /dev/null +++ b/tests/networking/test_hostlink_runtime.py @@ -0,0 +1,77 @@ +import os +from dataclasses import dataclass + +import pytest + +from unilabos.config.config import BasicConfig, HostLinkConfig +from unilabos.hostlink.runtime import ( + setup_hostlink_client, + setup_hostlink_server, + shutdown_hostlink, + startup_device_ids, +) + + +@pytest.fixture(autouse=True) +def isolated_hostlink_config(monkeypatch): + shutdown_hostlink() + monkeypatch.setattr(HostLinkConfig, "enable", True) + monkeypatch.setattr(HostLinkConfig, "host", "") + monkeypatch.setattr(HostLinkConfig, "port", 0) + monkeypatch.setattr(HostLinkConfig, "bind", "127.0.0.1") + monkeypatch.setattr(HostLinkConfig, "advertise_ip", "127.0.0.1") + monkeypatch.setattr(HostLinkConfig, "heartbeat_interval", 0.05) + monkeypatch.setattr(HostLinkConfig, "heartbeat_timeout", 1.0) + monkeypatch.setattr(HostLinkConfig, "connect_timeout", 0.5) + monkeypatch.setattr(HostLinkConfig, "request_timeout", 0.5) + monkeypatch.setattr(HostLinkConfig, "ros_assist_apply", True) + monkeypatch.setattr(HostLinkConfig, "ros_domain_id", "61") + monkeypatch.setattr(HostLinkConfig, "ros_discovery_range", "OFF") + monkeypatch.setattr(HostLinkConfig, "ros_static_peers", "") + monkeypatch.setattr(HostLinkConfig, "ros_discovery_server", "off") + monkeypatch.setattr(BasicConfig, "machine_name", "test-machine") + monkeypatch.setattr(BasicConfig, "slave_no_host", False) + monkeypatch.delenv("ROS_DOMAIN_ID", raising=False) + monkeypatch.delenv("ROS_STATIC_PEERS", raising=False) + monkeypatch.delenv("ROS_AUTOMATIC_DISCOVERY_RANGE", raising=False) + yield + shutdown_hostlink() + + +def test_runtime_syncs_domain_and_discovers_devices(monkeypatch) -> None: + server = setup_hostlink_server() + assert server is not None + monkeypatch.setattr(HostLinkConfig, "host", "127.0.0.1") + monkeypatch.setattr(HostLinkConfig, "port", server.port) + + client, domain_id = setup_hostlink_client(["centrifuge-1"], wait_for_host=True) + + assert client is not None and client.online + assert domain_id == 61 + assert os.environ["ROS_DOMAIN_ID"] == "61" + assert os.environ["ROS_AUTOMATIC_DISCOVERY_RANGE"] == "OFF" + assert "127.0.0.1" in os.environ["ROS_STATIC_PEERS"].split(";") + assert server.has_device("centrifuge-1") + + +@dataclass +class _Content: + id: str + type: str + + +@dataclass +class _Node: + res_content: _Content + + +class _Tree: + all_nodes = [ + _Node(_Content("pump-1", "device")), + _Node(_Content("plate-1", "resource")), + _Node(_Content("pump-2", "device")), + ] + + +def test_startup_device_ids_only_reports_devices() -> None: + assert startup_device_ids(_Tree()) == ["pump-1", "pump-2"] diff --git a/tests/ros/test_domain_init.py b/tests/ros/test_domain_init.py new file mode 100644 index 000000000..03d92d4b2 --- /dev/null +++ b/tests/ros/test_domain_init.py @@ -0,0 +1,37 @@ +from unilabos.ros import main_slave_run + + +def test_rclpy_receives_explicit_domain_id(monkeypatch) -> None: + calls = [] + monkeypatch.delenv("ROS_DOMAIN_ID", raising=False) + monkeypatch.setattr(main_slave_run.rclpy, "ok", lambda: False) + monkeypatch.setattr( + main_slave_run.rclpy, + "init", + lambda **kwargs: calls.append(kwargs), + ) + + main_slave_run._init_rclpy(["--log-level", "info"], 47) + + assert calls == [{"args": ["--log-level", "info"], "domain_id": 47}] + assert main_slave_run.os.environ["ROS_DOMAIN_ID"] == "47" + + +def test_old_rclpy_falls_back_to_environment_path(monkeypatch) -> None: + calls = [] + monkeypatch.delenv("ROS_DOMAIN_ID", raising=False) + monkeypatch.setattr(main_slave_run.rclpy, "ok", lambda: False) + + def fake_init(**kwargs): + calls.append(kwargs) + if "domain_id" in kwargs: + raise TypeError("unsupported domain_id") + + monkeypatch.setattr(main_slave_run.rclpy, "init", fake_init) + + main_slave_run._init_rclpy([], 9) + + assert calls == [ + {"args": [], "domain_id": 9}, + {"args": []}, + ] diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 2daf7729d..b587c70de 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -138,6 +138,38 @@ def load_config_from_file(config_path): load_config(config_path) +def _apply_hostlink_cli(args_dict: Dict[str, Any], *, is_slave: bool) -> None: + """Apply ROS HostLink CLI overrides after config/env loading.""" + + from unilabos.config.config import HostLinkConfig + from unilabos.hostlink.ros_assist import parse_host_target, validate_domain_id + + host_node_ip = str(args_dict.get("host_node_ip") or "").strip() + if host_node_ip: + host, hostlink_port = parse_host_target(host_node_ip, HostLinkConfig.port) + HostLinkConfig.port = hostlink_port + if is_slave: + HostLinkConfig.host = host + print_status( + f"Slave HostNode: {HostLinkConfig.host}:{HostLinkConfig.port}", + "info", + ) + else: + HostLinkConfig.advertise_ip = host + + ros_domain_id = validate_domain_id(args_dict.get("ros_domain_id")) + if ros_domain_id is not None: + HostLinkConfig.ros_domain_id = str(ros_domain_id) + os.environ["ROS_DOMAIN_ID"] = str(ros_domain_id) + print_status(f"ROS_DOMAIN_ID = {ros_domain_id}", "info") + if args_dict.get("ros_discovery_range") is not None: + HostLinkConfig.ros_discovery_range = args_dict["ros_discovery_range"] + if args_dict.get("ros_discovery_server") is not None: + HostLinkConfig.ros_discovery_server = str( + args_dict["ros_discovery_server"] + ).strip() + + def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser): # easier for user input, easier for dev search code option_strings = list(args._option_string_actions.keys()) @@ -193,10 +225,52 @@ def parse_args(): action="store_true", help="Run the backend as slave node (without host privileges).", ) + parser.add_argument( + "--host_node_ip", + "--host-node-ip", + dest="host_node_ip", + default="", + help=( + "HostNode address for a Slave, as ip/hostname or ip:port. " + "The HostLink control channel synchronizes ROS2 networking and device IDs." + ), + ) + parser.add_argument( + "--ros_domain_id", + "--ros-domain-id", + dest="ros_domain_id", + type=int, + default=None, + help=( + "ROS2 domain id (0-232). A Host advertises it to Slaves; " + "a Slave uses it as fallback until HostLink connects." + ), + ) + parser.add_argument( + "--ros_discovery_range", + "--ros-discovery-range", + dest="ros_discovery_range", + choices=["SYSTEM_DEFAULT", "SUBNET", "LOCALHOST", "OFF"], + default=None, + help="ROS_AUTOMATIC_DISCOVERY_RANGE advertised by the Host.", + ) + parser.add_argument( + "--ros_discovery_server", + "--ros-discovery-server", + dest="ros_discovery_server", + default=None, + help=( + "External Fast DDS discovery server host:port; use 'off' to clear it. " + "This slice does not start a discovery-server process." + ), + ) parser.add_argument( "--slave_no_host", action="store_true", - help="Skip waiting for host service in slave mode", + help=( + "Allow a Slave to start while HostLink/Host ROS services are offline; " + "the control channel keeps reconnecting in the background." + ), ) parser.add_argument( "--upload_registry", @@ -739,6 +813,12 @@ def main(): workflow_upload = args_dict.get("command") in ("workflow_upload", "wf") + # HostLink is a ROS-only control channel in this slice. It exchanges the + # ROS domain/discovery policy and Slave device IDs; normal device actions, + # resources and backend APIs keep their existing transports. + is_slave = bool(args_dict.get("is_slave", False)) + _apply_hostlink_cli(args_dict, is_slave=is_slave) + # 使用远程资源启动 if not workflow_upload and args_dict["use_remote_resource"]: print_status("使用远程资源启动", "info") @@ -753,7 +833,7 @@ def main(): BasicConfig.port = args_dict["port"] if args_dict["port"] else BasicConfig.port BasicConfig.disable_browser = args_dict["disable_browser"] or BasicConfig.disable_browser - BasicConfig.is_host_mode = not args_dict.get("is_slave", False) + BasicConfig.is_host_mode = not is_slave BasicConfig.slave_no_host = args_dict.get("slave_no_host", False) BasicConfig.upload_registry = args_dict.get("upload_registry", False) BasicConfig.no_update_feedback = args_dict.get("no_update_feedback", False) diff --git a/unilabos/config/config.py b/unilabos/config/config.py index be1f54cea..7dfd28d26 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -54,6 +54,26 @@ class HTTPConfig: schedule_addr = "" +# Host/Slave ROS2 组网控制通道。Host 在 ROS backend 启动时监听;Slave 只有在 +# ``host`` 非空(--host_node_ip)时连接,不接管物料、动作或微后端职责。 +class HostLinkConfig: + enable = True + host = "" # Slave 侧指定的 HostNode IP/主机名 + port = 7302 + bind = "0.0.0.0" + advertise_ip = "" # Host 下发给 Slave 的可达地址;空时自动探测 + heartbeat_interval = 5.0 + heartbeat_timeout = 15.0 + connect_timeout = 5.0 + request_timeout = 10.0 + ros_assist_apply = True + ros_domain_id = "" # 空时沿用 ROS_DOMAIN_ID + ros_discovery_range = "" # SYSTEM_DEFAULT / SUBNET / LOCALHOST / OFF + ros_static_peers = "" # 分号分隔 + # 外部 Fast DDS Discovery Server 的 host:port;off 表示明确清除继承值。 + ros_discovery_server = "" + + # ROS配置 class ROSConfig: modules = [ diff --git a/unilabos/config/example_config.py b/unilabos/config/example_config.py index d59d8b525..5e551c8bf 100644 --- a/unilabos/config/example_config.py +++ b/unilabos/config/example_config.py @@ -11,3 +11,12 @@ class WSConfig: max_reconnect_attempts = 999 # 最大重连次数 ws_ping_interval = 5 # ping间隔(秒),对齐服务端 PingPeriod ws_ping_timeout = 7 # pong等待超时(秒),对齐服务端 PongWait + + +# Host/Slave ROS2 组网;Slave 推荐通过 --host-node-ip 指定 Host,不必写死在文件中。 +class HostLinkConfig: + enable = True + port = 7302 + bind = "0.0.0.0" + advertise_ip = "" # Host 多网卡时填写 Slave 可达 IP + ros_domain_id = "" # Host 可在此统一配置,也可用 --ros-domain-id diff --git a/unilabos/hostlink/__init__.py b/unilabos/hostlink/__init__.py new file mode 100644 index 000000000..4611cfca4 --- /dev/null +++ b/unilabos/hostlink/__init__.py @@ -0,0 +1,11 @@ +"""Host/Slave ROS2 networking control channel.""" + +from unilabos.hostlink.client import HostLinkClient, get_hostlink_client +from unilabos.hostlink.server import HostLinkServer, get_hostlink_server + +__all__ = [ + "HostLinkClient", + "HostLinkServer", + "get_hostlink_client", + "get_hostlink_server", +] diff --git a/unilabos/hostlink/client.py b/unilabos/hostlink/client.py new file mode 100644 index 000000000..24e9ab164 --- /dev/null +++ b/unilabos/hostlink/client.py @@ -0,0 +1,304 @@ +"""Slave-side HostLink client for discovery and ROS2 configuration sync.""" + +from __future__ import annotations + +import socket +import threading +import time +import uuid +from typing import Any, Callable, Dict, Iterable, List, Optional + +from unilabos.hostlink.protocol import ( + ActionType, + LineReader, + LinkError, + PROTOCOL_VERSION, + RemoteError, + new_request, + read_message, + send_message, +) +from unilabos.hostlink.ros_assist import RosNetworkInfo +from unilabos.utils import logger + + +class _Pending: + __slots__ = ("event", "response") + + def __init__(self) -> None: + self.event = threading.Event() + self.response: Optional[Dict[str, Any]] = None + + +class HostLinkClient: + """Maintain one reconnecting TCP control connection to the Host.""" + + def __init__( + self, + host: str, + port: int = 7302, + machine_name: str = "", + device_ids: Optional[Iterable[str]] = None, + heartbeat_interval: float = 5.0, + connect_timeout: float = 5.0, + request_timeout: float = 10.0, + reconnect_max_backoff: float = 10.0, + on_status_change: Optional[Callable[[bool], None]] = None, + ) -> None: + if not str(host or "").strip(): + raise ValueError("HostLink host cannot be empty") + self.host = str(host).strip() + self.port = int(port) + self.machine_name = str(machine_name or "").strip() + self.heartbeat_interval = float(heartbeat_interval) + self.connect_timeout = float(connect_timeout) + self.request_timeout = float(request_timeout) + self.reconnect_max_backoff = float(reconnect_max_backoff) + self.on_status_change = on_status_change + self.node_id = self.machine_name or f"slave-{uuid.uuid4().hex}" + self.device_ids: List[str] = [] + self.configure_device_ids(device_ids or []) + self.capabilities = ["device-discovery", "ros-assist"] + self.hello_info: Dict[str, Any] = {} + + self._sock: Optional[socket.socket] = None + self._manager_thread: Optional[threading.Thread] = None + self._reader_thread: Optional[threading.Thread] = None + self._write_lock = threading.Lock() + self._pending: Dict[str, _Pending] = {} + self._pending_lock = threading.Lock() + self._stop = threading.Event() + self._connection_lost = threading.Event() + self._online = threading.Event() + self._status_condition = threading.Condition() + + def start(self) -> "HostLinkClient": + if self._manager_thread is not None and self._manager_thread.is_alive(): + return self + self._stop.clear() + self._manager_thread = threading.Thread( + target=self._run, + name="hostlink-client", + daemon=True, + ) + self._manager_thread.start() + return self + + def connect_blocking(self, timeout: Optional[float] = 10.0) -> bool: + """Start the reconnect manager and wait for a successful hello.""" + + self.start() + deadline = None if timeout is None else time.monotonic() + float(timeout) + with self._status_condition: + while not self._online.is_set() and not self._stop.is_set(): + remaining = None if deadline is None else deadline - time.monotonic() + if remaining is not None and remaining <= 0: + break + self._status_condition.wait(remaining) + return self._online.is_set() + + def close(self) -> None: + self._stop.set() + with self._status_condition: + self._status_condition.notify_all() + self._teardown_socket() + if self._manager_thread is not None and self._manager_thread.is_alive(): + self._manager_thread.join(timeout=3) + self._manager_thread = None + + @property + def online(self) -> bool: + return self._online.is_set() + + def configure_device_ids(self, device_ids: Iterable[str]) -> None: + normalized = sorted( + { + str(device_id).strip() + for device_id in device_ids + if str(device_id).strip() + } + ) + if normalized: + self.device_ids = normalized + self.node_id = f"device:{normalized[0]}" + + def request( + self, + action_type: str, + data: Optional[Dict[str, Any]] = None, + timeout: Optional[float] = None, + ) -> Any: + return self._request(action_type, data, timeout, require_online=True) + + def ros_info(self, timeout: Optional[float] = None) -> RosNetworkInfo: + data = self.request(ActionType.ROS_INFO, timeout=timeout) + return RosNetworkInfo.from_dict((data or {}).get("ros") or data) + + def hello_ros_info(self) -> RosNetworkInfo: + return RosNetworkInfo.from_dict(self.hello_info.get("ros")) + + def _identity_payload(self) -> Dict[str, Any]: + return { + "node_id": self.node_id, + "device_ids": list(self.device_ids), + "machine_name": self.machine_name, + "role": "slave", + "protocol_version": PROTOCOL_VERSION, + "capabilities": list(self.capabilities), + } + + def _request( + self, + action_type: str, + data: Optional[Dict[str, Any]], + timeout: Optional[float], + *, + require_online: bool, + ) -> Any: + sock = self._sock + if sock is None or (require_online and not self.online): + raise LinkError(f"hostlink offline ({self.host}:{self.port})") + message = new_request(action_type, data=data) + pending = _Pending() + request_id = str(message["id"]) + with self._pending_lock: + self._pending[request_id] = pending + try: + with self._write_lock: + send_message(sock, message) + if not pending.event.wait(timeout or self.request_timeout): + raise LinkError(f"request timeout: {action_type} ({request_id[:8]})") + except OSError as exc: + raise LinkError(f"request send failed: {exc}") from exc + finally: + with self._pending_lock: + self._pending.pop(request_id, None) + response = pending.response or {} + if not response.get("ok"): + raise RemoteError(str(response.get("error") or "remote error")) + return response.get("data") + + def _run(self) -> None: + backoff = 0.5 + while not self._stop.is_set(): + try: + self._connect_once() + backoff = 0.5 + self._heartbeat_loop() + except (OSError, LinkError) as exc: + logger.debug(f"[HostLink] connection cycle ended: {exc}") + self._set_online(False) + self._teardown_socket() + if not self._stop.is_set(): + self._stop.wait(backoff) + backoff = min(backoff * 2, self.reconnect_max_backoff) + + def _connect_once(self) -> None: + sock = socket.create_connection( + (self.host, self.port), + timeout=self.connect_timeout, + ) + sock.settimeout(None) + self._sock = sock + self._connection_lost.clear() + self._reader_thread = threading.Thread( + target=self._read_loop, + args=(sock,), + name="hostlink-reader", + daemon=True, + ) + self._reader_thread.start() + data = self._request( + ActionType.HELLO, + self._identity_payload(), + self.connect_timeout, + require_online=False, + ) + self.hello_info = dict(data or {}) + self._set_online(True) + logger.info( + f"[HostLink] connected to {self.host}:{self.port}; " + f"devices={self.device_ids}" + ) + + def _heartbeat_loop(self) -> None: + while not self._stop.wait(self.heartbeat_interval): + if self._connection_lost.is_set(): + raise LinkError("connection closed") + self.request(ActionType.PING, timeout=self.request_timeout) + + def _read_loop(self, sock: socket.socket) -> None: + reader = LineReader(sock) + try: + while not self._stop.is_set(): + message = read_message(reader) + if message is None: + break + if message.get("kind") != "resp": + continue + request_id = str(message.get("id") or "") + with self._pending_lock: + pending = self._pending.get(request_id) + if pending is not None: + pending.response = message + pending.event.set() + except (OSError, LinkError) as exc: + logger.debug(f"[HostLink] reader stopped: {exc}") + finally: + reader.close() + self._connection_lost.set() + self._set_online(False) + with self._pending_lock: + pending_items = list(self._pending.values()) + for pending in pending_items: + if pending.response is None: + pending.response = {"ok": False, "error": "connection closed"} + pending.event.set() + + def _teardown_socket(self) -> None: + sock, self._sock = self._sock, None + if sock is not None: + try: + sock.shutdown(socket.SHUT_RDWR) + except OSError: + pass + try: + sock.close() + except OSError: + pass + + def _set_online(self, value: bool) -> None: + changed = value != self._online.is_set() + if value: + self._online.set() + else: + self._online.clear() + with self._status_condition: + self._status_condition.notify_all() + if changed and self.on_status_change is not None: + try: + self.on_status_change(value) + except Exception: # noqa: BLE001 - observer must not kill reconnect + logger.exception("[HostLink] status callback failed") + + +_client_lock = threading.Lock() +_client: Optional[HostLinkClient] = None + + +def set_hostlink_client(client: Optional[HostLinkClient]) -> None: + global _client + with _client_lock: + _client = client + + +def get_hostlink_client() -> Optional[HostLinkClient]: + with _client_lock: + return _client + + +__all__ = [ + "HostLinkClient", + "get_hostlink_client", + "set_hostlink_client", +] diff --git a/unilabos/hostlink/protocol.py b/unilabos/hostlink/protocol.py new file mode 100644 index 000000000..a9b956d5c --- /dev/null +++ b/unilabos/hostlink/protocol.py @@ -0,0 +1,148 @@ +"""HostLink wire protocol: newline-delimited JSON over TCP. + +The first slice deliberately contains only networking control messages. Device +actions and material/resource queries continue to use the existing ROS2 and +HTTP paths. +""" + +from __future__ import annotations + +import json +import socket +import uuid +from typing import Any, Dict, Optional + +PROTOCOL_VERSION = 1 +MAX_FRAME_BYTES = 1024 * 1024 + + +class ActionType: + """Built-in networking actions.""" + + HELLO = "hello" + PING = "ping" + ROS_INFO = "ros_info" + + +class LinkError(Exception): + """Transport or framing error.""" + + +class RemoteError(LinkError): + """The remote endpoint returned ``ok=false``.""" + + +def new_request( + action_type: str, + data: Optional[Dict[str, Any]] = None, + request_id: str = "", +) -> Dict[str, Any]: + message: Dict[str, Any] = { + "v": PROTOCOL_VERSION, + "kind": "req", + "id": request_id or uuid.uuid4().hex, + "action_type": action_type, + } + if data is not None: + message["data"] = data + return message + + +def new_response( + request_id: str, + ok: bool, + data: Any = None, + error: str = "", +) -> Dict[str, Any]: + message: Dict[str, Any] = { + "v": PROTOCOL_VERSION, + "kind": "resp", + "id": request_id, + "ok": ok, + } + if ok: + message["data"] = data + else: + message["error"] = error or "unknown error" + return message + + +def encode_frame(message: Dict[str, Any]) -> bytes: + raw = ( + json.dumps(message, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + + b"\n" + ) + if len(raw) > MAX_FRAME_BYTES: + raise LinkError(f"frame too large: {len(raw)} bytes > {MAX_FRAME_BYTES}") + return raw + + +def send_message(sock: socket.socket, message: Dict[str, Any]) -> None: + sock.sendall(encode_frame(message)) + + +class LineReader: + """Buffered ``recv``-based line reader safe for timed sockets.""" + + def __init__(self, sock: socket.socket, max_bytes: int = 0) -> None: + self._sock = sock + self._max = max_bytes or MAX_FRAME_BYTES + self._buffer = bytearray() + self._eof = False + + def readline(self, limit: int = 0) -> bytes: + del limit + while True: + newline_at = self._buffer.find(b"\n") + if newline_at >= 0: + line = bytes(self._buffer[: newline_at + 1]) + del self._buffer[: newline_at + 1] + return line + if len(self._buffer) > self._max: + raise LinkError(f"frame too large: >{self._max} bytes") + if self._eof: + if self._buffer: + remainder = bytes(self._buffer) + self._buffer.clear() + return remainder + return b"" + chunk = self._sock.recv(65536) + if chunk: + self._buffer.extend(chunk) + else: + self._eof = True + + def close(self) -> None: + self._buffer.clear() + + +def read_message(reader: Any) -> Optional[Dict[str, Any]]: + line = reader.readline(MAX_FRAME_BYTES + 2) + if not line: + return None + if len(line) > MAX_FRAME_BYTES: + raise LinkError(f"frame too large: >{MAX_FRAME_BYTES} bytes") + if not line.endswith(b"\n"): + raise LinkError("truncated frame (no trailing newline)") + try: + message = json.loads(line) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise LinkError(f"invalid json frame: {exc}") from exc + if not isinstance(message, dict) or message.get("kind") not in {"req", "resp"}: + raise LinkError("invalid envelope") + return message + + +__all__ = [ + "ActionType", + "LineReader", + "LinkError", + "MAX_FRAME_BYTES", + "PROTOCOL_VERSION", + "RemoteError", + "encode_frame", + "new_request", + "new_response", + "read_message", + "send_message", +] diff --git a/unilabos/hostlink/ros_assist.py b/unilabos/hostlink/ros_assist.py new file mode 100644 index 000000000..44fe987d6 --- /dev/null +++ b/unilabos/hostlink/ros_assist.py @@ -0,0 +1,221 @@ +"""ROS2 networking settings exchanged during the HostLink handshake. + +HostLink is only the control channel. Device actions and node registration +remain on ROS2. The Slave applies the values below before ``rclpy.init`` so +both processes join the same ROS graph. +""" + +from __future__ import annotations + +import os +import socket +from dataclasses import asdict, dataclass, field +from typing import Any, Dict, List, MutableMapping, Optional, Tuple + +_VALID_DISCOVERY_RANGES = ("SYSTEM_DEFAULT", "SUBNET", "LOCALHOST", "OFF") + + +@dataclass +class RosNetworkInfo: + """Serializable ROS2 network policy advertised by the Host.""" + + domain_id: Optional[int] = None + automatic_discovery_range: str = "" + static_peers: List[str] = field(default_factory=list) + discovery_server: str = "" + discovery_server_disabled: bool = False + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + @classmethod + def from_dict(cls, data: Optional[Dict[str, Any]]) -> "RosNetworkInfo": + payload = data or {} + domain = payload.get("domain_id") + return cls( + domain_id=int(domain) if domain is not None else None, + automatic_discovery_range=str( + payload.get("automatic_discovery_range") or "" + ).upper(), + static_peers=[ + str(peer).strip() + for peer in payload.get("static_peers") or [] + if str(peer).strip() + ], + discovery_server=str(payload.get("discovery_server") or "").strip(), + discovery_server_disabled=bool( + payload.get("discovery_server_disabled", False) + ), + ) + + +def validate_domain_id(domain_id: Optional[int]) -> Optional[int]: + """Validate the portable ROS2 domain range.""" + + if domain_id is None: + return None + value = int(domain_id) + if not 0 <= value <= 232: + raise ValueError("ROS domain id must be between 0 and 232") + return value + + +def build_host_ros_info( + host_ip: str = "", + domain_id: Optional[int] = None, + discovery_range: str = "", + static_peers: Optional[List[str]] = None, + discovery_server: str = "", + discovery_server_disabled: bool = False, + environ: Optional[MutableMapping[str, str]] = None, +) -> RosNetworkInfo: + """Resolve explicit Host settings, then fall back to its environment.""" + + env = environ if environ is not None else os.environ + if domain_id is None: + raw_domain = env.get("ROS_DOMAIN_ID", "").strip() + domain_id = int(raw_domain) if raw_domain else None + domain_id = validate_domain_id(domain_id) + + discovery_range = ( + discovery_range + or env.get("ROS_AUTOMATIC_DISCOVERY_RANGE", "") + ).strip().upper() + if discovery_range and discovery_range not in _VALID_DISCOVERY_RANGES: + raise ValueError( + f"invalid ROS discovery range {discovery_range!r}; " + f"expected one of {_VALID_DISCOVERY_RANGES}" + ) + + if static_peers is None: + static_peers = [ + peer.strip() + for peer in env.get("ROS_STATIC_PEERS", "").split(";") + if peer.strip() + ] + if host_ip and host_ip not in static_peers: + static_peers.append(host_ip) + + if not discovery_server and not discovery_server_disabled: + discovery_server = env.get("ROS_DISCOVERY_SERVER", "").strip() + if discovery_server: + host, port = parse_host_port(discovery_server) + discovery_server = format_host_port(host, port) + + return RosNetworkInfo( + domain_id=domain_id, + automatic_discovery_range=discovery_range, + static_peers=list(dict.fromkeys(static_peers)), + discovery_server=discovery_server, + discovery_server_disabled=discovery_server_disabled, + ) + + +def apply_ros_network_env( + info: RosNetworkInfo, + environ: Optional[MutableMapping[str, str]] = None, +) -> Dict[str, str]: + """Apply Host networking settings before ``rclpy.init``.""" + + env = environ if environ is not None else os.environ + applied: Dict[str, str] = {} + domain_id = validate_domain_id(info.domain_id) + if domain_id is not None: + applied["ROS_DOMAIN_ID"] = str(domain_id) + if info.automatic_discovery_range: + discovery_range = info.automatic_discovery_range.upper() + if discovery_range not in _VALID_DISCOVERY_RANGES: + raise ValueError(f"invalid ROS discovery range {discovery_range!r}") + applied["ROS_AUTOMATIC_DISCOVERY_RANGE"] = discovery_range + if info.static_peers: + applied["ROS_STATIC_PEERS"] = ";".join(dict.fromkeys(info.static_peers)) + if info.discovery_server_disabled: + env.pop("ROS_DISCOVERY_SERVER", None) + elif info.discovery_server: + host, port = parse_host_port(info.discovery_server) + applied["ROS_DISCOVERY_SERVER"] = format_host_port(host, port) + env.update(applied) + return applied + + +def parse_host_port(endpoint: str) -> Tuple[str, int]: + """Parse ``host:port`` and ``[IPv6]:port`` endpoints.""" + + value = str(endpoint or "").strip() + if value.startswith("["): + close = value.find("]") + if close <= 1 or value[close + 1 : close + 2] != ":": + raise ValueError(f"invalid host:port endpoint: {endpoint!r}") + host, port_text = value[1:close], value[close + 2 :] + else: + host, separator, port_text = value.rpartition(":") + if not separator: + raise ValueError(f"invalid host:port endpoint: {endpoint!r}") + if not host or not port_text.isdigit(): + raise ValueError(f"invalid host:port endpoint: {endpoint!r}") + port = int(port_text) + if not 1 <= port <= 65535: + raise ValueError(f"port out of range in endpoint: {endpoint!r}") + return host, port + + +def parse_host_target(endpoint: str, default_port: int) -> Tuple[str, int]: + """Parse a HostNode address with an optional port.""" + + value = str(endpoint or "").strip() + if not value: + raise ValueError("HostNode address cannot be empty") + if value.startswith("["): + close = value.find("]") + if close <= 1: + raise ValueError(f"invalid HostNode address: {endpoint!r}") + host = value[1:close] + remainder = value[close + 1 :] + if not remainder: + return host, int(default_port) + if not remainder.startswith(":"): + raise ValueError(f"invalid HostNode address: {endpoint!r}") + return parse_host_port(value) + if value.count(":") == 1: + host, port_text = value.rsplit(":", 1) + if port_text.isdigit(): + return parse_host_port(value) + raise ValueError(f"invalid HostNode address: {endpoint!r}") + if value.count(":") > 1: + # Unbracketed IPv6 without a port. + return value, int(default_port) + return value, int(default_port) + + +def format_host_port(host: str, port: int) -> str: + clean_host = str(host or "").strip() + if not clean_host: + raise ValueError("host cannot be empty") + if not 1 <= int(port) <= 65535: + raise ValueError(f"port out of range: {port}") + if ":" in clean_host and not clean_host.startswith("["): + clean_host = f"[{clean_host}]" + return f"{clean_host}:{int(port)}" + + +def detect_local_ip(probe_addr: str = "8.8.8.8") -> str: + """Detect the preferred outbound IPv4 address without sending traffic.""" + + try: + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.connect((probe_addr, 80)) + return str(sock.getsockname()[0]) + except OSError: + return "" + + +__all__ = [ + "RosNetworkInfo", + "apply_ros_network_env", + "build_host_ros_info", + "detect_local_ip", + "format_host_port", + "parse_host_port", + "parse_host_target", + "validate_domain_id", +] diff --git a/unilabos/hostlink/runtime.py b/unilabos/hostlink/runtime.py new file mode 100644 index 000000000..2eae9c0a0 --- /dev/null +++ b/unilabos/hostlink/runtime.py @@ -0,0 +1,202 @@ +"""ROS-backend lifecycle wiring for HostLink. + +This module intentionally has no inventory, scheduler or local-backend imports. +Its only responsibilities are Slave/device presence and ROS2 network settings. +""" + +from __future__ import annotations + +import atexit +import threading +from typing import Any, Iterable, Optional, Tuple + +from unilabos.config.config import BasicConfig, HostLinkConfig +from unilabos.hostlink.client import ( + HostLinkClient, + get_hostlink_client, + set_hostlink_client, +) +from unilabos.hostlink.protocol import PROTOCOL_VERSION +from unilabos.hostlink.ros_assist import ( + RosNetworkInfo, + apply_ros_network_env, + build_host_ros_info, + detect_local_ip, + validate_domain_id, +) +from unilabos.hostlink.server import ( + HostLinkServer, + get_hostlink_server, + set_hostlink_server, +) +from unilabos.utils import logger + +_host_lock = threading.Lock() +_slave_lock = threading.Lock() +_cleanup_lock = threading.Lock() +_cleanup_registered = False + + +def _register_cleanup() -> None: + global _cleanup_registered + with _cleanup_lock: + if not _cleanup_registered: + atexit.register(shutdown_hostlink) + _cleanup_registered = True + + +def _configured_domain_id() -> Optional[int]: + raw = str(HostLinkConfig.ros_domain_id or "").strip() + return validate_domain_id(int(raw) if raw else None) + + +def _host_ros_info() -> RosNetworkInfo: + advertise_ip = str(HostLinkConfig.advertise_ip or "").strip() + if not advertise_ip and HostLinkConfig.bind not in {"", "0.0.0.0", "::"}: + advertise_ip = str(HostLinkConfig.bind) + advertise_ip = advertise_ip or detect_local_ip() or "127.0.0.1" + static_peers = [ + peer.strip() + for peer in str(HostLinkConfig.ros_static_peers or "").split(";") + if peer.strip() + ] + configured_server = str(HostLinkConfig.ros_discovery_server or "").strip() + server_disabled = configured_server.lower() in {"off", "none", "disabled"} + return build_host_ros_info( + host_ip=advertise_ip, + domain_id=_configured_domain_id(), + discovery_range=str(HostLinkConfig.ros_discovery_range or ""), + static_peers=static_peers or None, + discovery_server="" if server_disabled else configured_server, + discovery_server_disabled=server_disabled, + ) + + +def setup_hostlink_server() -> Optional[HostLinkServer]: + """Start the ROS Host listener and apply its own network policy.""" + + if not HostLinkConfig.enable: + return None + with _host_lock: + existing = get_hostlink_server() + if existing is not None: + return existing + ros_info = _host_ros_info() + applied = apply_ros_network_env(ros_info) + if applied: + logger.info(f"[HostLink] Host ROS2 network settings: {applied}") + server = HostLinkServer( + bind=str(HostLinkConfig.bind), + port=int(HostLinkConfig.port), + heartbeat_timeout=float(HostLinkConfig.heartbeat_timeout), + ) + server.hello_payload = { + "host_id": BasicConfig.machine_name, + "host_name": BasicConfig.machine_name, + "protocol_version": PROTOCOL_VERSION, + "ros": ros_info.to_dict(), + } + try: + server.start() + except Exception as exc: # noqa: BLE001 - retain legacy ROS discovery + logger.error(f"[HostLink] listener unavailable, use legacy ROS discovery: {exc}") + return None + set_hostlink_server(server) + _register_cleanup() + return server + + +def setup_hostlink_client( + device_ids: Optional[Iterable[str]] = None, + *, + wait_for_host: Optional[bool] = None, +) -> Tuple[Optional[HostLinkClient], Optional[int]]: + """Connect a Slave to its configured Host before ROS initialization.""" + + if not HostLinkConfig.enable or not str(HostLinkConfig.host or "").strip(): + return None, _configured_domain_id() + if wait_for_host is None: + wait_for_host = not BasicConfig.slave_no_host + with _slave_lock: + client = get_hostlink_client() + if client is None: + client = HostLinkClient( + str(HostLinkConfig.host), + int(HostLinkConfig.port), + machine_name=BasicConfig.machine_name, + device_ids=device_ids, + heartbeat_interval=float(HostLinkConfig.heartbeat_interval), + connect_timeout=float(HostLinkConfig.connect_timeout), + request_timeout=float(HostLinkConfig.request_timeout), + ) + set_hostlink_client(client) + _register_cleanup() + elif device_ids: + client.configure_device_ids(device_ids) + + if client.online: + connected = True + elif wait_for_host: + logger.info( + f"[HostLink] waiting for HostNode {HostLinkConfig.host}:" + f"{HostLinkConfig.port} before ROS2 initialization" + ) + connected = client.connect_blocking(timeout=None) + else: + client.start() + connected = client.online + + if not connected: + logger.warning( + f"[HostLink] HostNode {HostLinkConfig.host}:{HostLinkConfig.port} " + "is offline; continuing with local ROS2 settings" + ) + return client, _configured_domain_id() + + ros_info = client.hello_ros_info() + if not HostLinkConfig.ros_assist_apply: + return client, _configured_domain_id() + # The address used by the successful TCP connection is known reachable and + # should always be a static discovery peer, even on multi-NIC Hosts. + host = str(HostLinkConfig.host) + if host not in ros_info.static_peers: + ros_info.static_peers.append(host) + applied = apply_ros_network_env(ros_info) + if applied: + logger.info(f"[HostLink] applied Host ROS2 network settings: {applied}") + return client, ros_info.domain_id + + +def startup_device_ids(devices_config: Any) -> list[str]: + """Collect all device IDs from a Slave startup graph for Host discovery.""" + + result: list[str] = [] + for node in getattr(devices_config, "all_nodes", []) or []: + content = getattr(node, "res_content", None) + if getattr(content, "type", "") != "device": + continue + device_id = str(getattr(content, "id", "") or "").strip() + if device_id and device_id not in result: + result.append(device_id) + return result + + +def shutdown_hostlink() -> None: + with _slave_lock: + client = get_hostlink_client() + set_hostlink_client(None) + if client is not None: + client.close() + with _host_lock: + server = get_hostlink_server() + set_hostlink_server(None) + if server is not None: + server.stop() + + +__all__ = [ + "setup_hostlink_client", + "setup_hostlink_server", + "shutdown_hostlink", + "startup_device_ids", +] diff --git a/unilabos/hostlink/server.py b/unilabos/hostlink/server.py new file mode 100644 index 000000000..d54052a3b --- /dev/null +++ b/unilabos/hostlink/server.py @@ -0,0 +1,295 @@ +"""Host-side HostLink listener for Slave discovery and ROS2 settings.""" + +from __future__ import annotations + +import socket +import socketserver +import threading +import time +from typing import Any, Callable, Dict, List, Optional + +from unilabos.hostlink.protocol import ( + ActionType, + LineReader, + LinkError, + PROTOCOL_VERSION, + new_response, + read_message, + send_message, +) +from unilabos.utils import logger + +Handler = Callable[[Dict[str, Any], Dict[str, Any]], Dict[str, Any]] + + +class _LinkTCPServer(socketserver.ThreadingTCPServer): + allow_reuse_address = True + daemon_threads = True + + link: "HostLinkServer" + + +class _LinkRequestHandler(socketserver.BaseRequestHandler): + def handle(self) -> None: + link = self.server.link # type: ignore[attr-defined] + sock: socket.socket = self.request + sock.settimeout(link.socket_timeout) + reader = LineReader(sock) + peer_key = f"{self.client_address[0]}:{self.client_address[1]}" + try: + while not link.stopping.is_set(): + try: + message = read_message(reader) + except (socket.timeout, TimeoutError): + continue + except (LinkError, OSError) as exc: + logger.debug(f"[HostLink] close {peer_key}: {exc}") + break + if message is None: + break + if message.get("kind") != "req": + continue + response = link.dispatch(message, peer_key) + try: + send_message(sock, response) + except OSError: + break + finally: + reader.close() + link.mark_disconnected(peer_key) + + +class HostLinkServer: + """Track Slave/device presence and publish the Host ROS2 network policy.""" + + def __init__( + self, + bind: str = "0.0.0.0", + port: int = 7302, + heartbeat_timeout: float = 15.0, + socket_timeout: float = 1.0, + ) -> None: + self._bind = bind + self._port = int(port) + self.heartbeat_timeout = float(heartbeat_timeout) + self.socket_timeout = float(socket_timeout) + self.handlers: Dict[str, Handler] = {} + self.hello_payload: Dict[str, Any] = {} + self.stopping = threading.Event() + self._peers: Dict[str, Dict[str, Any]] = {} + self._connection_nodes: Dict[str, str] = {} + self._peers_lock = threading.Lock() + self._tcp: Optional[_LinkTCPServer] = None + self._thread: Optional[threading.Thread] = None + self.register_handler(ActionType.HELLO, self._handle_hello) + self.register_handler(ActionType.PING, self._handle_ping) + self.register_handler(ActionType.ROS_INFO, self._handle_ros_info) + + def start(self) -> "HostLinkServer": + if self._thread is not None and self._thread.is_alive(): + return self + self.stopping.clear() + self._tcp = _LinkTCPServer((self._bind, self._port), _LinkRequestHandler) + self._tcp.link = self + self._port = int(self._tcp.server_address[1]) + self._thread = threading.Thread( + target=self._tcp.serve_forever, + name="hostlink-server", + daemon=True, + ) + self._thread.start() + logger.info(f"[HostLink] server listening on {self._bind}:{self._port}") + return self + + def stop(self) -> None: + self.stopping.set() + tcp, self._tcp = self._tcp, None + if tcp is not None: + tcp.shutdown() + tcp.server_close() + if self._thread is not None and self._thread.is_alive(): + self._thread.join(timeout=3) + self._thread = None + + @property + def port(self) -> int: + return self._port + + def register_handler(self, action_type: str, handler: Handler) -> None: + self.handlers[action_type] = handler + + def dispatch(self, message: Dict[str, Any], peer_key: str) -> Dict[str, Any]: + request_id = str(message.get("id") or "") + if message.get("v") != PROTOCOL_VERSION: + return new_response( + request_id, + False, + error=f"unsupported protocol version: {message.get('v')!r}", + ) + action = str(message.get("action_type") or "") + handler = self.handlers.get(action) + if handler is None: + return new_response(request_id, False, error=f"unknown action: {action}") + raw_data = message.get("data") + data = raw_data if isinstance(raw_data, dict) else {} + try: + peer = self._touch_peer(peer_key, action, data) + return new_response(request_id, True, handler(data, peer)) + except Exception as exc: # noqa: BLE001 - protocol boundary + logger.warning(f"[HostLink] {action} from {peer_key} failed: {exc}") + return new_response(request_id, False, error=str(exc)) + + def _touch_peer( + self, + peer_key: str, + action: str, + data: Dict[str, Any], + ) -> Dict[str, Any]: + now = time.time() + with self._peers_lock: + known_node = self._connection_nodes.get(peer_key) + if action == ActionType.HELLO: + device_ids = sorted( + { + str(device_id).strip() + for device_id in data.get("device_ids") or [] + if str(device_id).strip() + } + ) + machine_name = str(data.get("machine_name") or "").strip() + node_id = str(data.get("node_id") or "").strip() + if device_ids: + # A reconnect keeps the logical peer keyed by its first + # globally unique device id even though the TCP port changes. + node_id = f"device:{device_ids[0]}" + node_id = node_id or machine_name or peer_key + if known_node and known_node != node_id: + temporary = self._peers.get(known_node) + if temporary and temporary.get("addr") == peer_key: + self._peers.pop(known_node, None) + self._connection_nodes[peer_key] = node_id + peer = self._peers.setdefault(node_id, {}) + if peer.get("addr") != peer_key: + peer["connected_at"] = now + peer.update( + { + "addr": peer_key, + "node_id": node_id, + "machine_name": machine_name, + "role": str(data.get("role") or "slave"), + "device_ids": device_ids, + "protocol_version": data.get("protocol_version"), + "capabilities": [ + str(item) + for item in data.get("capabilities") or [] + if str(item) + ], + } + ) + else: + node_id = known_node or peer_key + peer = self._peers.setdefault( + node_id, + { + "addr": peer_key, + "node_id": node_id, + "machine_name": "", + "role": "", + "device_ids": [], + "protocol_version": data.get("protocol_version"), + "capabilities": [], + "connected_at": now, + }, + ) + if known_node and peer.get("addr") != peer_key: + return dict(peer) + peer["last_seen"] = now + peer["connected"] = True + return dict(peer) + + def mark_disconnected(self, peer_key: str) -> None: + with self._peers_lock: + node_id = self._connection_nodes.pop(peer_key, peer_key) + peer = self._peers.get(node_id) + if peer is not None and peer.get("addr") == peer_key: + peer["connected"] = False + + def peers(self) -> List[Dict[str, Any]]: + """Return all known Slaves with a calculated ``online`` field.""" + + now = time.time() + with self._peers_lock: + result: List[Dict[str, Any]] = [] + for peer in self._peers.values(): + snapshot = dict(peer) + snapshot["online"] = bool( + snapshot.get("connected") + and now - float(snapshot.get("last_seen") or 0) + < self.heartbeat_timeout + ) + result.append(snapshot) + return result + + def devices(self, online_only: bool = True) -> Dict[str, Dict[str, Any]]: + """Map discovered device IDs to the Slave that advertised each one.""" + + result: Dict[str, Dict[str, Any]] = {} + for peer in self.peers(): + if online_only and not peer.get("online"): + continue + for device_id in peer.get("device_ids") or []: + result[str(device_id)] = dict(peer) + return result + + def has_device(self, device_id: str) -> bool: + return str(device_id) in self.devices(online_only=True) + + def _handle_hello( + self, + _data: Dict[str, Any], + peer: Dict[str, Any], + ) -> Dict[str, Any]: + return { + "server_time": time.time(), + "heartbeat_timeout": self.heartbeat_timeout, + **self.hello_payload, + "assigned_node_id": peer.get("node_id"), + "device_ids": list(peer.get("device_ids") or []), + } + + def _handle_ping( + self, + _data: Dict[str, Any], + _peer: Dict[str, Any], + ) -> Dict[str, Any]: + return {"pong": True, "server_time": time.time()} + + def _handle_ros_info( + self, + _data: Dict[str, Any], + _peer: Dict[str, Any], + ) -> Dict[str, Any]: + return {"ros": dict(self.hello_payload.get("ros") or {})} + + +_server_lock = threading.Lock() +_server: Optional[HostLinkServer] = None + + +def set_hostlink_server(server: Optional[HostLinkServer]) -> None: + global _server + with _server_lock: + _server = server + + +def get_hostlink_server() -> Optional[HostLinkServer]: + with _server_lock: + return _server + + +__all__ = [ + "Handler", + "HostLinkServer", + "get_hostlink_server", + "set_hostlink_server", +] diff --git a/unilabos/ros/main_slave_run.py b/unilabos/ros/main_slave_run.py index 7dca43e81..802933000 100644 --- a/unilabos/ros/main_slave_run.py +++ b/unilabos/ros/main_slave_run.py @@ -27,6 +27,23 @@ from unilabos.utils.type_check import TypeEncoder +def _init_rclpy(args: List[str], domain_id: Optional[int]) -> None: + """Initialize ROS with an explicit domain when supported by rclpy.""" + + if rclpy.ok(): + logger.info("[ROS] rclpy already initialized, reusing context") + return + if domain_id is not None: + # Also populate the environment for child processes and older rclpy + # versions that do not accept the domain_id keyword. + os.environ["ROS_DOMAIN_ID"] = str(domain_id) + try: + rclpy.init(args=args, domain_id=domain_id) + except TypeError: + # Older rclpy builds read ROS_DOMAIN_ID from the environment only. + rclpy.init(args=args) + + def exit() -> None: """关闭ROS节点和资源""" host_instance = HostNode.get_instance() @@ -40,6 +57,9 @@ def exit() -> None: if hasattr(device_node, "destroy_node"): device_node.ros_node_instance.destroy_node() host_instance.destroy_node() + from unilabos.hostlink.runtime import shutdown_hostlink + + shutdown_hostlink() rclpy.shutdown() @@ -57,11 +77,13 @@ def main( ) -> None: """主函数""" - # Support restart - check if rclpy is already initialized - if not rclpy.ok(): - rclpy.init(args=rclpy_init_args) - else: - logger.info("[ROS] rclpy already initialized, reusing context") + # HostLink must publish/apply the Host ROS2 policy before DDS is initialized. + from unilabos.hostlink.runtime import setup_hostlink_server + + setup_hostlink_server() + raw_domain_id = os.environ.get("ROS_DOMAIN_ID", "").strip() + domain_id = int(raw_domain_id) if raw_domain_id else None + _init_rclpy(rclpy_init_args, domain_id) executor = rclpy.__executor = MultiThreadedExecutor(num_threads=max(os.cpu_count() * 4, 48)) # 创建主机节点 host_node = HostNode( @@ -118,9 +140,14 @@ def slave( rclpy_init_args: List[str] = ["--log-level", "debug"], ) -> None: """从节点函数""" - # 1. 初始化 ROS2 - if not rclpy.ok(): - rclpy.init(args=rclpy_init_args) + # 1. Slave 先通过明确指定的 HostNode IP 获取 domain/discovery 信息, + # 再初始化 DDS;设备动作与注册流程仍继续走 ROS2。 + from unilabos.hostlink.runtime import setup_hostlink_client, startup_device_ids + + _hostlink_client, domain_id = setup_hostlink_client( + startup_device_ids(devices_config) + ) + _init_rclpy(rclpy_init_args, domain_id) executor = rclpy.__executor if not executor: executor = rclpy.__executor = MultiThreadedExecutor(num_threads=max(os.cpu_count() * 4, 48)) From c6ea0c47776ad764328210dab2b200ac8e41eaaa Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 10 Aug 2026 04:08:46 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(networking):=20=E7=BB=86=E5=8C=96=20Ho?= =?UTF-8?q?stLink=20=E4=B8=8E=20ROS2=20=E7=BB=84=E7=BD=91=E5=8F=82?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced_usage/configuration.md | 13 ++ docs/developer_guide/networking_overview.md | 39 +++++- docs/user_guide/launch.md | 24 ++++ tests/hostlink/test_cli.py | 65 +++++++++ unilabos/app/main.py | 148 ++++++++++++++++++-- unilabos/config/example_config.py | 8 ++ 6 files changed, 280 insertions(+), 17 deletions(-) diff --git a/docs/advanced_usage/configuration.md b/docs/advanced_usage/configuration.md index 71ffa1413..cfef331ef 100644 --- a/docs/advanced_usage/configuration.md +++ b/docs/advanced_usage/configuration.md @@ -188,7 +188,19 @@ Uni-Lab 允许通过命令行参数覆盖配置文件中的设置,提供更灵 | `BasicConfig` | `vis_2d_enable` | `--2d_vis` | 启用 2D 可视化 | | `HTTPConfig` | `remote_addr` | `--addr` | 远程服务地址 | | `HostLinkConfig` | `host` | `--host-node-ip` | Slave 连接的 HostNode IP/端口 | +| `HostLinkConfig` | `port` | `--hostlink-port` | HostLink TCP 端口,默认 7302 | +| `HostLinkConfig` | `bind` | `--hostlink-bind` | Host 监听地址 | +| `HostLinkConfig` | `advertise_ip` | `--hostlink-advertise-ip` | Host 多网卡发布地址 | +| `HostLinkConfig` | `enable` | `--disable-hostlink` | 禁用 HostLink | +| `HostLinkConfig` | `heartbeat_interval` | `--hostlink-heartbeat-interval` | Slave 心跳周期 | +| `HostLinkConfig` | `heartbeat_timeout` | `--hostlink-heartbeat-timeout` | Host 离线判定时间 | +| `HostLinkConfig` | `connect_timeout` | `--hostlink-connect-timeout` | TCP 连接/握手超时 | +| `HostLinkConfig` | `request_timeout` | `--hostlink-request-timeout` | 控制请求超时 | | `HostLinkConfig` | `ros_domain_id`| `--ros-domain-id` | Host 发布或 Slave 本地兜底 domain | +| `HostLinkConfig` | `ros_discovery_range` | `--ros-discovery-range` | ROS 自动发现范围 | +| `HostLinkConfig` | `ros_static_peers` | `--ros-static-peers` | 分号分隔的静态对端 | +| `HostLinkConfig` | `ros_discovery_server` | `--ros-discovery-server` | 外部 Fast DDS Server | +| `HostLinkConfig` | `ros_assist_apply` | `--no-ros-assist` | 不应用 Host 下发的 ROS 环境 | ### 特殊命令行参数 @@ -198,6 +210,7 @@ Uni-Lab 允许通过命令行参数覆盖配置文件中的设置,提供更灵 | ------------------- | ------------------------------------ | | `--config` | 指定配置文件路径 | | `--port` | Web 服务端口(不影响配置文件) | +| `--hostlink-port` | HostLink TCP 端口,与 Web `--port` 独立 | | `--disable_browser` | 禁用自动打开浏览器(不影响配置文件) | | `--visual` | 可视化工具选择(不影响配置文件) | | `--skip_env_check` | 跳过环境检查(不影响配置文件) | diff --git a/docs/developer_guide/networking_overview.md b/docs/developer_guide/networking_overview.md index 9cd06ad21..9a11e1c28 100644 --- a/docs/developer_guide/networking_overview.md +++ b/docs/developer_guide/networking_overview.md @@ -118,6 +118,41 @@ Host 运行 ROS backend 时会在 TCP `7302` 监听 HostLink。Slave 通过 HostLink 只辅助 ROS2 组网。设备 Action、节点注册和资源同步仍走现有 ROS2 接口;本阶段没有通过 HostLink 提供物料查询或无 ROS backend。 +#### 端口与前端归属 + +| 服务 | 默认地址 | 协议 | 使用者 | +|---|---|---|---| +| 主 Web/API | `0.0.0.0:8002` | HTTP/WebSocket over TCP | 状态页、主微前端、API 客户端 | +| HostLink | `0.0.0.0:7302` | NDJSON over raw TCP | Host/Slave 进程,不供浏览器访问 | +| F003 Local Bridge API | `127.0.0.1:8014` | HTTP | 仅完整集成分支中的本地工作流微前端 | + +因此微前端不访问 `7302`。接入主 OS API 的微前端跟随 `--port`,默认访问 +`8002`;F003 本地桥接微前端仍使用其独立的 `8014`。两个独立 TCP 服务不能绑定 +同一个 IP/端口。 + +#### HostLink 与 ROS2 参数 + +| 参数 | 作用域 | 默认值 | 说明 | +|---|---|---:|---| +| `--host-node-ip` | Slave | 空 | Host IP/主机名;兼容 `ip:port` | +| `--hostlink-port` | Host + Slave | `7302` | HostLink TCP 监听/连接端口;优先于 `--host-node-ip` 中的端口 | +| `--hostlink-bind` | Host | `0.0.0.0` | HostLink 监听网卡 | +| `--hostlink-advertise-ip` | Host | 自动探测 | 多网卡时发布给 Slave 的可达 IP | +| `--disable-hostlink` | Host + Slave | 否 | 禁用 HostLink,回退原 ROS2 发现 | +| `--hostlink-heartbeat-interval` | Slave | `5` 秒 | 心跳发送间隔 | +| `--hostlink-heartbeat-timeout` | Host | `15` 秒 | Slave 离线判定时间 | +| `--hostlink-connect-timeout` | Slave | `5` 秒 | 单次 TCP 连接和握手超时 | +| `--hostlink-request-timeout` | Slave | `10` 秒 | 控制请求超时 | +| `--ros-domain-id` | Host + Slave | 环境值 | Host 下发给 Slave;Slave 本地值仅作连接前兜底 | +| `--ros-discovery-range` | Host | 环境值 | `SYSTEM_DEFAULT/SUBNET/LOCALHOST/OFF` | +| `--ros-static-peers` | Host | 自动加入 Host IP | 分号分隔的静态发现对端 | +| `--ros-discovery-server` | Host | 环境值 | 外部 Fast DDS `host:port`;`off` 清除继承值 | +| `--no-ros-assist` | Slave | 否 | 保留 HostLink 心跳/设备发现,但不应用 Host ROS 参数 | + +本切片没有启动 Fast DDS Discovery Server 进程,因此没有 +`--ros-discovery-port`;该参数应与托管 Discovery Server 功能一并引入,不能成为 +无效果的占位参数。 + ### WebSocket 通信 **用途**: 主节点与云端通信 @@ -208,9 +243,9 @@ unilab --ak your_ak --sk your_sk -g host.json --ros-domain-id 42 ```bash unilab --ak your_ak --sk your_sk -g slave1.json \ - --is-slave --host-node-ip 192.168.1.10 + --is-slave --host-node-ip 192.168.1.10 --hostlink-port 7302 unilab --ak your_ak --sk your_sk -g slave2.json \ - --is-slave --host-node-ip 192.168.1.10 --port 8003 + --is-slave --host-node-ip 192.168.1.10 --hostlink-port 7302 --port 8003 ``` ### 云端集成模式 diff --git a/docs/user_guide/launch.md b/docs/user_guide/launch.md index 4f8df40db..5c4d90b69 100644 --- a/docs/user_guide/launch.md +++ b/docs/user_guide/launch.md @@ -155,6 +155,30 @@ unilab --config path/to/your/config.py 局域网内分别启动的 Uni-Lab 主站/从站将自动组网,互相能访问所有设备状态、传感器信息并发送指令。 +推荐由 Host 统一发布 ROS2 domain,Slave 只指定 Host IP: + +```bash +# Host:8002 是 Web/API,7302 是 HostLink TCP +unilab -g host.json --port 8002 \ + --hostlink-port 7302 --ros-domain-id 42 + +# Slave:--port 只影响本机 Web/API;HostLink 目标端口单独配置 +unilab -g slave.json --is-slave \ + --host-node-ip 192.168.1.10 --hostlink-port 7302 --port 8003 +``` + +主要组网参数: + +- `--host-node-ip`:Slave 指定 Host IP/主机名。 +- `--hostlink-port`:HostLink TCP 端口,默认 `7302`,与 Web `--port` 独立。 +- `--hostlink-bind` / `--hostlink-advertise-ip`:Host 监听地址与多网卡发布地址。 +- `--ros-domain-id`:Host 下发给 Slave 的 ROS2 domain。 +- `--ros-discovery-range` / `--ros-static-peers` / `--ros-discovery-server`:ROS2 发现策略。 +- `--no-ros-assist`:仅保留 HostLink 设备发现,不覆盖 Slave 的 ROS2 环境。 +- `--disable-hostlink`:完全关闭 HostLink,使用原 ROS2 发现流程。 + +浏览器和微前端访问 `--port`(默认 `8002`),不会访问 HostLink 的 `7302`。 + ## 可视化选项 ### 2D 可视化 diff --git a/tests/hostlink/test_cli.py b/tests/hostlink/test_cli.py index 83e710c64..c652c0cf5 100644 --- a/tests/hostlink/test_cli.py +++ b/tests/hostlink/test_cli.py @@ -10,16 +10,22 @@ def test_networking_cli_accepts_host_and_domain_aliases() -> None: "--is_slave", "--host-node-ip", "10.0.0.9:7402", + "--hostlink-port", + "7502", "--ros-domain-id", "52", "--ros-discovery-range", "OFF", + "--ros-static-peers", + "10.0.0.9;10.0.0.10", ] ) assert args.is_slave is True assert args.host_node_ip == "10.0.0.9:7402" + assert args.hostlink_port == 7502 assert args.ros_domain_id == 52 assert args.ros_discovery_range == "OFF" + assert args.ros_static_peers == "10.0.0.9;10.0.0.10" @pytest.mark.parametrize("domain_id", ["-1", "233"]) @@ -50,3 +56,62 @@ def test_networking_cli_is_applied_after_config(monkeypatch) -> None: assert HostLinkConfig.port == 7402 assert HostLinkConfig.ros_domain_id == "52" assert HostLinkConfig.ros_discovery_range == "OFF" + + +def test_detailed_hostlink_cli_overrides(monkeypatch) -> None: + monkeypatch.setattr(HostLinkConfig, "enable", True) + monkeypatch.setattr(HostLinkConfig, "host", "") + monkeypatch.setattr(HostLinkConfig, "port", 7302) + monkeypatch.setattr(HostLinkConfig, "bind", "0.0.0.0") + monkeypatch.setattr(HostLinkConfig, "advertise_ip", "") + monkeypatch.setattr(HostLinkConfig, "heartbeat_interval", 5.0) + monkeypatch.setattr(HostLinkConfig, "heartbeat_timeout", 15.0) + monkeypatch.setattr(HostLinkConfig, "connect_timeout", 5.0) + monkeypatch.setattr(HostLinkConfig, "request_timeout", 10.0) + monkeypatch.setattr(HostLinkConfig, "ros_static_peers", "") + monkeypatch.setattr(HostLinkConfig, "ros_assist_apply", True) + + _apply_hostlink_cli( + { + "host_node_ip": "10.0.0.9:7402", + "hostlink_port": 7502, + "hostlink_bind": "127.0.0.1", + "hostlink_advertise_ip": "10.0.0.8", + "disable_hostlink": True, + "hostlink_heartbeat_interval": 2.5, + "hostlink_heartbeat_timeout": 8, + "hostlink_connect_timeout": 3, + "hostlink_request_timeout": 6, + "ros_domain_id": None, + "ros_discovery_range": None, + "ros_static_peers": "10.0.0.8;10.0.0.9", + "ros_discovery_server": None, + "no_ros_assist": True, + }, + is_slave=True, + ) + + assert HostLinkConfig.host == "10.0.0.9" + assert HostLinkConfig.port == 7502 # 显式端口覆盖 host-node-ip 中的兼容端口 + assert HostLinkConfig.bind == "127.0.0.1" + assert HostLinkConfig.advertise_ip == "10.0.0.8" + assert HostLinkConfig.enable is False + assert HostLinkConfig.heartbeat_interval == 2.5 + assert HostLinkConfig.heartbeat_timeout == 8.0 + assert HostLinkConfig.connect_timeout == 3.0 + assert HostLinkConfig.request_timeout == 6.0 + assert HostLinkConfig.ros_static_peers == "10.0.0.8;10.0.0.9" + assert HostLinkConfig.ros_assist_apply is False + + +@pytest.mark.parametrize( + "overrides", + [ + {"hostlink_port": 0}, + {"hostlink_port": 65536}, + {"hostlink_connect_timeout": 0}, + ], +) +def test_invalid_hostlink_cli_values_are_rejected(overrides) -> None: + with pytest.raises(ValueError): + _apply_hostlink_cli(overrides, is_slave=False) diff --git a/unilabos/app/main.py b/unilabos/app/main.py index b587c70de..790ff4640 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -139,7 +139,7 @@ def load_config_from_file(config_path): def _apply_hostlink_cli(args_dict: Dict[str, Any], *, is_slave: bool) -> None: - """Apply ROS HostLink CLI overrides after config/env loading.""" + """在配置文件和环境变量之后应用 HostLink 命令行覆盖。""" from unilabos.config.config import HostLinkConfig from unilabos.hostlink.ros_assist import parse_host_target, validate_domain_id @@ -150,13 +150,50 @@ def _apply_hostlink_cli(args_dict: Dict[str, Any], *, is_slave: bool) -> None: HostLinkConfig.port = hostlink_port if is_slave: HostLinkConfig.host = host - print_status( - f"Slave HostNode: {HostLinkConfig.host}:{HostLinkConfig.port}", - "info", - ) else: HostLinkConfig.advertise_ip = host + explicit_port = args_dict.get("hostlink_port") + if explicit_port is not None: + if not 1 <= int(explicit_port) <= 65535: + raise ValueError("--hostlink-port must be between 1 and 65535") + HostLinkConfig.port = int(explicit_port) + + hostlink_bind = args_dict.get("hostlink_bind") + if hostlink_bind is not None: + HostLinkConfig.bind = str(hostlink_bind).strip() + if not HostLinkConfig.bind: + raise ValueError("--hostlink-bind cannot be empty") + + advertise_ip = args_dict.get("hostlink_advertise_ip") + if advertise_ip is not None: + HostLinkConfig.advertise_ip = str(advertise_ip).strip() + if not HostLinkConfig.advertise_ip: + raise ValueError("--hostlink-advertise-ip cannot be empty") + + if args_dict.get("disable_hostlink", False): + HostLinkConfig.enable = False + + if is_slave and HostLinkConfig.host: + print_status( + f"Slave HostNode: {HostLinkConfig.host}:{HostLinkConfig.port}", + "info", + ) + + timeout_fields = { + "hostlink_heartbeat_interval": "heartbeat_interval", + "hostlink_heartbeat_timeout": "heartbeat_timeout", + "hostlink_connect_timeout": "connect_timeout", + "hostlink_request_timeout": "request_timeout", + } + for argument, field in timeout_fields.items(): + value = args_dict.get(argument) + if value is None: + continue + if float(value) <= 0: + raise ValueError(f"--{argument.replace('_', '-')} must be greater than 0") + setattr(HostLinkConfig, field, float(value)) + ros_domain_id = validate_domain_id(args_dict.get("ros_domain_id")) if ros_domain_id is not None: HostLinkConfig.ros_domain_id = str(ros_domain_id) @@ -164,10 +201,14 @@ def _apply_hostlink_cli(args_dict: Dict[str, Any], *, is_slave: bool) -> None: print_status(f"ROS_DOMAIN_ID = {ros_domain_id}", "info") if args_dict.get("ros_discovery_range") is not None: HostLinkConfig.ros_discovery_range = args_dict["ros_discovery_range"] + if args_dict.get("ros_static_peers") is not None: + HostLinkConfig.ros_static_peers = str(args_dict["ros_static_peers"]).strip() if args_dict.get("ros_discovery_server") is not None: HostLinkConfig.ros_discovery_server = str( args_dict["ros_discovery_server"] ).strip() + if args_dict.get("no_ros_assist", False): + HostLinkConfig.ros_assist_apply = False def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser): @@ -231,10 +272,71 @@ def parse_args(): dest="host_node_ip", default="", help=( - "HostNode address for a Slave, as ip/hostname or ip:port. " - "The HostLink control channel synchronizes ROS2 networking and device IDs." + "Slave 连接的 HostNode IP/主机名,可兼容写成 ip:port;" + "建议端口单独使用 --hostlink-port。" ), ) + parser.add_argument( + "--hostlink_port", + "--hostlink-port", + dest="hostlink_port", + type=int, + default=None, + help="HostLink TCP 端口;Host 监听、Slave 连接,默认 7302。", + ) + parser.add_argument( + "--hostlink_bind", + "--hostlink-bind", + dest="hostlink_bind", + default=None, + help="HostLink 在 Host 上的监听地址,默认 0.0.0.0;Slave 忽略。", + ) + parser.add_argument( + "--hostlink_advertise_ip", + "--hostlink-advertise-ip", + dest="hostlink_advertise_ip", + default=None, + help="Host 向 Slave 发布的可达 IP;多网卡环境建议显式指定。", + ) + parser.add_argument( + "--disable_hostlink", + "--disable-hostlink", + dest="disable_hostlink", + action="store_true", + help="关闭 HostLink;ROS2 使用原有发现和注册流程。", + ) + parser.add_argument( + "--hostlink_heartbeat_interval", + "--hostlink-heartbeat-interval", + dest="hostlink_heartbeat_interval", + type=float, + default=None, + help="Slave 心跳发送间隔(秒),默认 5。", + ) + parser.add_argument( + "--hostlink_heartbeat_timeout", + "--hostlink-heartbeat-timeout", + dest="hostlink_heartbeat_timeout", + type=float, + default=None, + help="Host 判定 Slave 离线的心跳超时(秒),默认 15。", + ) + parser.add_argument( + "--hostlink_connect_timeout", + "--hostlink-connect-timeout", + dest="hostlink_connect_timeout", + type=float, + default=None, + help="单次 HostLink TCP 连接/握手超时(秒),默认 5。", + ) + parser.add_argument( + "--hostlink_request_timeout", + "--hostlink-request-timeout", + dest="hostlink_request_timeout", + type=float, + default=None, + help="HostLink 控制请求超时(秒),默认 10。", + ) parser.add_argument( "--ros_domain_id", "--ros-domain-id", @@ -242,8 +344,7 @@ def parse_args(): type=int, default=None, help=( - "ROS2 domain id (0-232). A Host advertises it to Slaves; " - "a Slave uses it as fallback until HostLink connects." + "ROS2 domain id(0-232);Host 下发给 Slave,Slave 本地值仅作连接前兜底。" ), ) parser.add_argument( @@ -252,7 +353,14 @@ def parse_args(): dest="ros_discovery_range", choices=["SYSTEM_DEFAULT", "SUBNET", "LOCALHOST", "OFF"], default=None, - help="ROS_AUTOMATIC_DISCOVERY_RANGE advertised by the Host.", + help="Host 下发的 ROS_AUTOMATIC_DISCOVERY_RANGE。", + ) + parser.add_argument( + "--ros_static_peers", + "--ros-static-peers", + dest="ros_static_peers", + default=None, + help="Host 发布的 ROS_STATIC_PEERS,多个地址用分号分隔。", ) parser.add_argument( "--ros_discovery_server", @@ -260,16 +368,23 @@ def parse_args(): dest="ros_discovery_server", default=None, help=( - "External Fast DDS discovery server host:port; use 'off' to clear it. " - "This slice does not start a discovery-server process." + "外部 Fast DDS Discovery Server 的 host:port;off 表示清除。" + "本切片不会自动启动 Discovery Server 进程。" ), ) + parser.add_argument( + "--no_ros_assist", + "--no-ros-assist", + dest="no_ros_assist", + action="store_true", + help="保留 HostLink 设备发现和心跳,但不应用 Host 下发的 ROS2 环境。", + ) parser.add_argument( "--slave_no_host", action="store_true", help=( - "Allow a Slave to start while HostLink/Host ROS services are offline; " - "the control channel keeps reconnecting in the background." + "允许 Slave 在 HostLink/Host ROS 服务离线时启动;" + "控制通道仍会在后台重连。" ), ) parser.add_argument( @@ -292,7 +407,10 @@ def parse_args(): "--port", type=int, default=None, - help="Port for web service information page", + help=( + "主 HTTP/Web API 端口,状态页和同源微前端使用,默认 8002;" + "不影响 HostLink TCP 端口。" + ), ) parser.add_argument( "--disable_browser", diff --git a/unilabos/config/example_config.py b/unilabos/config/example_config.py index 5e551c8bf..270f18ba7 100644 --- a/unilabos/config/example_config.py +++ b/unilabos/config/example_config.py @@ -19,4 +19,12 @@ class HostLinkConfig: port = 7302 bind = "0.0.0.0" advertise_ip = "" # Host 多网卡时填写 Slave 可达 IP + heartbeat_interval = 5.0 + heartbeat_timeout = 15.0 + connect_timeout = 5.0 + request_timeout = 10.0 + ros_assist_apply = True ros_domain_id = "" # Host 可在此统一配置,也可用 --ros-domain-id + ros_discovery_range = "" + ros_static_peers = "" + ros_discovery_server = "" # 外部 Fast DDS host:port;off 表示禁用 From 08ab6d703fd74f6ceb7c54e94b19a6d2323bb76b Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 10 Aug 2026 04:12:55 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(web):=20=E6=98=8E=E7=A1=AE=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=AB=AF=E5=8F=A3=E4=B8=8E=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced_usage/configuration.md | 24 +++++++++++++------- docs/developer_guide/networking_overview.md | 21 ++++++++--------- docs/user_guide/best_practice.md | 6 ++--- docs/user_guide/launch.md | 25 ++++++++++++--------- tests/hostlink/test_cli.py | 23 +++++++++++++++++++ unilabos/app/main.py | 24 +++++++++++++++----- unilabos/config/config.py | 4 ++-- unilabos/config/example_config.py | 2 ++ 8 files changed, 90 insertions(+), 39 deletions(-) diff --git a/docs/advanced_usage/configuration.md b/docs/advanced_usage/configuration.md index cfef331ef..465175910 100644 --- a/docs/advanced_usage/configuration.md +++ b/docs/advanced_usage/configuration.md @@ -28,6 +28,8 @@ Uni-Lab 使用 Python 格式的配置文件(`.py`),默认为 `unilabos_dat class BasicConfig: ak = "" # 实验室网页给您提供的ak代码 sk = "" # 实验室网页给您提供的sk代码 + port = 8002 # 管理端 HTTP/Web API 与主微前端端口 + disable_browser = False # 只禁止自动打开浏览器 # WebSocket配置,一般无需调整 @@ -60,6 +62,8 @@ class BasicConfig: enable_resource_load = True # 是否启用资源加载 communication_protocol = "websocket" # 通信协议 log_level = "DEBUG" # 日志级别:TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL + port = 8002 # 管理端 HTTP/Web API 与主微前端端口 + disable_browser = False # 只禁止自动打开浏览器,不停止管理端服务 # WebSocket配置 class WSConfig: @@ -206,14 +210,18 @@ Uni-Lab 允许通过命令行参数覆盖配置文件中的设置,提供更灵 除了直接覆盖配置项的参数外,还有一些特殊的命令行参数: -| 参数 | 说明 | -| ------------------- | ------------------------------------ | -| `--config` | 指定配置文件路径 | -| `--port` | Web 服务端口(不影响配置文件) | -| `--hostlink-port` | HostLink TCP 端口,与 Web `--port` 独立 | -| `--disable_browser` | 禁用自动打开浏览器(不影响配置文件) | -| `--visual` | 可视化工具选择(不影响配置文件) | -| `--skip_env_check` | 跳过环境检查(不影响配置文件) | +| 参数 | 说明 | +| --- | --- | +| `--config` | 指定配置文件路径 | +| `--port-management` / `--port_management` | 管理端 HTTP/Web API 与主微前端端口,默认 `8002`;`--port` 是兼容缩写 | +| `--hostlink-port` | HostLink TCP 端口,默认 `7302`,与管理端口独立 | +| `--disable-browser` / `--disable_browser` | 只禁用启动时自动打开浏览器,管理端 HTTP/Web 服务仍会启动 | +| `--visual` | 可视化工具选择(不影响配置文件) | +| `--skip_env_check` | 跳过环境检查(不影响配置文件) | + +`--port-management` 最终覆盖 `BasicConfig.port`。主前端和 API 客户端都连接该 +端口;即使设置 `--disable-browser`,该端口仍会监听。浏览器参数不影响 +HostLink 的 `7302/TCP`。 ### 命令行覆盖使用示例 diff --git a/docs/developer_guide/networking_overview.md b/docs/developer_guide/networking_overview.md index 9a11e1c28..bdb2ce943 100644 --- a/docs/developer_guide/networking_overview.md +++ b/docs/developer_guide/networking_overview.md @@ -122,13 +122,14 @@ HostLink 只辅助 ROS2 组网。设备 Action、节点注册和资源同步仍 | 服务 | 默认地址 | 协议 | 使用者 | |---|---|---|---| -| 主 Web/API | `0.0.0.0:8002` | HTTP/WebSocket over TCP | 状态页、主微前端、API 客户端 | +| 主 Web/API | `0.0.0.0:8002` | HTTP/WebSocket over TCP | 状态页、主微前端、API 客户端;由 `--port-management` 配置 | | HostLink | `0.0.0.0:7302` | NDJSON over raw TCP | Host/Slave 进程,不供浏览器访问 | | F003 Local Bridge API | `127.0.0.1:8014` | HTTP | 仅完整集成分支中的本地工作流微前端 | -因此微前端不访问 `7302`。接入主 OS API 的微前端跟随 `--port`,默认访问 -`8002`;F003 本地桥接微前端仍使用其独立的 `8014`。两个独立 TCP 服务不能绑定 -同一个 IP/端口。 +因此微前端不访问 `7302`。接入主 OS API 的微前端跟随 +`--port-management`(`--port` 为兼容缩写),默认访问 `8002`;F003 本地桥接 +微前端仍使用其独立的 `8014`。`--disable-browser` 只禁止自动打开页面,不会停止 +`8002` 的 HTTP/Web 服务。两个独立 TCP 服务不能绑定同一个 IP/端口。 #### HostLink 与 ROS2 参数 @@ -245,7 +246,7 @@ unilab --ak your_ak --sk your_sk -g host.json --ros-domain-id 42 unilab --ak your_ak --sk your_sk -g slave1.json \ --is-slave --host-node-ip 192.168.1.10 --hostlink-port 7302 unilab --ak your_ak --sk your_sk -g slave2.json \ - --is-slave --host-node-ip 192.168.1.10 --hostlink-port 7302 --port 8003 + --is-slave --host-node-ip 192.168.1.10 --hostlink-port 7302 --port-management 8003 ``` ### 云端集成模式 @@ -304,7 +305,7 @@ unilab --ak your_ak --sk your_sk -g host.json unilab --ak your_ak --sk your_sk -g host.json --upload_registry # 指定端口 -unilab --ak your_ak --sk your_sk -g host.json --port 8002 +unilab --ak your_ak --sk your_sk -g host.json --port-management 8002 ``` #### 3. 验证主节点 @@ -351,7 +352,7 @@ ros2 service list | grep host_node unilab --ak your_ak --sk your_sk -g slave1.json --is_slave # 指定不同端口(如果多个从节点在同一台机器) -unilab --ak your_ak --sk your_sk -g slave1.json --is_slave --port 8003 +unilab --ak your_ak --sk your_sk -g slave1.json --is_slave --port-management 8003 # 跳过等待主节点(独立测试) unilab --ak your_ak --sk your_sk -g slave1.json --is_slave --slave_no_host @@ -538,21 +539,21 @@ curl https://leap-lab.bohrium.com/api/v1/health ```bash # host.json -unilab --ak your_ak --sk your_sk -g host.json --port 8002 +unilab --ak your_ak --sk your_sk -g host.json --port-management 8002 ``` ### 房间 B - 从节点 1 ```bash # liquid_handler.json -unilab --ak your_ak --sk your_sk -g liquid_handler.json --is_slave --port 8003 +unilab --ak your_ak --sk your_sk -g liquid_handler.json --is_slave --port-management 8003 ``` ### 房间 C - 从节点 2 ```bash # analytical.json -unilab --ak your_ak --sk your_sk -g analytical.json --is_slave --port 8004 +unilab --ak your_ak --sk your_sk -g analytical.json --is_slave --port-management 8004 ``` --- diff --git a/docs/user_guide/best_practice.md b/docs/user_guide/best_practice.md index 4ca4298e8..eba73a5e2 100644 --- a/docs/user_guide/best_practice.md +++ b/docs/user_guide/best_practice.md @@ -203,8 +203,8 @@ unilab --ak your_ak --sk your_sk -g example_empty.json # 禁用自动打开浏览器 unilab --ak your_ak --sk your_sk -g graph.json --disable_browser -# 使用不同端口 -unilab --ak your_ak --sk your_sk -g graph.json --port 8080 +# 使用不同的管理 Web/API 端口 +unilab --ak your_ak --sk your_sk -g graph.json --port-management 8080 # 测试环境 unilab --addr test --ak your_ak --sk your_sk -g graph.json @@ -1631,7 +1631,7 @@ python -m unilabos.app.main \ --devices <克隆目录下的设备包目录> \ --external_devices_only \ --ak your_ak --sk your_sk --addr test --upload_registry \ - --disable_browser --port 8100 \ + --disable_browser --port-management 8100 \ -g <仓库内提供的图文件> ``` diff --git a/docs/user_guide/launch.md b/docs/user_guide/launch.md index 5c4d90b69..cf3b9c422 100644 --- a/docs/user_guide/launch.md +++ b/docs/user_guide/launch.md @@ -23,8 +23,10 @@ options: --slave_no_host Skip waiting for host service in slave mode --upload_registry Upload registry information when starting unilab --config CONFIG Configuration file path, supports .py format Python config files - --port PORT Port for web service information page - --disable_browser Disable opening information page on startup + --port_management PORT_MANAGEMENT, --port-management PORT_MANAGEMENT, --port PORT_MANAGEMENT + 管理端 HTTP/Web API 与主微前端端口,默认 8002 + --disable_browser, --disable-browser + 只禁止自动打开浏览器,管理端服务仍会启动 --2d_vis Enable 2D visualization when starting pylabrobot instance --visual {rviz,web,disable} Choose visualization tool: rviz, web, or disable @@ -158,26 +160,29 @@ unilab --config path/to/your/config.py 推荐由 Host 统一发布 ROS2 domain,Slave 只指定 Host IP: ```bash -# Host:8002 是 Web/API,7302 是 HostLink TCP -unilab -g host.json --port 8002 \ +# Host:8002 是管理 Web/API 和主微前端端口,7302 是 HostLink TCP +unilab -g host.json --port-management 8002 \ --hostlink-port 7302 --ros-domain-id 42 -# Slave:--port 只影响本机 Web/API;HostLink 目标端口单独配置 +# Slave:管理端口只影响本机 Web/API;HostLink 目标端口单独配置 unilab -g slave.json --is-slave \ - --host-node-ip 192.168.1.10 --hostlink-port 7302 --port 8003 + --host-node-ip 192.168.1.10 --hostlink-port 7302 --port-management 8003 ``` 主要组网参数: - `--host-node-ip`:Slave 指定 Host IP/主机名。 -- `--hostlink-port`:HostLink TCP 端口,默认 `7302`,与 Web `--port` 独立。 +- `--port-management` / `--port_management`:管理端 HTTP/Web API 和主微前端端口,默认 `8002`;`--port` 是兼容缩写。 +- `--disable-browser` / `--disable_browser`:只禁止启动时自动打开浏览器,不会停止管理端口。 +- `--hostlink-port`:HostLink TCP 端口,默认 `7302`,与管理端口独立。 - `--hostlink-bind` / `--hostlink-advertise-ip`:Host 监听地址与多网卡发布地址。 - `--ros-domain-id`:Host 下发给 Slave 的 ROS2 domain。 - `--ros-discovery-range` / `--ros-static-peers` / `--ros-discovery-server`:ROS2 发现策略。 - `--no-ros-assist`:仅保留 HostLink 设备发现,不覆盖 Slave 的 ROS2 环境。 - `--disable-hostlink`:完全关闭 HostLink,使用原 ROS2 发现流程。 -浏览器和微前端访问 `--port`(默认 `8002`),不会访问 HostLink 的 `7302`。 +浏览器和主微前端访问管理端口(默认 `8002`),不会访问 HostLink 的 `7302`。 +即使使用 `--disable-browser`,前端仍可手动访问 `http://<节点 IP>:8002`。 ## 可视化选项 @@ -229,8 +234,8 @@ unilab --ak your_ak --sk your_sk --is_slave # 启用可视化 unilab --ak your_ak --sk your_sk --visual web --2d_vis -# 指定本地信息网页服务端口和禁用自动跳出浏览器 -unilab --ak your_ak --sk your_sk --port 8080 --disable_browser +# 指定管理端口并禁止自动打开浏览器;HTTP/Web 服务仍在 8080 启动 +unilab --ak your_ak --sk your_sk --port-management 8080 --disable-browser ``` ## 常见问题 diff --git a/tests/hostlink/test_cli.py b/tests/hostlink/test_cli.py index c652c0cf5..1190fcad8 100644 --- a/tests/hostlink/test_cli.py +++ b/tests/hostlink/test_cli.py @@ -4,6 +4,29 @@ from unilabos.config.config import HostLinkConfig +@pytest.mark.parametrize( + ("option", "value"), + [ + ("--port-management", 8100), + ("--port_management", 8200), + ("--port", 8300), + ], +) +def test_management_port_accepts_semantic_name_and_short_alias(option, value) -> None: + args = parse_args().parse_args([option, str(value)]) + + assert args.port_management == value + + +def test_disable_browser_can_be_combined_with_management_port() -> None: + args = parse_args().parse_args( + ["--port-management", "8100", "--disable-browser"] + ) + + assert args.port_management == 8100 + assert args.disable_browser is True + + def test_networking_cli_accepts_host_and_domain_aliases() -> None: args = parse_args().parse_args( [ diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 790ff4640..857c8894b 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -404,18 +404,26 @@ def parse_args(): help="Configuration file path, supports .py format Python config files", ) parser.add_argument( + "--port_management", + "--port-management", "--port", + dest="port_management", type=int, default=None, help=( - "主 HTTP/Web API 端口,状态页和同源微前端使用,默认 8002;" - "不影响 HostLink TCP 端口。" + "管理端 HTTP/Web API 端口,状态页和主微前端使用,默认 8002;" + "--port 是兼容缩写,不影响 HostLink TCP 端口。" ), ) parser.add_argument( "--disable_browser", + "--disable-browser", + dest="disable_browser", action="store_true", - help="Disable opening information page on startup", + help=( + "仅禁止启动时自动打开浏览器;管理端 HTTP/Web 服务仍会在 " + "--port-management 指定的端口启动。" + ), ) parser.add_argument( "--2d_vis", @@ -949,7 +957,11 @@ def main(): else: print_status("远程资源不存在,本地将进行首次上报!", "info") - BasicConfig.port = args_dict["port"] if args_dict["port"] else BasicConfig.port + BasicConfig.port = ( + args_dict["port_management"] + if args_dict["port_management"] is not None + else BasicConfig.port + ) BasicConfig.disable_browser = args_dict["disable_browser"] or BasicConfig.disable_browser BasicConfig.is_host_mode = not is_slave BasicConfig.slave_no_host = args_dict.get("slave_no_host", False) @@ -1235,7 +1247,7 @@ def _exit(signum, frame): else: start_backend(**args_dict) restart_requested = start_server( - open_browser=not args_dict["disable_browser"], + open_browser=not BasicConfig.disable_browser, port=BasicConfig.port, ) if restart_requested: @@ -1247,7 +1259,7 @@ def _exit(signum, frame): # 启动服务器(默认支持WebSocket触发重启) restart_requested = start_server( - open_browser=not args_dict["disable_browser"], + open_browser=not BasicConfig.disable_browser, port=BasicConfig.port, ) if restart_requested: diff --git a/unilabos/config/config.py b/unilabos/config/config.py index 7dfd28d26..56dc8545e 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -20,8 +20,8 @@ class BasicConfig: enable_resource_load = True communication_protocol = "websocket" startup_json_path = None # 填写绝对路径 - disable_browser = False # 禁止浏览器自动打开 - port = 8002 # 本地HTTP服务 + disable_browser = False # 只禁止浏览器自动打开,不停止管理端服务 + port = 8002 # 管理端 HTTP/Web API 与主微前端服务 check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性 test_mode = False # 测试模式,所有动作不实际执行,返回模拟结果 extra_resource = False # 是否加载lab_开头的额外资源 diff --git a/unilabos/config/example_config.py b/unilabos/config/example_config.py index 270f18ba7..c15da3889 100644 --- a/unilabos/config/example_config.py +++ b/unilabos/config/example_config.py @@ -3,6 +3,8 @@ class BasicConfig: ak = "" # 实验室网页给您提供的ak代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --ak 传入,优先按照传入参数解析 sk = "" # 实验室网页给您提供的sk代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --sk 传入,优先按照传入参数解析 + port = 8002 # 管理端 HTTP/Web API 与主微前端端口 + disable_browser = False # 只禁止自动打开浏览器,不停止管理端服务 # WebSocket配置,一般无需调整 From 5b6bb1fe459256e2fd28c1663f68c180a7d38c36 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 10 Aug 2026 04:14:14 +0800 Subject: [PATCH 4/6] =?UTF-8?q?ci:=20=E5=A2=9E=E5=8A=A0=20HostLink=20?= =?UTF-8?q?=E7=BB=84=E7=BD=91=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-check.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 5773da553..83c353f0e 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -51,8 +51,15 @@ jobs: uv pip install -r unilabos/utils/requirements.txt uv pip install pywinauto git+https://github.com/Xuwznln/pylabrobot.git uv pip uninstall enum34 || echo enum34 not installed, skipping + uv pip install pytest uv pip install . + - name: Run HostLink networking tests + run: | + call conda activate check-env + echo Running HostLink, ROS2 domain and networking runtime tests... + python -m pytest -q tests\hostlink tests\networking tests\ros\test_domain_init.py + - name: Run check mode (AST registry validation) # check_mode 会真实 import 所有设备类(连带 matplotlib/opencv 等原生库)。 # Windows runner 上偶发原生崩溃(退出码 -1073741819 = 0xC0000005 访问违例), From 941fa214efccb57231d8237d550d801164802c41 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 10 Aug 2026 04:28:23 +0800 Subject: [PATCH 5/6] =?UTF-8?q?ci:=20=E7=A6=81=E7=94=A8=E4=B8=8D=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E7=9A=84=20ROS=20launch=20pytest=20=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 83c353f0e..823ed9803 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -58,7 +58,7 @@ jobs: run: | call conda activate check-env echo Running HostLink, ROS2 domain and networking runtime tests... - python -m pytest -q tests\hostlink tests\networking tests\ros\test_domain_init.py + python -m pytest -q tests\hostlink tests\networking tests\ros\test_domain_init.py -p no:launch_testing -p no:launch_ros - name: Run check mode (AST registry validation) # check_mode 会真实 import 所有设备类(连带 matplotlib/opencv 等原生库)。 From 44220681149b89b59afb67092f8f73a2661a7671 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 10 Aug 2026 04:38:11 +0800 Subject: [PATCH 6/6] =?UTF-8?q?chore(registry):=20=E8=A1=A5=E5=85=A8=20dis?= =?UTF-8?q?playname=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../device_comms/communication_devices.yaml | 2 ++ unilabos/registry/devices/Qone_nmr.yaml | 2 ++ unilabos/registry/devices/bioyond_cell.yaml | 2 ++ .../devices/bioyond_dispensing_station.yaml | 2 ++ unilabos/registry/devices/cameraSII.yaml | 2 ++ .../devices/characterization_chromatic.yaml | 4 +++ .../devices/characterization_optic.yaml | 2 ++ unilabos/registry/devices/chinwe.yaml | 2 ++ .../devices/coin_cell_workstation.yaml | 2 ++ unilabos/registry/devices/gas_handler.yaml | 4 +++ unilabos/registry/devices/hotel.yaml | 2 ++ .../registry/devices/laiyu_liquid_test.yaml | 2 ++ unilabos/registry/devices/liquid_handler.yaml | 10 +++++++ .../devices/neware_battery_test_system.yaml | 2 ++ unilabos/registry/devices/opcua_example.yaml | 2 ++ unilabos/registry/devices/opsky_ATR30007.yaml | 2 ++ .../devices/organic_miscellaneous.yaml | 4 +++ .../devices/post_process_station.yaml | 2 ++ unilabos/registry/devices/pump_and_valve.yaml | 8 ++++++ .../devices/reaction_station_bioyond.yaml | 4 +++ unilabos/registry/devices/robot_agv.yaml | 2 ++ unilabos/registry/devices/robot_arm.yaml | 6 ++++ unilabos/registry/devices/robot_gripper.yaml | 4 +++ .../registry/devices/robot_linear_motion.yaml | 6 ++++ .../registry/devices/solid_dispenser.yaml | 2 ++ unilabos/registry/devices/temperature.yaml | 6 ++++ unilabos/registry/devices/virtual_device.yaml | 28 +++++++++++++++++++ unilabos/registry/devices/work_station.yaml | 2 ++ unilabos/registry/devices/xrd_d7mate.yaml | 2 ++ unilabos/registry/devices/zhida_gcms.yaml | 2 ++ .../registry/resources/bioyond/YB_bottle.yaml | 7 +++++ .../resources/bioyond/YB_bottle_carriers.yaml | 14 ++++++++++ .../resources/bioyond/bottle_carriers.yaml | 4 +++ .../registry/resources/bioyond/bottles.yaml | 9 ++++++ unilabos/registry/resources/bioyond/deck.yaml | 4 +++ .../resources/common/resource_container.yaml | 5 ++++ .../registry/resources/laiyu/container.yaml | 2 ++ unilabos/registry/resources/laiyu/deck.yaml | 1 + .../registry/resources/opentrons/deck.yaml | 2 ++ .../resources/opentrons/plate_adapters.yaml | 1 + .../registry/resources/opentrons/plates.yaml | 15 ++++++++++ .../resources/opentrons/reservoirs.yaml | 6 ++++ .../resources/opentrons/tip_racks.yaml | 12 ++++++++ .../resources/opentrons/tube_racks.yaml | 20 +++++++++++++ .../registry/resources/organic/container.yaml | 1 + .../post_process/bottle_carriers.yaml | 2 ++ .../resources/post_process/bottles.yaml | 1 + .../registry/resources/post_process/deck.yaml | 1 + .../resources/prcxi/plate_adapters.yaml | 9 ++++++ unilabos/registry/resources/prcxi/plates.yaml | 11 ++++++++ .../registry/resources/prcxi/tip_racks.yaml | 6 ++++ unilabos/registry/resources/prcxi/trash.yaml | 1 + .../registry/resources/prcxi/tube_racks.yaml | 1 + 53 files changed, 257 insertions(+) diff --git a/unilabos/registry/device_comms/communication_devices.yaml b/unilabos/registry/device_comms/communication_devices.yaml index 782889d48..93191dd27 100644 --- a/unilabos/registry/device_comms/communication_devices.yaml +++ b/unilabos/registry/device_comms/communication_devices.yaml @@ -84,8 +84,10 @@ serial: config_info: [] description: Serial communication interface, used when sharing same serial port for multiple devices + displayname: serial handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/Qone_nmr.yaml b/unilabos/registry/devices/Qone_nmr.yaml index fd2761e48..5f822054d 100644 --- a/unilabos/registry/devices/Qone_nmr.yaml +++ b/unilabos/registry/devices/Qone_nmr.yaml @@ -188,8 +188,10 @@ Qone_nmr: type: python config_info: [] description: Oxford NMR设备驱动,支持CSV字符串到TXT文件的批量转换功能,并监测对应.nmr文件的大小变化以确认结果生成完成 + displayname: Qone_nmr handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: {} diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index 6b2d1b172..17ab61ab1 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -2165,8 +2165,10 @@ bioyond_cell: type: python config_info: [] description: '' + displayname: bioyond_cell handles: [] icon: benyao2.webp + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/bioyond_dispensing_station.yaml b/unilabos/registry/devices/bioyond_dispensing_station.yaml index 21f36e162..aedf70b08 100644 --- a/unilabos/registry/devices/bioyond_dispensing_station.yaml +++ b/unilabos/registry/devices/bioyond_dispensing_station.yaml @@ -691,8 +691,10 @@ bioyond_dispensing_station: type: python config_info: [] description: '' + displayname: bioyond_dispensing_station handles: [] icon: preparation_station.webp + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/cameraSII.yaml b/unilabos/registry/devices/cameraSII.yaml index 446357d07..68a516161 100644 --- a/unilabos/registry/devices/cameraSII.yaml +++ b/unilabos/registry/devices/cameraSII.yaml @@ -55,8 +55,10 @@ cameracontroller_device: type: python config_info: [] description: Uni-Lab-OS 摄像头驱动(Linux USB 摄像头版,无 PTZ) + displayname: cameracontroller_device handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/characterization_chromatic.yaml b/unilabos/registry/devices/characterization_chromatic.yaml index 1b33b9e27..5ba53c854 100644 --- a/unilabos/registry/devices/characterization_chromatic.yaml +++ b/unilabos/registry/devices/characterization_chromatic.yaml @@ -180,8 +180,10 @@ hplc.agilent: type: python config_info: [] description: 安捷伦高效液相色谱(HPLC)分析设备,用于复杂化合物的分离、检测和定量分析。该设备通过UI自动化技术控制安捷伦ChemStation软件,实现全自动的样品分析流程。具备序列启动、设备状态监控、数据文件提取、结果处理等功能。支持多样品批量处理和实时状态反馈,适用于药物分析、环境检测、食品安全、化学研究等需要高精度色谱分析的实验室应用。 + displayname: hplc.agilent handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -370,8 +372,10 @@ hplc.agilent-zhida: type: python config_info: [] description: 智达高效液相色谱(HPLC)分析设备,用于实验室样品的分离、检测和定量分析。该设备通过TCP socket与HPLC控制系统通信,支持远程控制和状态监控。具备自动进样、梯度洗脱、多检测器数据采集等功能,可执行复杂的色谱分析方法。适用于化学分析、药物检测、环境监测、生物样品分析等需要高精度分离分析的实验室应用场景。 + displayname: hplc.agilent-zhida handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/characterization_optic.yaml b/unilabos/registry/devices/characterization_optic.yaml index a7c0e98d1..7a00ba582 100644 --- a/unilabos/registry/devices/characterization_optic.yaml +++ b/unilabos/registry/devices/characterization_optic.yaml @@ -167,8 +167,10 @@ raman.home_made: type: python config_info: [] description: 拉曼光谱分析设备,用于物质的分子结构和化学成分表征。该设备集成激光器和CCD检测器,通过串口通信控制激光功率和光谱采集。具备背景扣除、多次平均、自动数据处理等功能,支持高精度的拉曼光谱测量。适用于材料表征、化学分析、质量控制、研究开发等需要分子指纹识别和结构分析的实验应用。 + displayname: raman.home_made handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/chinwe.yaml b/unilabos/registry/devices/chinwe.yaml index ac4d00bb0..aaf661e08 100644 --- a/unilabos/registry/devices/chinwe.yaml +++ b/unilabos/registry/devices/chinwe.yaml @@ -406,8 +406,10 @@ separator.chinwe: type: python config_info: [] description: ChinWe 简易工作站控制器 (3泵, 2电机, 1传感器) + displayname: separator.chinwe handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/coin_cell_workstation.yaml b/unilabos/registry/devices/coin_cell_workstation.yaml index b692506ce..6a410e408 100644 --- a/unilabos/registry/devices/coin_cell_workstation.yaml +++ b/unilabos/registry/devices/coin_cell_workstation.yaml @@ -756,8 +756,10 @@ coincellassemblyworkstation_device: type: python config_info: [] description: '' + displayname: coincellassemblyworkstation_device handles: [] icon: koudian.webp + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/gas_handler.yaml b/unilabos/registry/devices/gas_handler.yaml index ad2125758..f55a9ab32 100644 --- a/unilabos/registry/devices/gas_handler.yaml +++ b/unilabos/registry/devices/gas_handler.yaml @@ -152,6 +152,7 @@ gas_source.mock: type: python config_info: [] description: 模拟气体源设备,用于系统测试和开发调试。该设备模拟真实气体源的开关控制和状态监测功能,支持气体供应的启停操作。提供与真实气体源相同的接口和状态反馈,便于在没有实际硬件的情况下进行系统集成测试和算法验证。适用于气路系统调试、软件开发和实验流程验证等场景。 + displayname: gas_source.mock handles: - data_key: fluid_out data_source: executor @@ -160,6 +161,7 @@ gas_source.mock: io_type: source label: out icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -331,6 +333,7 @@ vacuum_pump.mock: type: python config_info: [] description: 模拟真空泵设备,用于系统测试和开发调试。该设备模拟真实真空泵的抽气功能和状态控制,支持真空系统的启停操作和状态监测。提供与真实真空泵相同的接口和控制逻辑,便于在没有实际硬件的情况下进行真空系统的集成测试。适用于真空工艺调试、软件开发和实验流程验证等场景。 + displayname: vacuum_pump.mock handles: - data_key: fluid_in data_source: handle @@ -339,6 +342,7 @@ vacuum_pump.mock: io_type: source label: out icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/hotel.yaml b/unilabos/registry/devices/hotel.yaml index 15d962865..aa025f619 100644 --- a/unilabos/registry/devices/hotel.yaml +++ b/unilabos/registry/devices/hotel.yaml @@ -9,8 +9,10 @@ hotel.thermo_orbitor_rs2_hotel: type: python config_info: [] description: Thermo Orbitor RS2 Hotel容器设备,用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控,主要用于存储实验样品、试剂瓶或其他实验器具,支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。 + displayname: hotel.thermo_orbitor_rs2_hotel handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/laiyu_liquid_test.yaml b/unilabos/registry/devices/laiyu_liquid_test.yaml index e3494cac1..d7c81dfff 100644 --- a/unilabos/registry/devices/laiyu_liquid_test.yaml +++ b/unilabos/registry/devices/laiyu_liquid_test.yaml @@ -628,8 +628,10 @@ xyz_stepper_controller: type: python config_info: [] description: 新XYZ控制器 + displayname: xyz_stepper_controller handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index 6ee8a4828..7e3eacf22 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -4852,8 +4852,10 @@ liquid_handler: type: python config_info: [] description: Liquid handler device controlled by pylabrobot + displayname: liquid_handler handles: [] icon: icon_yiyezhan.webp + init_param_enforce: {} init_param_schema: config: properties: @@ -5811,8 +5813,10 @@ liquid_handler.biomek: type: python config_info: [] description: Biomek液体处理器设备,基于pylabrobot控制 + displayname: liquid_handler.biomek handles: [] icon: icon_yiyezhan.webp + init_param_enforce: {} init_param_schema: config: properties: {} @@ -7008,8 +7012,10 @@ liquid_handler.laiyu: type: python config_info: [] description: Laiyu液体处理器设备,基于pylabrobot控制 + displayname: liquid_handler.laiyu handles: [] icon: icon_yiyezhan.webp + init_param_enforce: {} init_param_schema: config: properties: @@ -10240,8 +10246,10 @@ liquid_handler.prcxi: type: python config_info: [] description: prcxi液体处理器设备,基于pylabrobot控制 + displayname: liquid_handler.prcxi handles: [] icon: icon_yiyezhan.webp + init_param_enforce: {} init_param_schema: config: properties: @@ -10466,8 +10474,10 @@ liquid_handler.revvity: type: python config_info: [] description: '' + displayname: liquid_handler.revvity handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: {} diff --git a/unilabos/registry/devices/neware_battery_test_system.yaml b/unilabos/registry/devices/neware_battery_test_system.yaml index 63411b539..28b47b95d 100644 --- a/unilabos/registry/devices/neware_battery_test_system.yaml +++ b/unilabos/registry/devices/neware_battery_test_system.yaml @@ -330,8 +330,10 @@ neware_battery_test_system: type: python config_info: [] description: 新威电池测试系统驱动,提供720个通道的电池测试状态监控、物料管理和CSV批量提交功能。支持TCP通信实现远程控制,包含完整的物料管理系统(2盘电池状态映射),以及从CSV文件批量提交测试任务的能力。 + displayname: neware_battery_test_system handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/opcua_example.yaml b/unilabos/registry/devices/opcua_example.yaml index 271fd6826..e2bd9dd00 100644 --- a/unilabos/registry/devices/opcua_example.yaml +++ b/unilabos/registry/devices/opcua_example.yaml @@ -146,8 +146,10 @@ opcua_example: type: python config_info: [] description: null + displayname: opcua_example handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/opsky_ATR30007.yaml b/unilabos/registry/devices/opsky_ATR30007.yaml index a3fa7df8b..17d2eabeb 100644 --- a/unilabos/registry/devices/opsky_ATR30007.yaml +++ b/unilabos/registry/devices/opsky_ATR30007.yaml @@ -207,8 +207,10 @@ opsky_ATR30007: type: python config_info: [] description: OPSKY ATR30007 光纤拉曼模块,提供单一入口大函数以执行一次完整流程。 + displayname: opsky_ATR30007 handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/organic_miscellaneous.yaml b/unilabos/registry/devices/organic_miscellaneous.yaml index dc81b6710..71ea0d449 100644 --- a/unilabos/registry/devices/organic_miscellaneous.yaml +++ b/unilabos/registry/devices/organic_miscellaneous.yaml @@ -147,8 +147,10 @@ rotavap.one: type: python config_info: [] description: 旋转蒸发仪设备,用于有机化学实验中的溶剂回收和浓缩操作。该设备通过串口通信控制,集成旋转和真空泵功能,支持定时控制和自动化操作。具备旋转速度调节、真空度控制、温度管理等功能,实现高效的溶剂蒸发和回收。适用于有机合成、天然产物提取、药物制备等需要溶剂去除和浓缩的实验室应用场景。 + displayname: rotavap.one handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -484,8 +486,10 @@ separator.homemade: type: python config_info: [] description: 液-液分离器设备,基于自制Grbl控制器的自动化分离系统。该设备集成搅拌、沉降、阀门控制和电导率传感器,通过串口通信实现精确的分离操作控制。支持自动搅拌、分层沉降、基于传感器反馈的智能分液等功能。适用于有机化学中的萃取分离、相分离、液-液提取等需要精确分离控制的实验应用。 + displayname: separator.homemade handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/post_process_station.yaml b/unilabos/registry/devices/post_process_station.yaml index 1614a2c3a..fc48f4023 100644 --- a/unilabos/registry/devices/post_process_station.yaml +++ b/unilabos/registry/devices/post_process_station.yaml @@ -641,8 +641,10 @@ post_process_station: type: python config_info: [] description: 后处理站 + displayname: post_process_station handles: [] icon: post_process_station.webp + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/pump_and_valve.yaml b/unilabos/registry/devices/pump_and_valve.yaml index 25d647f7c..aba5a6571 100644 --- a/unilabos/registry/devices/pump_and_valve.yaml +++ b/unilabos/registry/devices/pump_and_valve.yaml @@ -180,8 +180,10 @@ solenoid_valve: type: python config_info: [] description: 电磁阀控制设备,用于精确的流体路径控制和开关操作。该设备通过串口通信控制电磁阀的开关状态,支持远程操作和状态监测。具备快速响应、可靠密封、状态反馈等特性,广泛应用于流体输送、样品进样、路径切换等需要精确流体控制的实验室自动化应用。 + displayname: solenoid_valve handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -342,6 +344,7 @@ solenoid_valve.mock: type: python config_info: [] description: 模拟电磁阀设备,用于系统测试和开发调试。该设备模拟真实电磁阀的开关操作和状态变化,提供与实际设备相同的控制接口和反馈机制。支持流体路径的虚拟控制,便于在没有实际硬件的情况下进行流体系统的集成测试和算法验证。适用于系统开发、流程调试和培训演示等场景。 + displayname: solenoid_valve.mock handles: - data_type: fluid handler_key: in @@ -354,6 +357,7 @@ solenoid_valve.mock: label: out side: SOUTH icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -818,6 +822,7 @@ syringe_pump_with_valve.runze.SY03B-T06: type: python config_info: [] description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 + displayname: syringe_pump_with_valve.runze.SY03B-T06 handles: - data_key: fluid_port_1 data_source: executor @@ -876,6 +881,7 @@ syringe_pump_with_valve.runze.SY03B-T06: label: 6-in side: WEST icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -1369,6 +1375,7 @@ syringe_pump_with_valve.runze.SY03B-T08: type: python config_info: [] description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 + displayname: syringe_pump_with_valve.runze.SY03B-T08 handles: - data_key: fluid_port_1 data_source: executor @@ -1443,6 +1450,7 @@ syringe_pump_with_valve.runze.SY03B-T08: label: '8' side: NORTH icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml index 7ab22df61..0abeb0149 100644 --- a/unilabos/registry/devices/reaction_station_bioyond.yaml +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -949,8 +949,10 @@ reaction_station.bioyond: type: python config_info: [] description: Bioyond反应站 + displayname: reaction_station.bioyond handles: [] icon: reaction_station.webp + init_param_enforce: {} init_param_schema: config: properties: @@ -1009,8 +1011,10 @@ reaction_station.reactor: type: python config_info: [] description: 反应站子设备-反应器 + displayname: reaction_station.reactor handles: [] icon: reaction_station.webp + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/robot_agv.yaml b/unilabos/registry/devices/robot_agv.yaml index b37a0c46f..8f67de2f3 100644 --- a/unilabos/registry/devices/robot_agv.yaml +++ b/unilabos/registry/devices/robot_agv.yaml @@ -86,8 +86,10 @@ agv.SEER: type: python config_info: [] description: SEER AGV自动导引车设备,用于实验室内物料和设备的自主移动运输。该AGV通过TCP socket与导航系统通信,具备精确的定位和路径规划能力。支持实时位置监控、状态查询和导航任务执行,可在预设的实验室环境中自主移动至指定位置。适用于样品运输、设备转移、多工位协作等实验室自动化物流场景。 + displayname: agv.SEER handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/robot_arm.yaml b/unilabos/registry/devices/robot_arm.yaml index b96e53412..49bcd078a 100644 --- a/unilabos/registry/devices/robot_arm.yaml +++ b/unilabos/registry/devices/robot_arm.yaml @@ -311,8 +311,10 @@ robotic_arm.SCARA_with_slider.moveit.virtual: type: python config_info: [] description: 机械臂与滑块运动系统,基于MoveIt2运动规划框架的多自由度机械臂控制设备。该系统集成机械臂和线性滑块,通过ROS2和MoveIt2实现精确的轨迹规划和协调运动控制。支持笛卡尔空间和关节空间的运动规划、碰撞检测、逆运动学求解等功能。适用于复杂的pick-and-place操作、精密装配、多工位协作等需要高精度多轴协调运动的实验室自动化应用。 + displayname: robotic_arm.SCARA_with_slider.moveit.virtual handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -486,8 +488,10 @@ robotic_arm.UR: type: python config_info: [] description: Universal Robots机械臂设备,用于实验室精密操作和自动化作业。该设备集成了UR机械臂本体、Robotiq夹爪和RTDE通信接口,支持六自由度精确运动控制和力觉反馈。具备实时位置监控、状态反馈、轨迹规划等功能,可执行复杂的多点位运动任务。适用于样品抓取、精密装配、实验器具操作等需要高精度和高重复性的实验室自动化场景。 + displayname: robotic_arm.UR handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -754,8 +758,10 @@ robotic_arm.elite: type: python config_info: [] description: Elite robot arm + displayname: robotic_arm.elite handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/robot_gripper.yaml b/unilabos/registry/devices/robot_gripper.yaml index 4f579e24a..362fbd772 100644 --- a/unilabos/registry/devices/robot_gripper.yaml +++ b/unilabos/registry/devices/robot_gripper.yaml @@ -432,8 +432,10 @@ gripper.misumi_rz: type: python config_info: [] description: Misumi RZ系列电子夹爪设备,集成旋转和抓取双重功能的精密夹爪系统。该设备通过Modbus RTU协议与控制系统通信,支持位置、速度、力矩的精确控制。具备高精度的位置反馈、实时状态监控和故障检测功能。适用于需要精密抓取和旋转操作的实验室自动化场景,如样品管理、精密装配、器件操作等应用。 + displayname: gripper.misumi_rz handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -593,8 +595,10 @@ gripper.mock: type: python config_info: [] description: 模拟夹爪设备,用于系统测试和开发调试。该设备模拟真实夹爪的位置、速度、力矩等物理特性,支持虚拟的抓取和移动操作。提供与真实夹爪相同的接口和状态反馈,便于在没有实际硬件的情况下进行系统集成测试和算法验证。适用于软件开发、系统调试和培训演示等场景。 + displayname: gripper.mock handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: {} diff --git a/unilabos/registry/devices/robot_linear_motion.yaml b/unilabos/registry/devices/robot_linear_motion.yaml index 14539321d..051a4c1ff 100644 --- a/unilabos/registry/devices/robot_linear_motion.yaml +++ b/unilabos/registry/devices/robot_linear_motion.yaml @@ -471,8 +471,10 @@ linear_motion.grbl: type: python config_info: [] description: Grbl数控机床(CNC)设备,用于实验室精密加工和三轴定位操作。该设备基于Grbl固件,通过串口通信控制步进电机实现X、Y、Z三轴的精确运动。支持绝对定位、轨迹规划、主轴控制和实时状态监控。具备安全限位保护和运动平滑控制功能。适用于精密钻孔、铣削、雕刻、样品制备等需要高精度定位和加工的实验室应用场景。 + displayname: linear_motion.grbl handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -822,8 +824,10 @@ linear_motion.toyo_xyz.sim: type: python config_info: [] description: 东洋XYZ三轴运动平台,基于MoveIt2运动规划框架的精密定位设备。该设备通过ROS2和MoveIt2实现三维空间的精确运动控制,支持复杂轨迹规划、多点定位、速度控制等功能。具备高精度定位、平稳运动、实时轨迹监控等特性。适用于精密加工、样品定位、检测扫描、自动化装配等需要高精度三维运动控制的实验室和工业应用场景。 + displayname: linear_motion.toyo_xyz.sim handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -987,8 +991,10 @@ motor.iCL42: type: python config_info: [] description: iCL42步进电机驱动器,用于实验室设备的精密线性运动控制。该设备通过串口通信控制iCL42型步进电机驱动器,支持多种运动模式和精确的位置、速度控制。具备位置反馈、运行状态监控和故障检测功能。适用于自动进样器、样品传送、精密定位平台等需要准确线性运动控制的实验室自动化设备。 + displayname: motor.iCL42 handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/solid_dispenser.yaml b/unilabos/registry/devices/solid_dispenser.yaml index 462806313..b2e5d45ed 100644 --- a/unilabos/registry/devices/solid_dispenser.yaml +++ b/unilabos/registry/devices/solid_dispenser.yaml @@ -363,8 +363,10 @@ solid_dispenser.laiyu: type: python config_info: [] description: 来渝固体粉末自动分装设备,用于实验室化学试剂的精确称量和分装。该设备通过Modbus RTU协议与控制系统通信,集成了精密天平、三轴运动平台、粉筒管理系统等组件。支持多种粉末试剂的自动拿取、精确称量、定点分装和归位操作。具备高精度称量、位置控制和批量处理能力,适用于化学合成、药物研发、材料制备等需要精确固体试剂配制的实验室应用场景。 + displayname: solid_dispenser.laiyu handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/temperature.yaml b/unilabos/registry/devices/temperature.yaml index 9e60adb1f..51485b450 100644 --- a/unilabos/registry/devices/temperature.yaml +++ b/unilabos/registry/devices/temperature.yaml @@ -166,8 +166,10 @@ chiller: type: python config_info: [] description: 实验室制冷设备,用于精确的温度控制和冷却操作。该设备通过Modbus RTU协议与控制系统通信,支持精确的温度设定和监控。具备快速降温、恒温控制和温度保持功能,广泛应用于需要低温环境的化学反应、样品保存、结晶操作等实验场景。提供稳定可靠的冷却性能,确保实验过程的温度精度。 + displayname: chiller handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -556,8 +558,10 @@ heaterstirrer.dalong: type: python config_info: [] description: 大龙加热搅拌器,集成加热和搅拌双重功能的实验室设备。该设备通过串口通信控制,支持精确的温度调节、搅拌速度控制和安全保护功能。具备实时温度监测、目标温度设定、安全温度报警等特性。适用于化学合成、样品制备、反应控制等需要同时进行加热和搅拌的实验操作,提供稳定均匀的反应环境。 + displayname: heaterstirrer.dalong handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -758,8 +762,10 @@ tempsensor: type: python config_info: [] description: 高精度温度传感器设备,用于实验室环境和设备的温度监测。该传感器通过Modbus RTU协议与控制系统通信,提供实时准确的温度数据。具备高精度测量、报警温度设定、数据稳定性好等特点。适用于反应器监控、环境温度监测、设备保护等需要精确温度测量的实验场景,为实验安全和数据可靠性提供保障。 + displayname: tempsensor handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index a34d6f556..3fb0cb5fa 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -251,6 +251,7 @@ virtual_centrifuge: type: python config_info: [] description: Virtual Centrifuge for CentrifugeProtocol Testing + displayname: virtual_centrifuge handles: - data_key: vessel data_source: handle @@ -261,6 +262,7 @@ virtual_centrifuge: label: centrifuge side: NORTH icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -675,6 +677,7 @@ virtual_column: type: python config_info: [] description: Virtual Column Chromatography Device for RunColumn Protocol Testing + displayname: virtual_column handles: - data_key: from_vessel data_source: handle @@ -693,6 +696,7 @@ virtual_column: label: columnout side: EAST icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -1110,6 +1114,7 @@ virtual_filter: type: python config_info: [] description: Virtual Filter for FilterProtocol Testing + displayname: virtual_filter handles: - data_key: vessel_in data_source: handle @@ -1136,6 +1141,7 @@ virtual_filter: label: retentate_out side: EAST icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -1375,6 +1381,7 @@ virtual_gas_source: type: python config_info: [] description: Virtual gas source + displayname: virtual_gas_source handles: - data_key: fluid_out data_source: executor @@ -1385,6 +1392,7 @@ virtual_gas_source: label: gassource side: SOUTH icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -1971,6 +1979,7 @@ virtual_heatchill: type: python config_info: [] description: Virtual HeatChill for HeatChillProtocol Testing + displayname: virtual_heatchill handles: - data_key: vessel data_source: handle @@ -1981,6 +1990,7 @@ virtual_heatchill: label: heatchill side: NORTH icon: Heater.webp + init_param_enforce: {} init_param_schema: config: properties: @@ -2337,6 +2347,7 @@ virtual_multiway_valve: type: python config_info: [] description: Virtual 8-Way Valve for flow direction control + displayname: virtual_multiway_valve handles: - data_key: fluid_in data_source: handle @@ -2419,6 +2430,7 @@ virtual_multiway_valve: label: '8' side: NORTH icon: EightPipeline.webp + init_param_enforce: {} init_param_schema: config: properties: @@ -2735,6 +2747,7 @@ virtual_rotavap: type: python config_info: [] description: Virtual Rotary Evaporator for EvaporateProtocol Testing + displayname: virtual_rotavap handles: - data_key: vessel_in data_source: handle @@ -2761,6 +2774,7 @@ virtual_rotavap: label: solvent_out side: EAST icon: Rotaryevaporator.webp + init_param_enforce: {} init_param_schema: config: properties: @@ -2986,8 +3000,10 @@ virtual_sample_demo: type: python config_info: [] description: Virtual sample tracking demo device + displayname: virtual_sample_demo handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -3947,6 +3963,7 @@ virtual_separator: type: python config_info: [] description: Virtual Separator for SeparateProtocol Testing + displayname: virtual_separator handles: - data_key: from_vessel data_source: handle @@ -3981,6 +3998,7 @@ virtual_separator: label: bind side: WEST icon: Separator.webp + init_param_enforce: {} init_param_schema: config: properties: @@ -4290,6 +4308,7 @@ virtual_solenoid_valve: type: python config_info: [] description: Virtual Solenoid Valve for simple on/off flow control + displayname: virtual_solenoid_valve handles: - data_key: fluid_port_in data_source: handle @@ -4308,6 +4327,7 @@ virtual_solenoid_valve: label: out side: SOUTH icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -4693,6 +4713,7 @@ virtual_solid_dispenser: config_info: [] description: Virtual Solid Dispenser for Add Protocol Testing - supports mass and molar additions + displayname: virtual_solid_dispenser handles: - data_key: solid_out data_source: executor @@ -4711,6 +4732,7 @@ virtual_solid_dispenser: label: SolidIn side: NORTH icon: '' + init_param_enforce: {} init_param_schema: config: properties: @@ -5315,6 +5337,7 @@ virtual_stirrer: type: python config_info: [] description: Virtual Stirrer for StirProtocol Testing + displayname: virtual_stirrer handles: - data_key: vessel data_source: handle @@ -5325,6 +5348,7 @@ virtual_stirrer: label: stirrer side: NORTH icon: Stirrer.webp + init_param_enforce: {} init_param_schema: config: properties: @@ -5848,6 +5872,7 @@ virtual_transfer_pump: type: python config_info: [] description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style) + displayname: virtual_transfer_pump handles: - data_key: fluid_port data_source: handle @@ -5858,6 +5883,7 @@ virtual_transfer_pump: label: transferpump side: SOUTH icon: Pump.webp + init_param_enforce: {} init_param_schema: config: properties: @@ -6090,6 +6116,7 @@ virtual_vacuum_pump: type: python config_info: [] description: Virtual vacuum pump + displayname: virtual_vacuum_pump handles: - data_key: fluid_in data_source: handle @@ -6100,6 +6127,7 @@ virtual_vacuum_pump: label: vacuumpump side: SOUTH icon: Vacuum.webp + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index 87a2fabef..0c65ecc67 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -6539,8 +6539,10 @@ workstation: type: python config_info: [] description: Workstation + displayname: workstation handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/xrd_d7mate.yaml b/unilabos/registry/devices/xrd_d7mate.yaml index 38e31718a..8d1c14a27 100644 --- a/unilabos/registry/devices/xrd_d7mate.yaml +++ b/unilabos/registry/devices/xrd_d7mate.yaml @@ -485,8 +485,10 @@ xrd_d7mate: type: python config_info: [] description: XRD D7-Mate X射线衍射分析设备,通过TCP通信实现远程控制与状态监控,支持自动模式控制、上样流程、数据获取、下样流程和高压电源控制等功能。 + displayname: xrd_d7mate handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/devices/zhida_gcms.yaml b/unilabos/registry/devices/zhida_gcms.yaml index b10b29ad9..00b969b0d 100644 --- a/unilabos/registry/devices/zhida_gcms.yaml +++ b/unilabos/registry/devices/zhida_gcms.yaml @@ -284,8 +284,10 @@ zhida_gcms: type: python config_info: [] description: 智达气相色谱-质谱联用(GC-MS)分析设备,通过 TCP 通信实现远程控制与状态监控,支持方法管理与任务启动等功能。 + displayname: zhida_gcms handles: [] icon: '' + init_param_enforce: {} init_param_schema: config: properties: diff --git a/unilabos/registry/resources/bioyond/YB_bottle.yaml b/unilabos/registry/resources/bioyond/YB_bottle.yaml index 199173723..52a2afc99 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle.yaml @@ -6,6 +6,7 @@ YB_20ml_fenyeping: module: unilabos.resources.bioyond.YB_bottles:YB_20ml_fenyeping type: pylabrobot description: YB_20ml_fenyeping + displayname: YB_20ml_fenyeping handles: [] icon: '' init_param_schema: {} @@ -18,6 +19,7 @@ YB_5ml_fenyeping: module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping type: pylabrobot description: YB_5ml_fenyeping + displayname: YB_5ml_fenyeping handles: [] icon: '' init_param_schema: {} @@ -30,6 +32,7 @@ YB_jia_yang_tou_da: module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da type: pylabrobot description: YB_jia_yang_tou_da + displayname: YB_jia_yang_tou_da handles: [] icon: '' init_param_schema: {} @@ -42,6 +45,7 @@ YB_pei_ye_da_Bottle: module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_da_Bottle type: pylabrobot description: YB_pei_ye_da_Bottle + displayname: YB_pei_ye_da_Bottle handles: [] icon: '' init_param_schema: {} @@ -54,6 +58,7 @@ YB_pei_ye_xiao_Bottle: module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle type: pylabrobot description: YB_pei_ye_xiao_Bottle + displayname: YB_pei_ye_xiao_Bottle handles: [] icon: '' init_param_schema: {} @@ -66,6 +71,7 @@ YB_qiang_tou: module: unilabos.resources.bioyond.YB_bottles:YB_qiang_tou type: pylabrobot description: YB_qiang_tou + displayname: YB_qiang_tou handles: [] icon: '' init_param_schema: {} @@ -79,6 +85,7 @@ YB_ye_Bottle: module: unilabos.resources.bioyond.YB_bottles:YB_ye_Bottle type: pylabrobot description: YB_ye_Bottle + displayname: YB_ye_Bottle handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml index 76b6b9387..a9fef5582 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml @@ -6,6 +6,7 @@ YB_100ml_yeti: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_100ml_yeti type: pylabrobot description: YB_100ml_yeti + displayname: YB_100ml_yeti handles: [] icon: '' init_param_schema: {} @@ -18,6 +19,7 @@ YB_20ml_fenyepingban: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_20ml_fenyepingban type: pylabrobot description: YB_20ml_fenyepingban + displayname: YB_20ml_fenyepingban handles: [] icon: '' init_param_schema: {} @@ -30,6 +32,7 @@ YB_5ml_fenyepingban: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_5ml_fenyepingban type: pylabrobot description: YB_5ml_fenyepingban + displayname: YB_5ml_fenyepingban handles: [] icon: '' init_param_schema: {} @@ -42,6 +45,7 @@ YB_6StockCarrier: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier type: pylabrobot description: YB_6StockCarrier + displayname: YB_6StockCarrier handles: [] icon: '' init_param_schema: {} @@ -54,6 +58,7 @@ YB_6VialCarrier: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier type: pylabrobot description: YB_6VialCarrier + displayname: YB_6VialCarrier handles: [] icon: '' init_param_schema: {} @@ -66,6 +71,7 @@ YB_gao_nian_ye_Bottle: module: unilabos.resources.bioyond.YB_bottles:YB_gao_nian_ye_Bottle type: pylabrobot description: YB_gao_nian_ye_Bottle + displayname: YB_gao_nian_ye_Bottle handles: [] icon: '' init_param_schema: {} @@ -78,6 +84,7 @@ YB_gaonianye: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_gaonianye type: pylabrobot description: YB_gaonianye + displayname: YB_gaonianye handles: [] icon: '' init_param_schema: {} @@ -90,6 +97,7 @@ YB_jia_yang_tou_da_Carrier: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_Carrier type: pylabrobot description: YB_jia_yang_tou_da_Carrier + displayname: YB_jia_yang_tou_da_Carrier handles: [] icon: '' init_param_schema: {} @@ -102,6 +110,7 @@ YB_peiyepingdaban: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingdaban type: pylabrobot description: YB_peiyepingdaban + displayname: YB_peiyepingdaban handles: [] icon: '' init_param_schema: {} @@ -114,6 +123,7 @@ YB_peiyepingxiaoban: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban type: pylabrobot description: YB_peiyepingxiaoban + displayname: YB_peiyepingxiaoban handles: [] icon: '' init_param_schema: {} @@ -126,6 +136,7 @@ YB_qiang_tou_he: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_qiang_tou_he type: pylabrobot description: YB_qiang_tou_he + displayname: YB_qiang_tou_he handles: [] icon: '' init_param_schema: {} @@ -138,6 +149,7 @@ YB_shi_pei_qi_kuai: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_shi_pei_qi_kuai type: pylabrobot description: YB_shi_pei_qi_kuai + displayname: YB_shi_pei_qi_kuai handles: [] icon: '' init_param_schema: {} @@ -150,6 +162,7 @@ YB_ye: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_ye type: pylabrobot description: YB_ye_Bottle_Carrier + displayname: YB_ye handles: [] icon: '' init_param_schema: {} @@ -162,6 +175,7 @@ YB_ye_100ml_Bottle: module: unilabos.resources.bioyond.YB_bottles:YB_ye_100ml_Bottle type: pylabrobot description: YB_ye_100ml_Bottle + displayname: YB_ye_100ml_Bottle handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/bioyond/bottle_carriers.yaml b/unilabos/registry/resources/bioyond/bottle_carriers.yaml index f72cc10d8..b0f62f49c 100644 --- a/unilabos/registry/resources/bioyond/bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/bottle_carriers.yaml @@ -5,6 +5,7 @@ BIOYOND_PolymerStation_1BottleCarrier: module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1BottleCarrier type: pylabrobot description: BIOYOND_PolymerStation_1BottleCarrier + displayname: BIOYOND_PolymerStation_1BottleCarrier handles: [] icon: '' init_param_schema: {} @@ -16,6 +17,7 @@ BIOYOND_PolymerStation_1FlaskCarrier: module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1FlaskCarrier type: pylabrobot description: BIOYOND_PolymerStation_1FlaskCarrier + displayname: BIOYOND_PolymerStation_1FlaskCarrier handles: [] icon: '' init_param_schema: {} @@ -27,6 +29,7 @@ BIOYOND_PolymerStation_6StockCarrier: module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6StockCarrier type: pylabrobot description: BIOYOND_PolymerStation_6StockCarrier + displayname: BIOYOND_PolymerStation_6StockCarrier handles: [] icon: '' init_param_schema: {} @@ -38,6 +41,7 @@ BIOYOND_PolymerStation_8StockCarrier: module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_8StockCarrier type: pylabrobot description: BIOYOND_PolymerStation_8StockCarrier (2x4布局,8个位置) + displayname: BIOYOND_PolymerStation_8StockCarrier handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/bioyond/bottles.yaml b/unilabos/registry/resources/bioyond/bottles.yaml index ecc5525d8..dca628419 100644 --- a/unilabos/registry/resources/bioyond/bottles.yaml +++ b/unilabos/registry/resources/bioyond/bottles.yaml @@ -6,6 +6,7 @@ BIOYOND_PolymerStation_Flask: module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Flask type: pylabrobot description: 聚合站-烧杯容器 + displayname: BIOYOND_PolymerStation_Flask handles: [] icon: '' init_param_schema: {} @@ -16,6 +17,7 @@ BIOYOND_PolymerStation_Liquid_Vial: class: module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Liquid_Vial type: pylabrobot + displayname: BIOYOND_PolymerStation_Liquid_Vial handles: [] icon: '' init_param_schema: {} @@ -27,6 +29,7 @@ BIOYOND_PolymerStation_Measurement_Vial: module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Measurement_Vial type: pylabrobot description: 聚合站-测量小瓶(测密度) + displayname: BIOYOND_PolymerStation_Measurement_Vial handles: [] icon: '' init_param_schema: {} @@ -38,6 +41,7 @@ BIOYOND_PolymerStation_Reactor: class: module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Reactor type: pylabrobot + displayname: BIOYOND_PolymerStation_Reactor handles: [] icon: '' init_param_schema: {} @@ -48,6 +52,7 @@ BIOYOND_PolymerStation_Reagent_Bottle: class: module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Reagent_Bottle type: pylabrobot + displayname: BIOYOND_PolymerStation_Reagent_Bottle handles: [] icon: '' init_param_schema: {} @@ -58,6 +63,7 @@ BIOYOND_PolymerStation_Solid_Stock: class: module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Stock type: pylabrobot + displayname: BIOYOND_PolymerStation_Solid_Stock handles: [] icon: '' init_param_schema: {} @@ -68,6 +74,7 @@ BIOYOND_PolymerStation_Solid_Vial: class: module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Vial type: pylabrobot + displayname: BIOYOND_PolymerStation_Solid_Vial handles: [] icon: '' init_param_schema: {} @@ -78,6 +85,7 @@ BIOYOND_PolymerStation_Solution_Beaker: class: module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solution_Beaker type: pylabrobot + displayname: BIOYOND_PolymerStation_Solution_Beaker handles: [] icon: '' init_param_schema: {} @@ -89,6 +97,7 @@ BIOYOND_PolymerStation_TipBox: class: module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_TipBox type: pylabrobot + displayname: BIOYOND_PolymerStation_TipBox handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml index 5770a2d12..50b533f31 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -5,6 +5,7 @@ BIOYOND_PolymerPreparationStation_Deck: module: unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck type: pylabrobot description: BIOYOND PolymerPreparationStation Deck + displayname: BIOYOND_PolymerPreparationStation_Deck handles: [] icon: 配液站.webp init_param_schema: {} @@ -16,6 +17,7 @@ BIOYOND_PolymerReactionStation_Deck: module: unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck type: pylabrobot description: BIOYOND PolymerReactionStation Deck + displayname: BIOYOND_PolymerReactionStation_Deck handles: [] icon: 反应站.webp init_param_schema: {} @@ -27,6 +29,7 @@ BIOYOND_YB_Deck: module: unilabos.resources.bioyond.decks:YB_Deck type: pylabrobot description: BIOYOND ElectrolyteFormulationStation Deck + displayname: BIOYOND_YB_Deck handles: [] icon: 配液站.webp init_param_schema: {} @@ -38,6 +41,7 @@ CoincellDeck: module: unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:YH_Deck type: pylabrobot description: YIHUA CoinCellAssembly Deck + displayname: CoincellDeck handles: [] icon: koudian.webp init_param_schema: {} diff --git a/unilabos/registry/resources/common/resource_container.yaml b/unilabos/registry/resources/common/resource_container.yaml index 751f1aa5e..53bad236b 100644 --- a/unilabos/registry/resources/common/resource_container.yaml +++ b/unilabos/registry/resources/common/resource_container.yaml @@ -5,6 +5,7 @@ hplc_plate: module: unilabos.devices.resource_container.container:PlateContainer type: python description: HPLC板 + displayname: hplc_plate handles: [] icon: '' init_param_schema: {} @@ -27,6 +28,7 @@ plate_96: module: unilabos.devices.resource_container.container:PlateContainer type: python description: 96孔板 + displayname: plate_96 handles: [] icon: '' init_param_schema: {} @@ -49,6 +51,7 @@ plate_96_high: module: unilabos.devices.resource_container.container:PlateContainer type: python description: 96孔板 + displayname: plate_96_high handles: [] icon: '' init_param_schema: {} @@ -71,6 +74,7 @@ tiprack_96_high: module: unilabos.devices.resource_container.container:TipRackContainer type: python description: 96孔板 + displayname: tiprack_96_high handles: [] icon: '' init_param_schema: {} @@ -102,6 +106,7 @@ tiprack_box: module: unilabos.devices.resource_container.container:TipRackContainer type: python description: 96针头盒 + displayname: tiprack_box handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/laiyu/container.yaml b/unilabos/registry/resources/laiyu/container.yaml index 400bc9312..8a1dd2ae0 100644 --- a/unilabos/registry/resources/laiyu/container.yaml +++ b/unilabos/registry/resources/laiyu/container.yaml @@ -6,6 +6,7 @@ bottle_container: module: unilabos.devices.resource_container.container:BottleRackContainer type: python description: 96孔板 + displayname: bottle_container handles: [] icon: '' init_param_schema: {} @@ -38,6 +39,7 @@ tube_container: module: unilabos.devices.resource_container.container:TubeRackContainer type: python description: 96孔板 + displayname: tube_container handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/laiyu/deck.yaml b/unilabos/registry/resources/laiyu/deck.yaml index 89973dded..9c75a7d47 100644 --- a/unilabos/registry/resources/laiyu/deck.yaml +++ b/unilabos/registry/resources/laiyu/deck.yaml @@ -5,6 +5,7 @@ TransformXYZDeck: module: unilabos.devices.liquid_handling.laiyu.laiyu:TransformXYZDeck type: pylabrobot description: Laiyu deck + displayname: TransformXYZDeck handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/opentrons/deck.yaml b/unilabos/registry/resources/opentrons/deck.yaml index 0e35e7b1c..ea8723d58 100644 --- a/unilabos/registry/resources/opentrons/deck.yaml +++ b/unilabos/registry/resources/opentrons/deck.yaml @@ -5,6 +5,7 @@ OTDeck: module: pylabrobot.resources.opentrons.deck:OTDeck type: pylabrobot description: Opentrons deck + displayname: OTDeck handles: [] icon: '' init_param_schema: {} @@ -20,6 +21,7 @@ hplc_station: module: unilabos.devices.resource_container.container:DeckContainer type: python description: hplc_station deck + displayname: hplc_station handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/opentrons/plate_adapters.yaml b/unilabos/registry/resources/opentrons/plate_adapters.yaml index d09bf7844..6f7073efa 100644 --- a/unilabos/registry/resources/opentrons/plate_adapters.yaml +++ b/unilabos/registry/resources/opentrons/plate_adapters.yaml @@ -5,6 +5,7 @@ Opentrons_96_adapter_Vb: module: pylabrobot.resources.opentrons.plate_adapters:Opentrons_96_adapter_Vb type: pylabrobot description: Opentrons 96 adapter Vb + displayname: Opentrons_96_adapter_Vb handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/opentrons/plates.yaml b/unilabos/registry/resources/opentrons/plates.yaml index 883bf147b..a62a11dc5 100644 --- a/unilabos/registry/resources/opentrons/plates.yaml +++ b/unilabos/registry/resources/opentrons/plates.yaml @@ -5,6 +5,7 @@ appliedbiosystemsmicroamp_384_wellplate_40ul: module: pylabrobot.resources.opentrons.plates:appliedbiosystemsmicroamp_384_wellplate_40ul type: pylabrobot description: Applied Biosystems microamp 384 wellplate 40ul + displayname: appliedbiosystemsmicroamp_384_wellplate_40ul handles: [] icon: '' init_param_schema: {} @@ -16,6 +17,7 @@ biorad_384_wellplate_50ul: module: pylabrobot.resources.opentrons.plates:biorad_384_wellplate_50ul type: pylabrobot description: BioRad 384 wellplate 50ul + displayname: biorad_384_wellplate_50ul handles: [] icon: '' init_param_schema: {} @@ -27,6 +29,7 @@ biorad_96_wellplate_200ul_pcr: module: pylabrobot.resources.opentrons.plates:biorad_96_wellplate_200ul_pcr type: pylabrobot description: BioRad 96 wellplate 200ul pcr + displayname: biorad_96_wellplate_200ul_pcr handles: [] icon: '' init_param_schema: {} @@ -38,6 +41,7 @@ corning_12_wellplate_6point9ml_flat: module: pylabrobot.resources.opentrons.plates:corning_12_wellplate_6point9ml_flat type: pylabrobot description: Corning 12 wellplate 6.9ml flat + displayname: corning_12_wellplate_6point9ml_flat handles: [] icon: '' init_param_schema: {} @@ -49,6 +53,7 @@ corning_24_wellplate_3point4ml_flat: module: pylabrobot.resources.opentrons.plates:corning_24_wellplate_3point4ml_flat type: pylabrobot description: Corning 24 wellplate 3.4ml flat + displayname: corning_24_wellplate_3point4ml_flat handles: [] icon: '' init_param_schema: {} @@ -60,6 +65,7 @@ corning_384_wellplate_112ul_flat: module: pylabrobot.resources.opentrons.plates:corning_384_wellplate_112ul_flat type: pylabrobot description: Corning 384 wellplate 112ul flat + displayname: corning_384_wellplate_112ul_flat handles: [] icon: '' init_param_schema: {} @@ -71,6 +77,7 @@ corning_48_wellplate_1point6ml_flat: module: pylabrobot.resources.opentrons.plates:corning_48_wellplate_1point6ml_flat type: pylabrobot description: Corning 48 wellplate 1.6ml flat + displayname: corning_48_wellplate_1point6ml_flat handles: [] icon: '' init_param_schema: {} @@ -82,6 +89,7 @@ corning_6_wellplate_16point8ml_flat: module: pylabrobot.resources.opentrons.plates:corning_6_wellplate_16point8ml_flat type: pylabrobot description: Corning 6 wellplate 16.8ml flat + displayname: corning_6_wellplate_16point8ml_flat handles: [] icon: '' init_param_schema: {} @@ -93,6 +101,7 @@ corning_96_wellplate_360ul_flat: module: pylabrobot.resources.opentrons.plates:corning_96_wellplate_360ul_flat type: pylabrobot description: Corning 96 wellplate 360ul flat + displayname: corning_96_wellplate_360ul_flat handles: [] icon: '' init_param_schema: {} @@ -104,6 +113,7 @@ nest_96_wellplate_100ul_pcr_full_skirt: module: pylabrobot.resources.opentrons.plates:nest_96_wellplate_100ul_pcr_full_skirt type: pylabrobot description: Nest 96 wellplate 100ul pcr full skirt + displayname: nest_96_wellplate_100ul_pcr_full_skirt handles: [] icon: '' init_param_schema: {} @@ -135,6 +145,7 @@ nest_96_wellplate_200ul_flat: module: pylabrobot.resources.opentrons.plates:nest_96_wellplate_200ul_flat type: pylabrobot description: Nest 96 wellplate 200ul flat + displayname: nest_96_wellplate_200ul_flat handles: [] icon: '' init_param_schema: {} @@ -146,6 +157,7 @@ nest_96_wellplate_2ml_deep: module: pylabrobot.resources.opentrons.plates:nest_96_wellplate_2ml_deep type: pylabrobot description: Nest 96 wellplate 2ml deep + displayname: nest_96_wellplate_2ml_deep handles: [] icon: '' init_param_schema: {} @@ -168,6 +180,7 @@ thermoscientificnunc_96_wellplate_1300ul: module: pylabrobot.resources.opentrons.plates:thermoscientificnunc_96_wellplate_1300ul type: pylabrobot description: Thermoscientific Nunc 96 wellplate 1300ul + displayname: thermoscientificnunc_96_wellplate_1300ul handles: [] icon: '' init_param_schema: {} @@ -179,6 +192,7 @@ thermoscientificnunc_96_wellplate_2000ul: module: pylabrobot.resources.opentrons.plates:thermoscientificnunc_96_wellplate_2000ul type: pylabrobot description: Thermoscientific Nunc 96 wellplate 2000ul + displayname: thermoscientificnunc_96_wellplate_2000ul handles: [] icon: '' init_param_schema: {} @@ -190,6 +204,7 @@ usascientific_96_wellplate_2point4ml_deep: module: pylabrobot.resources.opentrons.plates:usascientific_96_wellplate_2point4ml_deep type: pylabrobot description: USAScientific 96 wellplate 2.4ml deep + displayname: usascientific_96_wellplate_2point4ml_deep handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/opentrons/reservoirs.yaml b/unilabos/registry/resources/opentrons/reservoirs.yaml index 6b2033d93..8579f40ce 100644 --- a/unilabos/registry/resources/opentrons/reservoirs.yaml +++ b/unilabos/registry/resources/opentrons/reservoirs.yaml @@ -5,6 +5,7 @@ agilent_1_reservoir_290ml: module: pylabrobot.resources.opentrons.reservoirs:agilent_1_reservoir_290ml type: pylabrobot description: Agilent 1 reservoir 290ml + displayname: agilent_1_reservoir_290ml handles: [] icon: '' init_param_schema: {} @@ -16,6 +17,7 @@ axygen_1_reservoir_90ml: module: pylabrobot.resources.opentrons.reservoirs:axygen_1_reservoir_90ml type: pylabrobot description: Axygen 1 reservoir 90ml + displayname: axygen_1_reservoir_90ml handles: [] icon: '' init_param_schema: {} @@ -27,6 +29,7 @@ nest_12_reservoir_15ml: module: pylabrobot.resources.opentrons.reservoirs:nest_12_reservoir_15ml type: pylabrobot description: Nest 12 reservoir 15ml + displayname: nest_12_reservoir_15ml handles: [] icon: '' init_param_schema: {} @@ -38,6 +41,7 @@ nest_1_reservoir_195ml: module: pylabrobot.resources.opentrons.reservoirs:nest_1_reservoir_195ml type: pylabrobot description: Nest 1 reservoir 195ml + displayname: nest_1_reservoir_195ml handles: [] icon: '' init_param_schema: {} @@ -49,6 +53,7 @@ nest_1_reservoir_290ml: module: pylabrobot.resources.opentrons.reservoirs:nest_1_reservoir_290ml type: pylabrobot description: Nest 1 reservoir 290ml + displayname: nest_1_reservoir_290ml handles: [] icon: '' init_param_schema: {} @@ -60,6 +65,7 @@ usascientific_12_reservoir_22ml: module: pylabrobot.resources.opentrons.reservoirs:usascientific_12_reservoir_22ml type: pylabrobot description: USAScientific 12 reservoir 22ml + displayname: usascientific_12_reservoir_22ml handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/opentrons/tip_racks.yaml b/unilabos/registry/resources/opentrons/tip_racks.yaml index ec8380185..137559371 100644 --- a/unilabos/registry/resources/opentrons/tip_racks.yaml +++ b/unilabos/registry/resources/opentrons/tip_racks.yaml @@ -5,6 +5,7 @@ eppendorf_96_tiprack_1000ul_eptips: module: pylabrobot.resources.opentrons.tip_racks:eppendorf_96_tiprack_1000ul_eptips type: pylabrobot description: Eppendorf 96 tiprack 1000ul eptips + displayname: eppendorf_96_tiprack_1000ul_eptips handles: [] icon: '' init_param_schema: {} @@ -16,6 +17,7 @@ eppendorf_96_tiprack_10ul_eptips: module: pylabrobot.resources.opentrons.tip_racks:eppendorf_96_tiprack_10ul_eptips type: pylabrobot description: Eppendorf 96 tiprack 10ul eptips + displayname: eppendorf_96_tiprack_10ul_eptips handles: [] icon: '' init_param_schema: {} @@ -27,6 +29,7 @@ geb_96_tiprack_1000ul: module: pylabrobot.resources.opentrons.tip_racks:geb_96_tiprack_1000ul type: pylabrobot description: Geb 96 tiprack 1000ul + displayname: geb_96_tiprack_1000ul handles: [] icon: '' init_param_schema: {} @@ -38,6 +41,7 @@ geb_96_tiprack_10ul: module: pylabrobot.resources.opentrons.tip_racks:geb_96_tiprack_10ul type: pylabrobot description: Geb 96 tiprack 10ul + displayname: geb_96_tiprack_10ul handles: [] icon: '' init_param_schema: {} @@ -49,6 +53,7 @@ opentrons_96_filtertiprack_1000ul: module: pylabrobot.resources.opentrons.tip_racks:opentrons_96_filtertiprack_1000ul type: pylabrobot description: Opentrons 96 filtertiprack 1000ul + displayname: opentrons_96_filtertiprack_1000ul handles: [] icon: '' init_param_schema: {} @@ -79,6 +84,7 @@ opentrons_96_filtertiprack_10ul: module: pylabrobot.resources.opentrons.tip_racks:opentrons_96_filtertiprack_10ul type: pylabrobot description: Opentrons 96 filtertiprack 10ul + displayname: opentrons_96_filtertiprack_10ul handles: [] icon: '' init_param_schema: {} @@ -90,6 +96,7 @@ opentrons_96_filtertiprack_200ul: module: pylabrobot.resources.opentrons.tip_racks:opentrons_96_filtertiprack_200ul type: pylabrobot description: Opentrons 96 filtertiprack 200ul + displayname: opentrons_96_filtertiprack_200ul handles: [] icon: '' init_param_schema: {} @@ -101,6 +108,7 @@ opentrons_96_filtertiprack_20ul: module: pylabrobot.resources.opentrons.tip_racks:opentrons_96_filtertiprack_20ul type: pylabrobot description: Opentrons 96 filtertiprack 20ul + displayname: opentrons_96_filtertiprack_20ul handles: [] icon: '' init_param_schema: {} @@ -112,6 +120,7 @@ opentrons_96_tiprack_1000ul: module: pylabrobot.resources.opentrons.tip_racks:opentrons_96_tiprack_1000ul type: pylabrobot description: Opentrons 96 tiprack 1000ul + displayname: opentrons_96_tiprack_1000ul handles: [] icon: '' init_param_schema: {} @@ -123,6 +132,7 @@ opentrons_96_tiprack_10ul: module: pylabrobot.resources.opentrons.tip_racks:opentrons_96_tiprack_10ul type: pylabrobot description: Opentrons 96 tiprack 10ul + displayname: opentrons_96_tiprack_10ul handles: [] icon: '' init_param_schema: {} @@ -134,6 +144,7 @@ opentrons_96_tiprack_20ul: module: pylabrobot.resources.opentrons.tip_racks:opentrons_96_tiprack_20ul type: pylabrobot description: Opentrons 96 tiprack 20ul + displayname: opentrons_96_tiprack_20ul handles: [] icon: '' init_param_schema: {} @@ -144,6 +155,7 @@ opentrons_96_tiprack_300ul: class: module: pylabrobot.resources.opentrons.tip_racks:opentrons_96_tiprack_300ul type: pylabrobot + displayname: opentrons_96_tiprack_300ul handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/opentrons/tube_racks.yaml b/unilabos/registry/resources/opentrons/tube_racks.yaml index 33ec5dc92..774247bed 100644 --- a/unilabos/registry/resources/opentrons/tube_racks.yaml +++ b/unilabos/registry/resources/opentrons/tube_racks.yaml @@ -5,6 +5,7 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical: module: pylabrobot.resources.opentrons.tube_racks:opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical type: pylabrobot description: Opentrons 10 tuberack falcon 4x50ml 6x15ml conical + displayname: opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical handles: [] icon: '' init_param_schema: {} @@ -16,6 +17,7 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic: module: pylabrobot.resources.opentrons.tube_racks:opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic type: pylabrobot description: Opentrons 10 tuberack falcon 4x50ml 6x15ml conical acrylic + displayname: opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic handles: [] icon: '' init_param_schema: {} @@ -27,6 +29,7 @@ opentrons_10_tuberack_nest_4x50ml_6x15ml_conical: module: pylabrobot.resources.opentrons.tube_racks:opentrons_10_tuberack_nest_4x50ml_6x15ml_conical type: pylabrobot description: Opentrons 10 tuberack nest 4x50ml 6x15ml conical + displayname: opentrons_10_tuberack_nest_4x50ml_6x15ml_conical handles: [] icon: '' init_param_schema: {} @@ -38,6 +41,7 @@ opentrons_15_tuberack_falcon_15ml_conical: module: pylabrobot.resources.opentrons.tube_racks:opentrons_15_tuberack_falcon_15ml_conical type: pylabrobot description: Opentrons 15 tuberack falcon 15ml conical + displayname: opentrons_15_tuberack_falcon_15ml_conical handles: [] icon: '' init_param_schema: {} @@ -49,6 +53,7 @@ opentrons_15_tuberack_nest_15ml_conical: module: pylabrobot.resources.opentrons.tube_racks:opentrons_15_tuberack_nest_15ml_conical type: pylabrobot description: Opentrons 15 tuberack nest 15ml conical + displayname: opentrons_15_tuberack_nest_15ml_conical handles: [] icon: '' init_param_schema: {} @@ -60,6 +65,7 @@ opentrons_24_aluminumblock_generic_2ml_screwcap: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_aluminumblock_generic_2ml_screwcap type: pylabrobot description: Opentrons 24 aluminumblock generic 2ml screwcap + displayname: opentrons_24_aluminumblock_generic_2ml_screwcap handles: [] icon: '' init_param_schema: {} @@ -71,6 +77,7 @@ opentrons_24_aluminumblock_nest_1point5ml_snapcap: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_aluminumblock_nest_1point5ml_snapcap type: pylabrobot description: Opentrons 24 aluminumblock nest 1.5ml snapcap + displayname: opentrons_24_aluminumblock_nest_1point5ml_snapcap handles: [] icon: '' init_param_schema: {} @@ -82,6 +89,7 @@ opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap type: pylabrobot description: Opentrons 24 tuberack eppendorf 1.5ml safelock snapcap + displayname: opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap handles: [] icon: '' init_param_schema: {} @@ -93,6 +101,7 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap type: pylabrobot description: Opentrons 24 tuberack eppendorf 2ml safelock snapcap + displayname: opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap handles: [] icon: '' init_param_schema: {} @@ -104,6 +113,7 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic type: pylabrobot description: Opentrons 24 tuberack eppendorf 2ml safelock snapcap acrylic + displayname: opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic handles: [] icon: '' init_param_schema: {} @@ -115,6 +125,7 @@ opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic type: pylabrobot description: Opentrons 24 tuberack generic 0.75ml snapcap acrylic + displayname: opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic handles: [] icon: '' init_param_schema: {} @@ -126,6 +137,7 @@ opentrons_24_tuberack_generic_2ml_screwcap: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_tuberack_generic_2ml_screwcap type: pylabrobot description: Opentrons 24 tuberack generic 2ml screwcap + displayname: opentrons_24_tuberack_generic_2ml_screwcap handles: [] icon: '' init_param_schema: {} @@ -137,6 +149,7 @@ opentrons_24_tuberack_nest_0point5ml_screwcap: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_tuberack_nest_0point5ml_screwcap type: pylabrobot description: Opentrons 24 tuberack nest 0.5ml screwcap + displayname: opentrons_24_tuberack_nest_0point5ml_screwcap handles: [] icon: '' init_param_schema: {} @@ -148,6 +161,7 @@ opentrons_24_tuberack_nest_1point5ml_screwcap: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_tuberack_nest_1point5ml_screwcap type: pylabrobot description: Opentrons 24 tuberack nest 1.5ml screwcap + displayname: opentrons_24_tuberack_nest_1point5ml_screwcap handles: [] icon: '' init_param_schema: {} @@ -159,6 +173,7 @@ opentrons_24_tuberack_nest_1point5ml_snapcap: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_tuberack_nest_1point5ml_snapcap type: pylabrobot description: Opentrons 24 tuberack nest 1.5ml snapcap + displayname: opentrons_24_tuberack_nest_1point5ml_snapcap handles: [] icon: '' init_param_schema: {} @@ -170,6 +185,7 @@ opentrons_24_tuberack_nest_2ml_screwcap: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_tuberack_nest_2ml_screwcap type: pylabrobot description: Opentrons 24 tuberack nest 2ml screwcap + displayname: opentrons_24_tuberack_nest_2ml_screwcap handles: [] icon: '' init_param_schema: {} @@ -181,6 +197,7 @@ opentrons_24_tuberack_nest_2ml_snapcap: module: pylabrobot.resources.opentrons.tube_racks:opentrons_24_tuberack_nest_2ml_snapcap type: pylabrobot description: Opentrons 24 tuberack nest 2ml snapcap + displayname: opentrons_24_tuberack_nest_2ml_snapcap handles: [] icon: '' init_param_schema: {} @@ -192,6 +209,7 @@ opentrons_6_tuberack_falcon_50ml_conical: module: pylabrobot.resources.opentrons.tube_racks:opentrons_6_tuberack_falcon_50ml_conical type: pylabrobot description: Opentrons 6 tuberack falcon 50ml conical + displayname: opentrons_6_tuberack_falcon_50ml_conical handles: [] icon: '' init_param_schema: {} @@ -203,6 +221,7 @@ opentrons_6_tuberack_nest_50ml_conical: module: pylabrobot.resources.opentrons.tube_racks:opentrons_6_tuberack_nest_50ml_conical type: pylabrobot description: Opentrons 6 tuberack nest 50ml conical + displayname: opentrons_6_tuberack_nest_50ml_conical handles: [] icon: '' init_param_schema: {} @@ -214,6 +233,7 @@ opentrons_96_well_aluminum_block: module: pylabrobot.resources.opentrons.tube_racks:opentrons_96_well_aluminum_block type: pylabrobot description: Opentrons 96 well aluminum block + displayname: opentrons_96_well_aluminum_block handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/organic/container.yaml b/unilabos/registry/resources/organic/container.yaml index 240cdf799..dba5142a0 100644 --- a/unilabos/registry/resources/organic/container.yaml +++ b/unilabos/registry/resources/organic/container.yaml @@ -5,6 +5,7 @@ container: module: unilabos.resources.container:get_regular_container type: pylabrobot description: regular organic container + displayname: container handles: - data_key: fluid_in data_source: handle diff --git a/unilabos/registry/resources/post_process/bottle_carriers.yaml b/unilabos/registry/resources/post_process/bottle_carriers.yaml index 45054311a..76f1787b2 100644 --- a/unilabos/registry/resources/post_process/bottle_carriers.yaml +++ b/unilabos/registry/resources/post_process/bottle_carriers.yaml @@ -5,6 +5,7 @@ POST_PROCESS_Raw_1BottleCarrier: module: unilabos.devices.workstation.post_process.bottle_carriers:POST_PROCESS_Raw_1BottleCarrier type: pylabrobot description: POST_PROCESS_Raw_1BottleCarrier + displayname: POST_PROCESS_Raw_1BottleCarrier handles: [] icon: '' init_param_schema: {} @@ -16,6 +17,7 @@ POST_PROCESS_Reaction_1BottleCarrier: module: unilabos.devices.workstation.post_process.bottle_carriers:POST_PROCESS_Reaction_1BottleCarrier type: pylabrobot description: POST_PROCESS_Reaction_1BottleCarrier + displayname: POST_PROCESS_Reaction_1BottleCarrier handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/post_process/bottles.yaml b/unilabos/registry/resources/post_process/bottles.yaml index 25fc39773..dd0e2e7da 100644 --- a/unilabos/registry/resources/post_process/bottles.yaml +++ b/unilabos/registry/resources/post_process/bottles.yaml @@ -4,6 +4,7 @@ POST_PROCESS_PolymerStation_Reagent_Bottle: class: module: unilabos.devices.workstation.post_process.bottles:POST_PROCESS_PolymerStation_Reagent_Bottle type: pylabrobot + displayname: POST_PROCESS_PolymerStation_Reagent_Bottle handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/post_process/deck.yaml b/unilabos/registry/resources/post_process/deck.yaml index e5d4cc8db..af4d6550f 100644 --- a/unilabos/registry/resources/post_process/deck.yaml +++ b/unilabos/registry/resources/post_process/deck.yaml @@ -6,6 +6,7 @@ post_process_deck: module: unilabos.devices.workstation.post_process.decks:post_process_deck type: pylabrobot description: post_process_deck + displayname: post_process_deck handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/prcxi/plate_adapters.yaml b/unilabos/registry/resources/prcxi/plate_adapters.yaml index 3e960f2ee..432265f4e 100644 --- a/unilabos/registry/resources/prcxi/plate_adapters.yaml +++ b/unilabos/registry/resources/prcxi/plate_adapters.yaml @@ -6,6 +6,7 @@ PRCXI_30mm_Adapter: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_30mm_Adapter type: pylabrobot description: '30mm适配器 (Code: ZX-58-30)' + displayname: PRCXI_30mm_Adapter handles: [] icon: '' init_param_schema: {} @@ -18,6 +19,7 @@ PRCXI_Adapter: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Adapter type: pylabrobot description: '适配器 (Code: Fhh478)' + displayname: PRCXI_Adapter handles: [] icon: '' init_param_schema: {} @@ -30,6 +32,7 @@ PRCXI_Deep10_Adapter: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Deep10_Adapter type: pylabrobot description: '10ul专用深孔板适配器 (Code: ZX-002-10)' + displayname: PRCXI_Deep10_Adapter handles: [] icon: '' init_param_schema: {} @@ -42,6 +45,7 @@ PRCXI_Deep300_Adapter: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Deep300_Adapter type: pylabrobot description: '300ul深孔板适配器 (Code: ZX-002-300)' + displayname: PRCXI_Deep300_Adapter handles: [] icon: '' init_param_schema: {} @@ -54,6 +58,7 @@ PRCXI_PCR_Adapter: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Adapter type: pylabrobot description: '全裙边 PCR适配器 (Code: ZX-58-0001)' + displayname: PRCXI_PCR_Adapter handles: [] icon: '' init_param_schema: {} @@ -66,6 +71,7 @@ PRCXI_Reservoir_Adapter: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Reservoir_Adapter type: pylabrobot description: '储液槽 适配器 (Code: ZX-ADP-001)' + displayname: PRCXI_Reservoir_Adapter handles: [] icon: '' init_param_schema: {} @@ -78,6 +84,7 @@ PRCXI_Tip10_Adapter: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Tip10_Adapter type: pylabrobot description: '吸头10ul 适配器 (Code: ZX-58-10)' + displayname: PRCXI_Tip10_Adapter handles: [] icon: '' init_param_schema: {} @@ -90,6 +97,7 @@ PRCXI_Tip1250_Adapter: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Tip1250_Adapter type: pylabrobot description: 'Tip头适配器 1250uL (Code: ZX-58-1250)' + displayname: PRCXI_Tip1250_Adapter handles: [] icon: '' init_param_schema: {} @@ -102,6 +110,7 @@ PRCXI_Tip300_Adapter: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Tip300_Adapter type: pylabrobot description: 'ZHONGXI 适配器 300uL (Code: ZX-58-300)' + displayname: PRCXI_Tip300_Adapter handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/prcxi/plates.yaml b/unilabos/registry/resources/prcxi/plates.yaml index b8527dbfe..ffb694452 100644 --- a/unilabos/registry/resources/prcxi/plates.yaml +++ b/unilabos/registry/resources/prcxi/plates.yaml @@ -6,6 +6,7 @@ PRCXI_48_DeepWell: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_48_DeepWell type: pylabrobot description: '48孔深孔板 (Code: 22)' + displayname: PRCXI_48_DeepWell handles: [] icon: '' init_param_schema: {} @@ -18,6 +19,7 @@ PRCXI_96_DeepWell: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_96_DeepWell type: pylabrobot description: '96深孔板 (Code: q2)' + displayname: PRCXI_96_DeepWell handles: [] icon: '' init_param_schema: {} @@ -30,6 +32,7 @@ PRCXI_AGenBio_4_troughplate: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_AGenBio_4_troughplate type: pylabrobot description: '4道储液槽 (Code: sdfrth654)' + displayname: PRCXI_AGenBio_4_troughplate handles: [] icon: '' init_param_schema: {} @@ -42,6 +45,7 @@ PRCXI_BioER_96_wellplate: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_BioER_96_wellplate type: pylabrobot description: '2.2ml 深孔板 (Code: ZX-019-2.2)' + displayname: PRCXI_BioER_96_wellplate handles: [] icon: '' init_param_schema: {} @@ -54,6 +58,7 @@ PRCXI_BioRad_384_wellplate: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_BioRad_384_wellplate type: pylabrobot description: '384板 (Code: q3)' + displayname: PRCXI_BioRad_384_wellplate handles: [] icon: '' init_param_schema: {} @@ -66,6 +71,7 @@ PRCXI_CellTreat_96_wellplate: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_CellTreat_96_wellplate type: pylabrobot description: '细菌培养皿 (Code: ZX-78-096)' + displayname: PRCXI_CellTreat_96_wellplate handles: [] icon: '' init_param_schema: {} @@ -78,6 +84,7 @@ PRCXI_PCR_Plate_200uL_nonskirted: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Plate_200uL_nonskirted type: pylabrobot description: '0.2ml PCR 板 (Code: ZX-023-0.2)' + displayname: PRCXI_PCR_Plate_200uL_nonskirted handles: [] icon: '' init_param_schema: {} @@ -90,6 +97,7 @@ PRCXI_PCR_Plate_200uL_semiskirted: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Plate_200uL_semiskirted type: pylabrobot description: '0.2ml PCR 板 (Code: ZX-023-0.2)' + displayname: PRCXI_PCR_Plate_200uL_semiskirted handles: [] icon: '' init_param_schema: {} @@ -102,6 +110,7 @@ PRCXI_PCR_Plate_200uL_skirted: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Plate_200uL_skirted type: pylabrobot description: '0.2ml PCR 板 (Code: ZX-023-0.2)' + displayname: PRCXI_PCR_Plate_200uL_skirted handles: [] icon: '' init_param_schema: {} @@ -114,6 +123,7 @@ PRCXI_nest_12_troughplate: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_nest_12_troughplate type: pylabrobot description: '12道储液槽 (Code: 12道储液槽)' + displayname: PRCXI_nest_12_troughplate handles: [] icon: '' init_param_schema: {} @@ -126,6 +136,7 @@ PRCXI_nest_1_troughplate: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_nest_1_troughplate type: pylabrobot description: '储液槽 (Code: ZX-58-10000)' + displayname: PRCXI_nest_1_troughplate handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/prcxi/tip_racks.yaml b/unilabos/registry/resources/prcxi/tip_racks.yaml index f6d2e7f05..8b87359a4 100644 --- a/unilabos/registry/resources/prcxi/tip_racks.yaml +++ b/unilabos/registry/resources/prcxi/tip_racks.yaml @@ -6,6 +6,7 @@ PRCXI_1000uL_Tips: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_1000uL_Tips type: pylabrobot description: '1000μL Tip头 (Code: ZX-001-1000)' + displayname: PRCXI_1000uL_Tips handles: [] icon: '' init_param_schema: {} @@ -18,6 +19,7 @@ PRCXI_10uL_Tips: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_10uL_Tips type: pylabrobot description: '10μL Tip头 (Code: ZX-001-10)' + displayname: PRCXI_10uL_Tips handles: [] icon: '' init_param_schema: {} @@ -30,6 +32,7 @@ PRCXI_10ul_eTips: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_10ul_eTips type: pylabrobot description: '10μL加长 Tip头 (Code: ZX-001-10+)' + displayname: PRCXI_10ul_eTips handles: [] icon: '' init_param_schema: {} @@ -42,6 +45,7 @@ PRCXI_1250uL_Tips: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_1250uL_Tips type: pylabrobot description: '1250μL Tip头 (Code: ZX-001-1250)' + displayname: PRCXI_1250uL_Tips handles: [] icon: '' init_param_schema: {} @@ -54,6 +58,7 @@ PRCXI_200uL_Tips: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_200uL_Tips type: pylabrobot description: '200μL Tip头 (Code: ZX-001-200)' + displayname: PRCXI_200uL_Tips handles: [] icon: '' init_param_schema: {} @@ -66,6 +71,7 @@ PRCXI_300ul_Tips: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_300ul_Tips type: pylabrobot description: '300μL Tip头 (Code: ZX-001-300)' + displayname: PRCXI_300ul_Tips handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/prcxi/trash.yaml b/unilabos/registry/resources/prcxi/trash.yaml index 952a832b4..3b0673c08 100644 --- a/unilabos/registry/resources/prcxi/trash.yaml +++ b/unilabos/registry/resources/prcxi/trash.yaml @@ -6,6 +6,7 @@ PRCXI_trash: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_trash type: pylabrobot description: '废弃槽 (Code: q1)' + displayname: PRCXI_trash handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/prcxi/tube_racks.yaml b/unilabos/registry/resources/prcxi/tube_racks.yaml index 6510c16cd..e750baab4 100644 --- a/unilabos/registry/resources/prcxi/tube_racks.yaml +++ b/unilabos/registry/resources/prcxi/tube_racks.yaml @@ -6,6 +6,7 @@ PRCXI_EP_Adapter: module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_EP_Adapter type: pylabrobot description: 'ep适配器 (Code: 1)' + displayname: PRCXI_EP_Adapter handles: [] icon: '' init_param_schema: {}