mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-19 14:01:20 +00:00
Compare commits
5 Commits
7b8638aa03
...
v0.9.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b420d1fa8e | ||
|
|
767e0fcdee | ||
|
|
84944396e9 | ||
|
|
bfcb214b53 | ||
|
|
ec4e6c6cfd |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ __pycache__/
|
|||||||
.vscode
|
.vscode
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
service
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -4,83 +4,86 @@
|
|||||||
|
|
||||||
# Uni-Lab-OS
|
# Uni-Lab-OS
|
||||||
|
|
||||||
|
<!-- Language switcher -->
|
||||||
|
**English** | [中文](README_zh.md)
|
||||||
|
|
||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/network/members)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/network/members)
|
||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
|
||||||
|
|
||||||
Uni-Lab 操作系统是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
Uni-Lab Operating System is a platform for laboratory automation, designed to connect and control various experimental equipment, enabling automation and standardization of experimental workflows.
|
||||||
|
|
||||||
## 核心特点
|
## Key Features
|
||||||
|
|
||||||
- 多设备集成管理
|
- Multi-device integration management
|
||||||
- 自动化实验流程
|
- Automated experimental workflows
|
||||||
- 云端连接能力
|
- Cloud connectivity capabilities
|
||||||
- 灵活的配置系统
|
- Flexible configuration system
|
||||||
- 支持多种实验协议
|
- Support for multiple experimental protocols
|
||||||
|
|
||||||
## 文档
|
## Documentation
|
||||||
|
|
||||||
详细文档可在以下位置找到:
|
Detailed documentation can be found at:
|
||||||
|
|
||||||
- [在线文档](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
- [Online Documentation](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
||||||
|
|
||||||
## 快速开始
|
## Quick Start
|
||||||
|
|
||||||
1. 配置Conda环境
|
1. Configure Conda Environment
|
||||||
|
|
||||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
Uni-Lab-OS recommends using `mamba` for environment management. Choose the appropriate environment file for your operating system:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建新环境
|
# Create new environment
|
||||||
mamba env create -f unilabos-[YOUR_OS].yaml
|
mamba env create -f unilabos-[YOUR_OS].yaml
|
||||||
mamba activate unilab
|
mamba activate unilab
|
||||||
|
|
||||||
# 或更新现有环境
|
# Or update existing environment
|
||||||
# 其中 `[YOUR_OS]` 可以是 `win64`, `linux-64`, `osx-64`, 或 `osx-arm64`。
|
# Where `[YOUR_OS]` can be `win64`, `linux-64`, `osx-64`, or `osx-arm64`.
|
||||||
conda env update --file unilabos-[YOUR_OS].yml -n 环境名
|
conda env update --file unilabos-[YOUR_OS].yml -n environment_name
|
||||||
|
|
||||||
# 现阶段,需要安装 `unilabos_msgs` 包
|
# Currently, you need to install the `unilabos_msgs` package
|
||||||
# 可以前往 Release 页面下载系统对应的包进行安装
|
# You can download the system-specific package from the Release page
|
||||||
conda install ros-humble-unilabos-msgs-0.9.0-xxxxx.tar.bz2
|
conda install ros-humble-unilabos-msgs-0.9.1-xxxxx.tar.bz2
|
||||||
|
|
||||||
# 安装PyLabRobot等前置
|
# Install PyLabRobot and other prerequisites
|
||||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||||
cd plr_repo
|
cd plr_repo
|
||||||
pip install .[opentrons]
|
pip install .[opentrons]
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 安装 Uni-Lab-OS:
|
2. Install Uni-Lab-OS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆仓库
|
# Clone the repository
|
||||||
git clone https://github.com/dptech-corp/Uni-Lab-OS.git
|
git clone https://github.com/dptech-corp/Uni-Lab-OS.git
|
||||||
cd Uni-Lab-OS
|
cd Uni-Lab-OS
|
||||||
|
|
||||||
# 安装 Uni-Lab-OS
|
# Install Uni-Lab-OS
|
||||||
pip install .
|
pip install .
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 启动 Uni-Lab 系统:
|
3. Start Uni-Lab System:
|
||||||
|
|
||||||
请见[文档-启动样例](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
Please refer to [Documentation - Boot Examples](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
||||||
|
|
||||||
## 消息格式
|
## Message Format
|
||||||
|
|
||||||
Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在 [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) 页面找到已构建的版本。
|
Uni-Lab-OS uses pre-built `unilabos_msgs` for system communication. You can find the built versions on the [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) page.
|
||||||
|
|
||||||
## 许可证
|
## License
|
||||||
|
|
||||||
此项目采用 GPL-3.0 许可 - 详情请参阅 [LICENSE](LICENSE) 文件。
|
This project is licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
## 项目统计
|
## Project Statistics
|
||||||
|
|
||||||
### Stars 趋势
|
### Stars Trend
|
||||||
|
|
||||||
<a href="https://star-history.com/#dptech-corp/Uni-Lab-OS&Date">
|
<a href="https://star-history.com/#dptech-corp/Uni-Lab-OS&Date">
|
||||||
<img src="https://api.star-history.com/svg?repos=dptech-corp/Uni-Lab-OS&type=Date" alt="Star History Chart" width="600">
|
<img src="https://api.star-history.com/svg?repos=dptech-corp/Uni-Lab-OS&type=Date" alt="Star History Chart" width="600">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## 联系我们
|
## Contact Us
|
||||||
|
|
||||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
89
README_zh.md
Normal file
89
README_zh.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="docs/logo.png" alt="Uni-Lab Logo" width="200"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# Uni-Lab-OS
|
||||||
|
|
||||||
|
<!-- Language switcher -->
|
||||||
|
[English](README.md) | **中文**
|
||||||
|
|
||||||
|
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
||||||
|
[](https://github.com/dptech-corp/Uni-Lab-OS/network/members)
|
||||||
|
[](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
|
[](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
|
||||||
|
|
||||||
|
Uni-Lab 操作系统是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
||||||
|
|
||||||
|
## 核心特点
|
||||||
|
|
||||||
|
- 多设备集成管理
|
||||||
|
- 自动化实验流程
|
||||||
|
- 云端连接能力
|
||||||
|
- 灵活的配置系统
|
||||||
|
- 支持多种实验协议
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
详细文档可在以下位置找到:
|
||||||
|
|
||||||
|
- [在线文档](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
1. 配置Conda环境
|
||||||
|
|
||||||
|
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建新环境
|
||||||
|
mamba env create -f unilabos-[YOUR_OS].yaml
|
||||||
|
mamba activate unilab
|
||||||
|
|
||||||
|
# 或更新现有环境
|
||||||
|
# 其中 `[YOUR_OS]` 可以是 `win64`, `linux-64`, `osx-64`, 或 `osx-arm64`。
|
||||||
|
conda env update --file unilabos-[YOUR_OS].yml -n 环境名
|
||||||
|
|
||||||
|
# 现阶段,需要安装 `unilabos_msgs` 包
|
||||||
|
# 可以前往 Release 页面下载系统对应的包进行安装
|
||||||
|
conda install ros-humble-unilabos-msgs-0.9.1-xxxxx.tar.bz2
|
||||||
|
|
||||||
|
# 安装PyLabRobot等前置
|
||||||
|
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||||
|
cd plr_repo
|
||||||
|
pip install .[opentrons]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 安装 Uni-Lab-OS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆仓库
|
||||||
|
git clone https://github.com/dptech-corp/Uni-Lab-OS.git
|
||||||
|
cd Uni-Lab-OS
|
||||||
|
|
||||||
|
# 安装 Uni-Lab-OS
|
||||||
|
pip install .
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 启动 Uni-Lab 系统:
|
||||||
|
|
||||||
|
请见[文档-启动样例](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
||||||
|
|
||||||
|
## 消息格式
|
||||||
|
|
||||||
|
Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在 [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) 页面找到已构建的版本。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
此项目采用 GPL-3.0 许可 - 详情请参阅 [LICENSE](LICENSE) 文件。
|
||||||
|
|
||||||
|
## 项目统计
|
||||||
|
|
||||||
|
### Stars 趋势
|
||||||
|
|
||||||
|
<a href="https://star-history.com/#dptech-corp/Uni-Lab-OS&Date">
|
||||||
|
<img src="https://api.star-history.com/svg?repos=dptech-corp/Uni-Lab-OS&type=Date" alt="Star History Chart" width="600">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## 联系我们
|
||||||
|
|
||||||
|
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: ros-humble-unilabos-msgs
|
name: ros-humble-unilabos-msgs
|
||||||
version: 0.9.0
|
version: 0.9.1
|
||||||
source:
|
source:
|
||||||
path: ../../unilabos_msgs
|
path: ../../unilabos_msgs
|
||||||
folder: ros-humble-unilabos-msgs/src/work
|
folder: ros-humble-unilabos-msgs/src/work
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: unilabos
|
name: unilabos
|
||||||
version: "0.9.0"
|
version: "0.9.1"
|
||||||
|
|
||||||
source:
|
source:
|
||||||
path: ../..
|
path: ../..
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name=package_name,
|
name=package_name,
|
||||||
version='0.9.0',
|
version='0.9.1',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=['setuptools'],
|
install_requires=['setuptools'],
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ def job_add(req: JobAddReq) -> JobData:
|
|||||||
action_kwargs = {"command": json.dumps(action_kwargs)}
|
action_kwargs = {"command": json.dumps(action_kwargs)}
|
||||||
elif "command" in action_kwargs:
|
elif "command" in action_kwargs:
|
||||||
action_kwargs = action_kwargs["command"]
|
action_kwargs = action_kwargs["command"]
|
||||||
print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
|
# print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
|
||||||
HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id)
|
HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id, server_info=req.server_info)
|
||||||
return JobData(jobId=req.job_id)
|
return JobData(jobId=req.job_id)
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ def main():
|
|||||||
args_dict["resources_mesh_config"] = resource_visualization.resource_model
|
args_dict["resources_mesh_config"] = resource_visualization.resource_model
|
||||||
start_backend(**args_dict)
|
start_backend(**args_dict)
|
||||||
server_thread = threading.Thread(target=start_server, kwargs=dict(
|
server_thread = threading.Thread(target=start_server, kwargs=dict(
|
||||||
open_browser=not args_dict["disable_browser"]
|
open_browser=not args_dict["disable_browser"], port=args_dict["port"],
|
||||||
))
|
))
|
||||||
server_thread.start()
|
server_thread.start()
|
||||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||||
@@ -201,10 +201,10 @@ def main():
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
else:
|
||||||
start_backend(**args_dict)
|
start_backend(**args_dict)
|
||||||
start_server(open_browser=not args_dict["disable_browser"])
|
start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],)
|
||||||
else:
|
else:
|
||||||
start_backend(**args_dict)
|
start_backend(**args_dict)
|
||||||
start_server(open_browser=not args_dict["disable_browser"])
|
start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ class Resp(BaseModel):
|
|||||||
class JobAddReq(BaseModel):
|
class JobAddReq(BaseModel):
|
||||||
device_id: str = Field(examples=["Gripper"], description="device id")
|
device_id: str = Field(examples=["Gripper"], description="device id")
|
||||||
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}])
|
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}])
|
||||||
job_id: str = Field(examples=["sfsfsfeq"], description="goal uuid")
|
job_id: str = Field(examples=["job_id"], description="goal uuid")
|
||||||
node_id: str = Field(examples=["sfsfsfeq"], description="node uuid")
|
node_id: str = Field(examples=["node_id"], description="node uuid")
|
||||||
|
server_info: dict = Field(examples=[{"send_timestamp": 1717000000.0}], description="server info")
|
||||||
|
|
||||||
|
|
||||||
class JobStepFinishReq(BaseModel):
|
class JobStepFinishReq(BaseModel):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import tempfile
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from unilabos.config.config import MQConfig
|
from unilabos.config.config import MQConfig
|
||||||
from unilabos.app.controler import devices, job_add
|
from unilabos.app.controler import job_add
|
||||||
from unilabos.app.model import JobAddReq
|
from unilabos.app.model import JobAddReq
|
||||||
from unilabos.utils import logger
|
from unilabos.utils import logger
|
||||||
from unilabos.utils.type_check import TypeEncoder
|
from unilabos.utils.type_check import TypeEncoder
|
||||||
@@ -43,13 +43,10 @@ class MQTTClient:
|
|||||||
def _on_connect(self, client, userdata, flags, rc, properties=None):
|
def _on_connect(self, client, userdata, flags, rc, properties=None):
|
||||||
logger.info("[MQTT] Connected with result code " + str(rc))
|
logger.info("[MQTT] Connected with result code " + str(rc))
|
||||||
client.subscribe(f"labs/{MQConfig.lab_id}/job/start/", 0)
|
client.subscribe(f"labs/{MQConfig.lab_id}/job/start/", 0)
|
||||||
isok, data = devices()
|
client.subscribe(f"labs/{MQConfig.lab_id}/pong/", 0)
|
||||||
if not isok:
|
|
||||||
logger.error("[MQTT] on_connect ErrorHostNotInit")
|
|
||||||
return
|
|
||||||
|
|
||||||
def _on_message(self, client, userdata, msg) -> None:
|
def _on_message(self, client, userdata, msg) -> None:
|
||||||
logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
|
# logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
|
||||||
try:
|
try:
|
||||||
payload_str = msg.payload.decode("utf-8")
|
payload_str = msg.payload.decode("utf-8")
|
||||||
payload_json = json.loads(payload_str)
|
payload_json = json.loads(payload_str)
|
||||||
@@ -63,6 +60,14 @@ class MQTTClient:
|
|||||||
job_req = JobAddReq.model_validate(payload_json)
|
job_req = JobAddReq.model_validate(payload_json)
|
||||||
data = job_add(job_req)
|
data = job_add(job_req)
|
||||||
return
|
return
|
||||||
|
elif msg.topic == f"labs/{MQConfig.lab_id}/pong/":
|
||||||
|
# 处理pong响应,通知HostNode
|
||||||
|
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||||
|
|
||||||
|
host_instance = HostNode.get_instance(0)
|
||||||
|
if host_instance:
|
||||||
|
host_instance.handle_pong_response(payload_json)
|
||||||
|
return
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.error(f"[MQTT] JSON 解析错误: {e}")
|
logger.error(f"[MQTT] JSON 解析错误: {e}")
|
||||||
@@ -179,6 +184,28 @@ class MQTTClient:
|
|||||||
self.client.publish(address, json.dumps(action_info), qos=2)
|
self.client.publish(address, json.dumps(action_info), qos=2)
|
||||||
logger.debug(f"Action data published: address: {address}, {action_id}, {action_info}")
|
logger.debug(f"Action data published: address: {address}, {action_id}, {action_info}")
|
||||||
|
|
||||||
|
def send_ping(self, ping_id: str, timestamp: float):
|
||||||
|
"""发送ping消息到服务端"""
|
||||||
|
if self.mqtt_disable:
|
||||||
|
return
|
||||||
|
address = f"labs/{MQConfig.lab_id}/ping/"
|
||||||
|
ping_data = {"ping_id": ping_id, "client_timestamp": timestamp, "type": "ping"}
|
||||||
|
self.client.publish(address, json.dumps(ping_data), qos=2)
|
||||||
|
|
||||||
|
def setup_pong_subscription(self):
|
||||||
|
"""设置pong消息订阅"""
|
||||||
|
if self.mqtt_disable:
|
||||||
|
return
|
||||||
|
pong_topic = f"labs/{MQConfig.lab_id}/pong/"
|
||||||
|
self.client.subscribe(pong_topic, 0)
|
||||||
|
logger.debug(f"Subscribed to pong topic: {pong_topic}")
|
||||||
|
|
||||||
|
def handle_pong(self, pong_data: dict):
|
||||||
|
"""处理pong响应(这个方法会在收到pong消息时被调用)"""
|
||||||
|
logger.debug(f"Pong received: {pong_data}")
|
||||||
|
# 这里会被HostNode的ping-pong处理逻辑调用
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
mqtt_client = MQTTClient()
|
mqtt_client = MQTTClient()
|
||||||
|
|
||||||
|
|||||||
@@ -25,58 +25,10 @@ class Registry:
|
|||||||
self.ResourceCreateFromOuterEasy = self._replace_type_with_class(
|
self.ResourceCreateFromOuterEasy = self._replace_type_with_class(
|
||||||
"ResourceCreateFromOuterEasy", "host_node", f"动作 create_resource"
|
"ResourceCreateFromOuterEasy", "host_node", f"动作 create_resource"
|
||||||
)
|
)
|
||||||
self.device_type_registry = {
|
self.EmptyIn = self._replace_type_with_class(
|
||||||
"host_node": {
|
"EmptyIn", "host_node", f""
|
||||||
"description": "UniLabOS主机节点",
|
)
|
||||||
"class": {
|
self.device_type_registry = {}
|
||||||
"module": "unilabos.ros.nodes.presets.host_node",
|
|
||||||
"type": "python",
|
|
||||||
"status_types": {},
|
|
||||||
"action_value_mappings": {
|
|
||||||
"create_resource_detailed": {
|
|
||||||
"type": msg_converter_manager.search_class("ResourceCreateFromOuter"),
|
|
||||||
"goal": {
|
|
||||||
"resources": "resources",
|
|
||||||
"device_ids": "device_ids",
|
|
||||||
"bind_parent_ids": "bind_parent_ids",
|
|
||||||
"bind_locations": "bind_locations",
|
|
||||||
"other_calling_params": "other_calling_params",
|
|
||||||
},
|
|
||||||
"feedback": {},
|
|
||||||
"result": {
|
|
||||||
"success": "success"
|
|
||||||
},
|
|
||||||
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuter)
|
|
||||||
},
|
|
||||||
"create_resource": {
|
|
||||||
"type": msg_converter_manager.search_class("ResourceCreateFromOuterEasy"),
|
|
||||||
"goal": {
|
|
||||||
"res_id": "res_id",
|
|
||||||
"class_name": "class_name",
|
|
||||||
"parent": "parent",
|
|
||||||
"device_id": "device_id",
|
|
||||||
"bind_locations": "bind_locations",
|
|
||||||
"liquid_input_slot": "liquid_input_slot[]",
|
|
||||||
"liquid_type": "liquid_type[]",
|
|
||||||
"liquid_volume": "liquid_volume[]",
|
|
||||||
"slot_on_deck": "slot_on_deck",
|
|
||||||
},
|
|
||||||
"feedback": {},
|
|
||||||
"result": {
|
|
||||||
"success": "success"
|
|
||||||
},
|
|
||||||
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuterEasy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schema": {
|
|
||||||
"properties": {},
|
|
||||||
"additionalProperties": False,
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"file_path": "/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.resource_type_registry = {}
|
self.resource_type_registry = {}
|
||||||
self._setup_called = False # 跟踪setup是否已调用
|
self._setup_called = False # 跟踪setup是否已调用
|
||||||
# 其他状态变量
|
# 其他状态变量
|
||||||
@@ -88,9 +40,70 @@ class Registry:
|
|||||||
logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用")
|
logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 标记setup已被调用
|
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
|
||||||
self._setup_called = True
|
|
||||||
|
|
||||||
|
self.device_type_registry.update(
|
||||||
|
{
|
||||||
|
"host_node": {
|
||||||
|
"description": "UniLabOS主机节点",
|
||||||
|
"class": {
|
||||||
|
"module": "unilabos.ros.nodes.presets.host_node",
|
||||||
|
"type": "python",
|
||||||
|
"status_types": {},
|
||||||
|
"action_value_mappings": {
|
||||||
|
"create_resource_detailed": {
|
||||||
|
"type": self.ResourceCreateFromOuter,
|
||||||
|
"goal": {
|
||||||
|
"resources": "resources",
|
||||||
|
"device_ids": "device_ids",
|
||||||
|
"bind_parent_ids": "bind_parent_ids",
|
||||||
|
"bind_locations": "bind_locations",
|
||||||
|
"other_calling_params": "other_calling_params",
|
||||||
|
},
|
||||||
|
"feedback": {},
|
||||||
|
"result": {"success": "success"},
|
||||||
|
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuter),
|
||||||
|
"goal_default": yaml.safe_load(
|
||||||
|
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuter.Goal))
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"create_resource": {
|
||||||
|
"type": self.ResourceCreateFromOuterEasy,
|
||||||
|
"goal": {
|
||||||
|
"res_id": "res_id",
|
||||||
|
"class_name": "class_name",
|
||||||
|
"parent": "parent",
|
||||||
|
"device_id": "device_id",
|
||||||
|
"bind_locations": "bind_locations",
|
||||||
|
"liquid_input_slot": "liquid_input_slot[]",
|
||||||
|
"liquid_type": "liquid_type[]",
|
||||||
|
"liquid_volume": "liquid_volume[]",
|
||||||
|
"slot_on_deck": "slot_on_deck",
|
||||||
|
},
|
||||||
|
"feedback": {},
|
||||||
|
"result": {"success": "success"},
|
||||||
|
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuterEasy),
|
||||||
|
"goal_default": yaml.safe_load(
|
||||||
|
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuterEasy.Goal))
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"test_latency": {
|
||||||
|
"type": self.EmptyIn,
|
||||||
|
"goal": {},
|
||||||
|
"feedback": {},
|
||||||
|
"result": {"latency_ms": "latency_ms", "time_diff_ms": "time_diff_ms"},
|
||||||
|
"schema": ros_action_to_json_schema(self.EmptyIn),
|
||||||
|
"goal_default": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"icon": "icon_device.webp",
|
||||||
|
"registry_type": "device",
|
||||||
|
"schema": {"properties": {}, "additionalProperties": False, "type": "object"},
|
||||||
|
"file_path": "/",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
logger.debug(f"[UniLab Registry] ----------Setup----------")
|
logger.debug(f"[UniLab Registry] ----------Setup----------")
|
||||||
self.registry_paths = [Path(path).absolute() for path in self.registry_paths]
|
self.registry_paths = [Path(path).absolute() for path in self.registry_paths]
|
||||||
for i, path in enumerate(self.registry_paths):
|
for i, path in enumerate(self.registry_paths):
|
||||||
@@ -100,6 +113,8 @@ class Registry:
|
|||||||
self.load_device_types(path)
|
self.load_device_types(path)
|
||||||
self.load_resource_types(path)
|
self.load_resource_types(path)
|
||||||
logger.info("[UniLab Registry] 注册表设置完成")
|
logger.info("[UniLab Registry] 注册表设置完成")
|
||||||
|
# 标记setup已被调用
|
||||||
|
self._setup_called = True
|
||||||
|
|
||||||
def load_resource_types(self, path: os.PathLike):
|
def load_resource_types(self, path: os.PathLike):
|
||||||
abs_path = Path(path).absolute()
|
abs_path = Path(path).absolute()
|
||||||
@@ -115,6 +130,9 @@ class Registry:
|
|||||||
resource_info["file_path"] = str(file.absolute()).replace("\\", "/")
|
resource_info["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||||
if "description" not in resource_info:
|
if "description" not in resource_info:
|
||||||
resource_info["description"] = ""
|
resource_info["description"] = ""
|
||||||
|
if "icon" not in resource_info:
|
||||||
|
resource_info["icon"] = ""
|
||||||
|
resource_info["registry_type"] = "resource"
|
||||||
self.resource_type_registry.update(data)
|
self.resource_type_registry.update(data)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(files)} "
|
f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(files)} "
|
||||||
@@ -164,6 +182,7 @@ class Registry:
|
|||||||
)
|
)
|
||||||
current_device_number = len(self.device_type_registry) + 1
|
current_device_number = len(self.device_type_registry) + 1
|
||||||
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
|
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
|
||||||
|
|
||||||
for i, file in enumerate(files):
|
for i, file in enumerate(files):
|
||||||
data = yaml.safe_load(open(file, encoding="utf-8"))
|
data = yaml.safe_load(open(file, encoding="utf-8"))
|
||||||
if data:
|
if data:
|
||||||
@@ -173,6 +192,9 @@ class Registry:
|
|||||||
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
|
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||||
if "description" not in device_config:
|
if "description" not in device_config:
|
||||||
device_config["description"] = ""
|
device_config["description"] = ""
|
||||||
|
if "icon" not in device_config:
|
||||||
|
device_config["icon"] = ""
|
||||||
|
device_config["registry_type"] = "device"
|
||||||
if "class" in device_config:
|
if "class" in device_config:
|
||||||
# 处理状态类型
|
# 处理状态类型
|
||||||
if "status_types" in device_config["class"]:
|
if "status_types" in device_config["class"]:
|
||||||
@@ -189,7 +211,9 @@ class Registry:
|
|||||||
action_config["type"], device_id, f"动作 {action_name}"
|
action_config["type"], device_id, f"动作 {action_name}"
|
||||||
)
|
)
|
||||||
if action_config["type"] is not None:
|
if action_config["type"] is not None:
|
||||||
action_config["goal_default"] = yaml.safe_load(io.StringIO(get_yaml_from_goal_type(action_config["type"].Goal)))
|
action_config["goal_default"] = yaml.safe_load(
|
||||||
|
io.StringIO(get_yaml_from_goal_type(action_config["type"].Goal))
|
||||||
|
)
|
||||||
action_config["schema"] = ros_action_to_json_schema(action_config["type"])
|
action_config["schema"] = ros_action_to_json_schema(action_config["type"])
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -212,13 +236,17 @@ class Registry:
|
|||||||
def obtain_registry_device_info(self):
|
def obtain_registry_device_info(self):
|
||||||
devices = []
|
devices = []
|
||||||
for device_id, device_info in self.device_type_registry.items():
|
for device_id, device_info in self.device_type_registry.items():
|
||||||
msg = {
|
msg = {"id": device_id, **device_info}
|
||||||
"id": device_id,
|
|
||||||
**device_info
|
|
||||||
}
|
|
||||||
devices.append(msg)
|
devices.append(msg)
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
|
def obtain_registry_resource_info(self):
|
||||||
|
resources = []
|
||||||
|
for resource_id, resource_info in self.resource_type_registry.items():
|
||||||
|
msg = {"id": resource_id, **resource_info}
|
||||||
|
resources.append(msg)
|
||||||
|
return resources
|
||||||
|
|
||||||
|
|
||||||
# 全局单例实例
|
# 全局单例实例
|
||||||
lab_registry = Registry()
|
lab_registry = Registry()
|
||||||
|
|||||||
@@ -12,8 +12,14 @@ from rclpy.action import ActionClient, get_action_server_names_and_types_by_node
|
|||||||
from rclpy.callback_groups import ReentrantCallbackGroup
|
from rclpy.callback_groups import ReentrantCallbackGroup
|
||||||
from rclpy.service import Service
|
from rclpy.service import Service
|
||||||
from unilabos_msgs.msg import Resource # type: ignore
|
from unilabos_msgs.msg import Resource # type: ignore
|
||||||
from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, \
|
from unilabos_msgs.srv import (
|
||||||
SerialCommand # type: ignore
|
ResourceAdd,
|
||||||
|
ResourceGet,
|
||||||
|
ResourceDelete,
|
||||||
|
ResourceUpdate,
|
||||||
|
ResourceList,
|
||||||
|
SerialCommand,
|
||||||
|
) # type: ignore
|
||||||
from unique_identifier_msgs.msg import UUID
|
from unique_identifier_msgs.msg import UUID
|
||||||
|
|
||||||
from unilabos.registry.registry import lab_registry
|
from unilabos.registry.registry import lab_registry
|
||||||
@@ -87,6 +93,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self.__class__._instance = self
|
self.__class__._instance = self
|
||||||
|
|
||||||
# 初始化配置
|
# 初始化配置
|
||||||
|
self.server_latest_timestamp = 0.0 #
|
||||||
self.devices_config = devices_config
|
self.devices_config = devices_config
|
||||||
self.resources_config = resources_config
|
self.resources_config = resources_config
|
||||||
self.physical_setup_graph = physical_setup_graph
|
self.physical_setup_graph = physical_setup_graph
|
||||||
@@ -100,16 +107,32 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
# 创建设备、动作客户端和目标存储
|
# 创建设备、动作客户端和目标存储
|
||||||
self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射
|
self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射
|
||||||
self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例
|
self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例
|
||||||
self.device_machine_names: Dict[str, str] = {device_id: "本地", } # 存储设备ID到机器名称的映射
|
self.device_machine_names: Dict[str, str] = {
|
||||||
|
device_id: "本地",
|
||||||
|
} # 存储设备ID到机器名称的映射
|
||||||
self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型,host的默认写好
|
self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型,host的默认写好
|
||||||
"/devices/host_node/create_resource": ActionClient(
|
"/devices/host_node/create_resource": ActionClient(
|
||||||
self, lab_registry.ResourceCreateFromOuterEasy, "/devices/host_node/create_resource", callback_group=self.callback_group
|
self,
|
||||||
|
lab_registry.ResourceCreateFromOuterEasy,
|
||||||
|
"/devices/host_node/create_resource",
|
||||||
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
"/devices/host_node/create_resource_detailed": ActionClient(
|
"/devices/host_node/create_resource_detailed": ActionClient(
|
||||||
self, lab_registry.ResourceCreateFromOuter, "/devices/host_node/create_resource_detailed", callback_group=self.callback_group
|
self,
|
||||||
)
|
lab_registry.ResourceCreateFromOuter,
|
||||||
|
"/devices/host_node/create_resource_detailed",
|
||||||
|
callback_group=self.callback_group,
|
||||||
|
),
|
||||||
|
"/devices/host_node/test_latency": ActionClient(
|
||||||
|
self,
|
||||||
|
lab_registry.EmptyIn,
|
||||||
|
"/devices/host_node/test_latency",
|
||||||
|
callback_group=self.callback_group,
|
||||||
|
),
|
||||||
} # 用来存储多个ActionClient实例
|
} # 用来存储多个ActionClient实例
|
||||||
self._action_value_mappings: Dict[str, Dict] = {} # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系
|
self._action_value_mappings: Dict[str, Dict] = (
|
||||||
|
{}
|
||||||
|
) # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系
|
||||||
self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
|
self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
|
||||||
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
|
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
|
||||||
self._last_discovery_time = 0.0 # 上次设备发现的时间
|
self._last_discovery_time = 0.0 # 上次设备发现的时间
|
||||||
@@ -123,8 +146,11 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self.device_status_timestamps = {} # 用来存储设备状态最后更新时间
|
self.device_status_timestamps = {} # 用来存储设备状态最后更新时间
|
||||||
|
|
||||||
from unilabos.app.mq import mqtt_client
|
from unilabos.app.mq import mqtt_client
|
||||||
for device_config in lab_registry.obtain_registry_device_info():
|
|
||||||
mqtt_client.publish_registry(device_config["id"], device_config)
|
for device_info in lab_registry.obtain_registry_device_info():
|
||||||
|
mqtt_client.publish_registry(device_info["id"], device_info)
|
||||||
|
for resource_info in lab_registry.obtain_registry_resource_info():
|
||||||
|
mqtt_client.publish_registry(resource_info["id"], resource_info)
|
||||||
|
|
||||||
# 首次发现网络中的设备
|
# 首次发现网络中的设备
|
||||||
self._discover_devices()
|
self._discover_devices()
|
||||||
@@ -149,21 +175,20 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
].items():
|
].items():
|
||||||
controller_config["update_rate"] = update_rate
|
controller_config["update_rate"] = update_rate
|
||||||
self.initialize_controller(controller_id, controller_config)
|
self.initialize_controller(controller_id, controller_config)
|
||||||
resources_config.insert(0, {
|
resources_config.insert(
|
||||||
"id": "host_node",
|
0,
|
||||||
"name": "host_node",
|
{
|
||||||
"parent": None,
|
"id": "host_node",
|
||||||
"type": "device",
|
"name": "host_node",
|
||||||
"class": "host_node",
|
"parent": None,
|
||||||
"position": {
|
"type": "device",
|
||||||
"x": 0,
|
"class": "host_node",
|
||||||
"y": 0,
|
"position": {"x": 0, "y": 0, "z": 0},
|
||||||
"z": 0
|
"config": {},
|
||||||
|
"data": {},
|
||||||
|
"children": [],
|
||||||
},
|
},
|
||||||
"config": {},
|
)
|
||||||
"data": {},
|
|
||||||
"children": []
|
|
||||||
})
|
|
||||||
resource_with_parent_name = []
|
resource_with_parent_name = []
|
||||||
resource_ids_to_instance = {i["id"]: i for i in resources_config}
|
resource_ids_to_instance = {i["id"]: i for i in resources_config}
|
||||||
for res in resources_config:
|
for res in resources_config:
|
||||||
@@ -189,6 +214,10 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup()
|
discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 添加ping-pong相关属性
|
||||||
|
self._ping_responses = {} # 存储ping响应
|
||||||
|
self._ping_lock = threading.Lock()
|
||||||
|
|
||||||
self.lab_logger().info("[Host Node] Host node initialized.")
|
self.lab_logger().info("[Host Node] Host node initialized.")
|
||||||
HostNode._ready_event.set()
|
HostNode._ready_event.set()
|
||||||
|
|
||||||
@@ -233,7 +262,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
target=self._send_re_register,
|
target=self._send_re_register,
|
||||||
args=(sclient,),
|
args=(sclient,),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}"
|
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}",
|
||||||
).start()
|
).start()
|
||||||
elif device_key not in self._online_devices:
|
elif device_key not in self._online_devices:
|
||||||
# 设备重新上线
|
# 设备重新上线
|
||||||
@@ -244,7 +273,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
target=self._send_re_register,
|
target=self._send_re_register,
|
||||||
args=(sclient,),
|
args=(sclient,),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}"
|
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}",
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
# 检测离线设备
|
# 检测离线设备
|
||||||
@@ -288,7 +317,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self, action_type, action_id, callback_group=self.callback_group
|
self, action_type, action_id, callback_group=self.callback_group
|
||||||
)
|
)
|
||||||
self.lab_logger().debug(f"[Host Node] Created ActionClient (Discovery): {action_id}")
|
self.lab_logger().debug(f"[Host Node] Created ActionClient (Discovery): {action_id}")
|
||||||
action_name = action_id[len(namespace) + 1:]
|
action_name = action_id[len(namespace) + 1 :]
|
||||||
edge_device_id = namespace[9:]
|
edge_device_id = namespace[9:]
|
||||||
# from unilabos.app.mq import mqtt_client
|
# from unilabos.app.mq import mqtt_client
|
||||||
# info_with_schema = ros_action_to_json_schema(action_type)
|
# info_with_schema = ros_action_to_json_schema(action_type)
|
||||||
@@ -301,54 +330,83 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.lab_logger().error(f"[Host Node] Failed to create ActionClient for {action_id}: {str(e)}")
|
self.lab_logger().error(f"[Host Node] Failed to create ActionClient for {action_id}: {str(e)}")
|
||||||
|
|
||||||
def create_resource_detailed(self, resources: list["Resource"], device_ids: list[str], bind_parent_ids: list[str], bind_locations: list[Point], other_calling_params: list[str]):
|
def create_resource_detailed(
|
||||||
for resource, device_id, bind_parent_id, bind_location, other_calling_param in zip(resources, device_ids, bind_parent_ids, bind_locations, other_calling_params):
|
self,
|
||||||
|
resources: list["Resource"],
|
||||||
|
device_ids: list[str],
|
||||||
|
bind_parent_ids: list[str],
|
||||||
|
bind_locations: list[Point],
|
||||||
|
other_calling_params: list[str],
|
||||||
|
):
|
||||||
|
for resource, device_id, bind_parent_id, bind_location, other_calling_param in zip(
|
||||||
|
resources, device_ids, bind_parent_ids, bind_locations, other_calling_params
|
||||||
|
):
|
||||||
# 这里要求device_id传入必须是edge_device_id
|
# 这里要求device_id传入必须是edge_device_id
|
||||||
namespace = "/devices/" + device_id
|
namespace = "/devices/" + device_id
|
||||||
srv_address = f"/srv{namespace}/append_resource"
|
srv_address = f"/srv{namespace}/append_resource"
|
||||||
sclient = self.create_client(SerialCommand, srv_address)
|
sclient = self.create_client(SerialCommand, srv_address)
|
||||||
sclient.wait_for_service()
|
sclient.wait_for_service()
|
||||||
request = SerialCommand.Request()
|
request = SerialCommand.Request()
|
||||||
request.command = json.dumps({
|
request.command = json.dumps(
|
||||||
"resource": resource, # 单个/单组 可为 list[list[Resource]]
|
{
|
||||||
"namespace": namespace,
|
"resource": resource, # 单个/单组 可为 list[list[Resource]]
|
||||||
"edge_device_id": device_id,
|
"namespace": namespace,
|
||||||
"bind_parent_id": bind_parent_id,
|
"edge_device_id": device_id,
|
||||||
"bind_location": {
|
"bind_parent_id": bind_parent_id,
|
||||||
"x": bind_location.x,
|
"bind_location": {
|
||||||
"y": bind_location.y,
|
"x": bind_location.x,
|
||||||
"z": bind_location.z,
|
"y": bind_location.y,
|
||||||
|
"z": bind_location.z,
|
||||||
|
},
|
||||||
|
"other_calling_param": json.loads(other_calling_param) if other_calling_param else {},
|
||||||
},
|
},
|
||||||
"other_calling_param": json.loads(other_calling_param) if other_calling_param else {},
|
ensure_ascii=False,
|
||||||
}, ensure_ascii=False)
|
)
|
||||||
response = sclient.call(request)
|
response = sclient.call(request)
|
||||||
pass
|
pass
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def create_resource(self, device_id: str, res_id: str, class_name: str, parent: str, bind_locations: Point, liquid_input_slot: list[int], liquid_type: list[str], liquid_volume: list[int], slot_on_deck: int):
|
def create_resource(
|
||||||
init_new_res = initialize_resource({
|
self,
|
||||||
"name": res_id,
|
device_id: str,
|
||||||
"class": class_name,
|
res_id: str,
|
||||||
"parent": parent,
|
class_name: str,
|
||||||
"position": {
|
parent: str,
|
||||||
"x": bind_locations.x,
|
bind_locations: Point,
|
||||||
"y": bind_locations.y,
|
liquid_input_slot: list[int],
|
||||||
"z": bind_locations.z,
|
liquid_type: list[str],
|
||||||
|
liquid_volume: list[int],
|
||||||
|
slot_on_deck: int,
|
||||||
|
):
|
||||||
|
init_new_res = initialize_resource(
|
||||||
|
{
|
||||||
|
"name": res_id,
|
||||||
|
"class": class_name,
|
||||||
|
"parent": parent,
|
||||||
|
"position": {
|
||||||
|
"x": bind_locations.x,
|
||||||
|
"y": bind_locations.y,
|
||||||
|
"z": bind_locations.z,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}) # flatten的格式
|
) # flatten的格式
|
||||||
resources = [init_new_res]
|
resources = init_new_res # initialize_resource已经返回list[dict]
|
||||||
device_id = [device_id]
|
device_ids = [device_id]
|
||||||
bind_parent_id = [parent]
|
bind_parent_id = [parent]
|
||||||
bind_location = [bind_locations]
|
bind_location = [bind_locations]
|
||||||
other_calling_param = [json.dumps({
|
other_calling_param = [
|
||||||
"ADD_LIQUID_TYPE": liquid_type,
|
json.dumps(
|
||||||
"LIQUID_VOLUME": liquid_volume,
|
{
|
||||||
"LIQUID_INPUT_SLOT": liquid_input_slot,
|
"ADD_LIQUID_TYPE": liquid_type,
|
||||||
"initialize_full": False,
|
"LIQUID_VOLUME": liquid_volume,
|
||||||
"slot": slot_on_deck
|
"LIQUID_INPUT_SLOT": liquid_input_slot,
|
||||||
})]
|
"initialize_full": False,
|
||||||
|
"slot": slot_on_deck,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
return self.create_resource_detailed(resources, device_id, bind_parent_id, bind_location, other_calling_param)
|
return self.create_resource_detailed(resources, device_ids, bind_parent_id, bind_location, other_calling_param)
|
||||||
|
|
||||||
def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None:
|
def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -377,7 +435,9 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
if action_id not in self._action_clients:
|
if action_id not in self._action_clients:
|
||||||
action_type = action_value_mapping["type"]
|
action_type = action_value_mapping["type"]
|
||||||
self._action_clients[action_id] = ActionClient(self, action_type, action_id)
|
self._action_clients[action_id] = ActionClient(self, action_type, action_id)
|
||||||
self.lab_logger().debug(f"[Host Node] Created ActionClient (Local): {action_id}") # 子设备再创建用的是Discover发现的
|
self.lab_logger().debug(
|
||||||
|
f"[Host Node] Created ActionClient (Local): {action_id}"
|
||||||
|
) # 子设备再创建用的是Discover发现的
|
||||||
# from unilabos.app.mq import mqtt_client
|
# from unilabos.app.mq import mqtt_client
|
||||||
# info_with_schema = ros_action_to_json_schema(action_type)
|
# info_with_schema = ros_action_to_json_schema(action_type)
|
||||||
# mqtt_client.publish_actions(action_name, {
|
# mqtt_client.publish_actions(action_name, {
|
||||||
@@ -477,7 +537,12 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def send_goal(
|
def send_goal(
|
||||||
self, device_id: str, action_name: str, action_kwargs: Dict[str, Any], goal_uuid: Optional[str] = None
|
self,
|
||||||
|
device_id: str,
|
||||||
|
action_name: str,
|
||||||
|
action_kwargs: Dict[str, Any],
|
||||||
|
goal_uuid: Optional[str] = None,
|
||||||
|
server_info: Optional[Dict[str, Any]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
向设备发送目标请求
|
向设备发送目标请求
|
||||||
@@ -489,6 +554,8 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
goal_uuid: 目标UUID,如果为None则自动生成
|
goal_uuid: 目标UUID,如果为None则自动生成
|
||||||
"""
|
"""
|
||||||
action_id = f"/devices/{device_id}/{action_name}"
|
action_id = f"/devices/{device_id}/{action_name}"
|
||||||
|
if action_name == "test_latency" and server_info is not None:
|
||||||
|
self.server_latest_timestamp = server_info.get("send_timestamp", 0.0)
|
||||||
if action_id not in self._action_clients:
|
if action_id not in self._action_clients:
|
||||||
self.lab_logger().error(f"[Host Node] ActionClient {action_id} not found.")
|
self.lab_logger().error(f"[Host Node] ActionClient {action_id} not found.")
|
||||||
return
|
return
|
||||||
@@ -783,3 +850,148 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
# 这里可以实现返回资源列表的逻辑
|
# 这里可以实现返回资源列表的逻辑
|
||||||
self.lab_logger().debug(f"[Host Node-Resource] List parameters: {request}")
|
self.lab_logger().debug(f"[Host Node-Resource] List parameters: {request}")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def test_latency(self):
|
||||||
|
"""
|
||||||
|
测试网络延迟的action实现
|
||||||
|
通过5次ping-pong机制校对时间误差并计算实际延迟
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
import uuid as uuid_module
|
||||||
|
|
||||||
|
self.lab_logger().info("=" * 60)
|
||||||
|
self.lab_logger().info("开始网络延迟测试...")
|
||||||
|
|
||||||
|
# 记录任务开始执行的时间
|
||||||
|
task_start_time = time.time()
|
||||||
|
|
||||||
|
# 进行5次ping-pong测试
|
||||||
|
ping_results = []
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
self.lab_logger().info(f"第{i+1}/5次ping-pong测试...")
|
||||||
|
|
||||||
|
# 生成唯一的ping ID
|
||||||
|
ping_id = str(uuid_module.uuid4())
|
||||||
|
|
||||||
|
# 记录发送时间
|
||||||
|
send_timestamp = time.time()
|
||||||
|
|
||||||
|
# 发送ping
|
||||||
|
from unilabos.app.mq import mqtt_client
|
||||||
|
|
||||||
|
mqtt_client.send_ping(ping_id, send_timestamp)
|
||||||
|
|
||||||
|
# 等待pong响应
|
||||||
|
timeout = 10.0
|
||||||
|
start_wait_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_wait_time < timeout:
|
||||||
|
with self._ping_lock:
|
||||||
|
if ping_id in self._ping_responses:
|
||||||
|
pong_data = self._ping_responses.pop(ping_id)
|
||||||
|
break
|
||||||
|
time.sleep(0.001)
|
||||||
|
else:
|
||||||
|
self.lab_logger().error(f"❌ 第{i+1}次测试超时")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 计算本次测试结果
|
||||||
|
receive_timestamp = time.time()
|
||||||
|
client_timestamp = pong_data["client_timestamp"]
|
||||||
|
server_timestamp = pong_data["server_timestamp"]
|
||||||
|
|
||||||
|
# 往返时间
|
||||||
|
rtt_ms = (receive_timestamp - send_timestamp) * 1000
|
||||||
|
|
||||||
|
# 客户端与服务端时间差(客户端时间 - 服务端时间)
|
||||||
|
# 假设网络延迟对称,取中间点的服务端时间
|
||||||
|
mid_point_time = send_timestamp + (receive_timestamp - send_timestamp) / 2
|
||||||
|
time_diff_ms = (mid_point_time - server_timestamp) * 1000
|
||||||
|
|
||||||
|
ping_results.append({"rtt_ms": rtt_ms, "time_diff_ms": time_diff_ms})
|
||||||
|
|
||||||
|
self.lab_logger().info(f"✅ 第{i+1}次: 往返时间={rtt_ms:.2f}ms, 时间差={time_diff_ms:.2f}ms")
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
if not ping_results:
|
||||||
|
self.lab_logger().error("❌ 所有ping-pong测试都失败了")
|
||||||
|
return {"status": "all_timeout"}
|
||||||
|
|
||||||
|
# 统计分析
|
||||||
|
rtts = [r["rtt_ms"] for r in ping_results]
|
||||||
|
time_diffs = [r["time_diff_ms"] for r in ping_results]
|
||||||
|
|
||||||
|
avg_rtt_ms = sum(rtts) / len(rtts)
|
||||||
|
avg_time_diff_ms = sum(time_diffs) / len(time_diffs)
|
||||||
|
max_time_diff_error_ms = max(abs(min(time_diffs)), abs(max(time_diffs)))
|
||||||
|
|
||||||
|
self.lab_logger().info("-" * 50)
|
||||||
|
self.lab_logger().info("[测试统计]")
|
||||||
|
self.lab_logger().info(f"有效测试次数: {len(ping_results)}/5")
|
||||||
|
self.lab_logger().info(f"平均往返时间: {avg_rtt_ms:.2f}ms")
|
||||||
|
self.lab_logger().info(f"平均时间差: {avg_time_diff_ms:.2f}ms")
|
||||||
|
self.lab_logger().info(f"时间差范围: {min(time_diffs):.2f}ms ~ {max(time_diffs):.2f}ms")
|
||||||
|
self.lab_logger().info(f"最大时间误差: ±{max_time_diff_error_ms:.2f}ms")
|
||||||
|
|
||||||
|
# 计算任务执行延迟
|
||||||
|
if hasattr(self, "server_latest_timestamp") and self.server_latest_timestamp > 0:
|
||||||
|
self.lab_logger().info("-" * 50)
|
||||||
|
self.lab_logger().info("[任务执行延迟分析]")
|
||||||
|
self.lab_logger().info(f"服务端任务下发时间: {self.server_latest_timestamp:.6f}")
|
||||||
|
self.lab_logger().info(f"客户端任务开始时间: {task_start_time:.6f}")
|
||||||
|
|
||||||
|
# 原始时间差(不考虑时间同步误差)
|
||||||
|
raw_delay_ms = (task_start_time - self.server_latest_timestamp) * 1000
|
||||||
|
|
||||||
|
# 考虑时间同步误差后的延迟(用平均时间差校正)
|
||||||
|
corrected_delay_ms = raw_delay_ms - avg_time_diff_ms
|
||||||
|
|
||||||
|
self.lab_logger().info(f"📊 原始时间差: {raw_delay_ms:.2f}ms")
|
||||||
|
self.lab_logger().info(f"🔧 时间同步校正: {avg_time_diff_ms:.2f}ms")
|
||||||
|
self.lab_logger().info(f"⏰ 实际任务延迟: {corrected_delay_ms:.2f}ms")
|
||||||
|
self.lab_logger().info(f"📏 误差范围: ±{max_time_diff_error_ms:.2f}ms")
|
||||||
|
|
||||||
|
# 给出延迟范围
|
||||||
|
min_delay = corrected_delay_ms - max_time_diff_error_ms
|
||||||
|
max_delay = corrected_delay_ms + max_time_diff_error_ms
|
||||||
|
self.lab_logger().info(f"📋 延迟范围: {min_delay:.2f}ms ~ {max_delay:.2f}ms")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.lab_logger().warning("⚠️ 无法获取服务端任务下发时间,跳过任务延迟分析")
|
||||||
|
corrected_delay_ms = -1
|
||||||
|
|
||||||
|
self.lab_logger().info("=" * 60)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"avg_rtt_ms": avg_rtt_ms,
|
||||||
|
"avg_time_diff_ms": avg_time_diff_ms,
|
||||||
|
"max_time_error_ms": max_time_diff_error_ms,
|
||||||
|
"task_delay_ms": corrected_delay_ms if corrected_delay_ms > 0 else -1,
|
||||||
|
"raw_delay_ms": (
|
||||||
|
raw_delay_ms if hasattr(self, "server_latest_timestamp") and self.server_latest_timestamp > 0 else -1
|
||||||
|
),
|
||||||
|
"test_count": len(ping_results),
|
||||||
|
"status": "success",
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_pong_response(self, pong_data: dict):
|
||||||
|
"""
|
||||||
|
处理pong响应
|
||||||
|
"""
|
||||||
|
ping_id = pong_data.get("ping_id")
|
||||||
|
if ping_id:
|
||||||
|
with self._ping_lock:
|
||||||
|
self._ping_responses[ping_id] = pong_data
|
||||||
|
|
||||||
|
# 详细信息合并为一条日志
|
||||||
|
client_timestamp = pong_data.get("client_timestamp", 0)
|
||||||
|
server_timestamp = pong_data.get("server_timestamp", 0)
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
self.lab_logger().debug(
|
||||||
|
f"📨 Pong | ID:{ping_id[:8]}.. | C→S→C: {client_timestamp:.3f}→{server_timestamp:.3f}→{current_time:.3f}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.lab_logger().warning("⚠️ 收到无效的Pong响应(缺少ping_id)")
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
float64[] asp_vols
|
|
||||||
float64[] dis_vols
|
|
||||||
Resource[] reagent_sources
|
|
||||||
Resource[] targets
|
|
||||||
int32[] use_channels
|
|
||||||
float64[] flow_rates
|
|
||||||
geometry_msgs/Point[] offsets
|
|
||||||
float64[] liquid_height
|
|
||||||
float64[] blow_out_air_volume
|
|
||||||
string spread
|
|
||||||
bool is_96_well
|
|
||||||
int32 mix_time
|
|
||||||
int32 mix_vol
|
|
||||||
int32 mix_rate
|
|
||||||
float64 mix_liquid_height
|
|
||||||
string[] none_keys
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
float64 seconds
|
|
||||||
string msg
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
Resource[] targets
|
|
||||||
int32 mix_time
|
|
||||||
int32 mix_vol
|
|
||||||
float64 height_to_bottom
|
|
||||||
geometry_msgs/Point[] offsets
|
|
||||||
float64 mix_rate
|
|
||||||
string[] none_keys
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Resource well
|
|
||||||
float64 dis_to_top
|
|
||||||
int32 channel
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
float64[] vols
|
|
||||||
Resource[] sources
|
|
||||||
Resource waste_liquid
|
|
||||||
int32[] use_channels
|
|
||||||
float64[] flow_rates
|
|
||||||
geometry_msgs/Point[] offsets
|
|
||||||
float64[] liquid_height
|
|
||||||
float64[] blow_out_air_volume
|
|
||||||
string spread
|
|
||||||
int32[] delays
|
|
||||||
bool is_96_well
|
|
||||||
float64[] top
|
|
||||||
string[] none_keys
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Resource[] tip_racks
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Resource[] targets
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
float64[] asp_vols
|
|
||||||
float64[] dis_vols
|
|
||||||
Resource[] sources
|
|
||||||
Resource[] targets
|
|
||||||
Resource[] tip_racks
|
|
||||||
int32[] use_channels
|
|
||||||
float64[] asp_flow_rates
|
|
||||||
float64[] dis_flow_rates
|
|
||||||
geometry_msgs/Point[] offsets
|
|
||||||
bool touch_tip
|
|
||||||
float64[] liquid_height
|
|
||||||
float64[] blow_out_air_volume
|
|
||||||
string spread
|
|
||||||
bool is_96_well
|
|
||||||
string mix_stage
|
|
||||||
int32[] mix_times
|
|
||||||
int32 mix_vol
|
|
||||||
int32 mix_rate
|
|
||||||
float64 mix_liquid_height
|
|
||||||
int32[] delays
|
|
||||||
string[] none_keys
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
Reference in New Issue
Block a user