Closes #3. Closes #12.

Closes #3. Closes #12.
* Update README and MQTTClient for installation instructions and code improvements

* feat: 支持local_config启动
add: 增加对crt path的说明,为传入config.py的相对路径
move: web component

* add: registry description

* feat: node_info_update srv
fix: OTDeck cant create

* close #12
feat: slave node registry

* feat: show machine name
fix: host node registry not uploaded

* feat: add hplc registry

* feat: add hplc registry

* fix: hplc status typo

* fix: devices/

* fix: device.class possible null

* fix: HPLC additions with online service

* fix: slave mode spin not working

* fix: slave mode spin not working

* feat: 多ProtocolNode 允许子设备ID相同
feat: 上报发现的ActionClient
feat: Host重启动,通过discover机制要求slaveNode重新注册,实现信息及时上报

---------

Co-authored-by: Harvey Que <Q-Query@outlook.com>
This commit is contained in:
Xuwznln
2025-05-01 14:58:36 +08:00
committed by GitHub
parent 74ae2a88ac
commit 01ac3415ae
15 changed files with 300 additions and 106 deletions

View File

@@ -58,6 +58,18 @@ def parse_args():
default=None,
help="配置文件路径,支持.py格式的Python配置文件",
)
parser.add_argument(
"--port",
type=int,
default=8002,
help="信息页web服务的启动端口",
)
parser.add_argument(
"--open_browser",
type=bool,
default=True,
help="是否在启动时打开信息页",
)
return parser.parse_args()
@@ -84,6 +96,9 @@ def main():
# 设置BasicConfig参数
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
machine_name = os.popen("hostname").read().strip()
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
BasicConfig.machine_name = machine_name
from unilabos.resources.graphio import (
read_node_link_json,
@@ -151,7 +166,7 @@ def main():
mqtt_client.start()
start_backend(**args_dict)
start_server()
start_server(port=args_dict.get("port", 8002), open_browser=args_dict.get("open_browser", False))
if __name__ == "__main__":

View File

@@ -146,7 +146,7 @@ class MQTTClient:
if self.mqtt_disable:
return
status = {"data": device_status.get(device_id, {}), "device_id": device_id}
address = f"labs/{MQConfig.lab_id}/devices"
address = f"labs/{MQConfig.lab_id}/devices/"
self.client.publish(address, json.dumps(status), qos=2)
logger.critical(f"Device status published: address: {address}, {status}")
@@ -168,11 +168,8 @@ class MQTTClient:
if self.mqtt_disable:
return
address = f"labs/{MQConfig.lab_id}/actions/"
action_type_name = action_info["title"]
action_info["title"] = action_id
action_data = json.dumps({action_type_name: action_info}, ensure_ascii=False)
self.client.publish(address, action_data, qos=2)
logger.debug(f"Action data published: address: {address}, {action_data}")
self.client.publish(address, json.dumps(action_info), qos=2)
logger.debug(f"Action data published: address: {address}, {action_id}, {action_info}")
mqtt_client = MQTTClient()

View File

@@ -92,19 +92,7 @@ def setup_web_pages(router: APIRouter) -> None:
# 获取已加载的设备
if lab_registry:
# 设备类型
for device_id, device_info in lab_registry.device_type_registry.items():
msg = {
"id": device_id,
"name": device_info.get("name", "未命名"),
"file_path": device_info.get("file_path", ""),
"class_json": json.dumps(
device_info.get("class", {}), indent=4, ensure_ascii=False, cls=TypeEncoder
),
}
mqtt_client.publish_registry(device_id, device_info)
devices.append(msg)
devices = json.loads(json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder))
# 资源类型
for resource_id, resource_info in lab_registry.resource_type_registry.items():
resources.append(

View File

@@ -96,17 +96,19 @@
<tr>
<th>设备ID</th>
<th>命名空间</th>
<th>机器名称</th>
<th>状态</th>
</tr>
{% for device_id, device_info in host_node_info.devices.items() %}
<tr>
<td>{{ device_id }}</td>
<td>{{ device_info.namespace }}</td>
<td>{{ device_info.machine_name }}</td>
<td><span class="status-badge online">{{ "在线" if device_info.is_online else "离线" }}</span></td>
</tr>
{% else %}
<tr>
<td colspan="3" class="empty-state">没有发现已管理的设备</td>
<td colspan="4" class="empty-state">没有发现已管理的设备</td>
</tr>
{% endfor %}
</table>
@@ -218,6 +220,7 @@
<th>Device ID</th>
<th>节点名称</th>
<th>命名空间</th>
<th>机器名称</th>
<th>状态项</th>
<th>动作数</th>
</tr>
@@ -227,6 +230,7 @@
<td>{{ device_id }}</td>
<td>{{ device_info.node_name }}</td>
<td>{{ device_info.namespace }}</td>
<td>{{ device_info.machine_name|default("本地") }}</td>
<td>{{ ros_node_info.device_topics.get(device_id, {})|length }}</td>
<td>{{ ros_node_info.device_actions.get(device_id, {})|length }} <span class="toggle-indicator"></span></td>
</tr>
@@ -329,8 +333,13 @@
<tr id="device-info-{{ loop.index }}" class="detail-row" style="display: none;">
<td colspan="5">
<div class="content-full">
<pre>{{ device.class_json }}</pre>
{% if device.class %}
<pre>{{ device.class | tojson(indent=4) }}</pre>
{% else %}
<!-- 这里可以放占位内容,比如 -->
<pre>// No data</pre>
{% endif %}
{% if device.is_online %}
<div class="status-badge"><span class="online-status">在线</span></div>
{% endif %}
@@ -362,7 +371,12 @@
<button class="copy-btn" onclick="copyToClipboard(this.previousElementSibling.textContent, event)">复制</button>
<button class="debug-btn" onclick="toggleDebugInfo(this, event)">调试</button>
<div class="debug-info" style="display:none;">
<pre>{{ action_info|tojson(indent=2) }}</pre>
{% if action_info %}
<pre>{{ action_info | tojson(indent=4) }}</pre>
{% else %}
<!-- 这里可以放占位内容,比如 -->
<pre>// No data</pre>
{% endif %}
</div>
</div>

View File

@@ -30,20 +30,19 @@ def get_host_node_info() -> Dict[str, Any]:
return host_info
host_info["available"] = True
host_info["devices"] = {
device_id: {
edge_device_id: {
"namespace": namespace,
"is_online": f"{namespace}/{device_id}" in host_node._online_devices,
"key": f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}",
"is_online": f"{namespace}/{edge_device_id}" in host_node._online_devices,
"key": f"{namespace}/{edge_device_id}" if namespace.startswith("/") else f"/{namespace}/{edge_device_id}",
"machine_name": host_node.device_machine_names.get(edge_device_id, "未知"),
}
for device_id, namespace in host_node.devices_names.items()
for edge_device_id, namespace in host_node.devices_names.items()
}
# 获取已订阅的主题
host_info["subscribed_topics"] = sorted(list(host_node._subscribed_topics))
# 获取动作客户端信息
for action_id, client in host_node._action_clients.items():
host_info["action_clients"] = {
action_id: get_action_info(client, full_name=action_id)
}
host_info["action_clients"] = {action_id: get_action_info(client, full_name=action_id)}
# 获取设备状态
host_info["device_status"] = host_node.device_status

View File

@@ -12,6 +12,7 @@ from unilabos.app.web.utils.action_utils import get_action_info
# 存储 ROS 节点信息的全局变量
ros_node_info = {"online_devices": {}, "device_topics": {}, "device_actions": {}}
def get_ros_node_info() -> Dict[str, Any]:
"""获取 ROS 节点信息,包括设备节点、发布的状态和动作
@@ -35,6 +36,13 @@ def update_ros_node_info() -> Dict[str, Any]:
try:
from unilabos.ros.nodes.base_device_node import registered_devices
from unilabos.ros.nodes.presets.host_node import HostNode
# 尝试获取主机节点实例
host_node = HostNode.get_instance(0)
device_machine_names = {}
if host_node:
device_machine_names = host_node.device_machine_names
for device_id, device_info in registered_devices.items():
# 设备基本信息
@@ -42,6 +50,7 @@ def update_ros_node_info() -> Dict[str, Any]:
"node_name": device_info["node_name"],
"namespace": device_info["namespace"],
"uuid": device_info["uuid"],
"machine_name": device_machine_names.get(device_id, "本地"),
}
# 设备话题(状态)信息
@@ -55,10 +64,7 @@ def update_ros_node_info() -> Dict[str, Any]:
}
# 设备动作信息
result["device_actions"][device_id] = {
k: get_action_info(v, k)
for k, v in device_info["actions"].items()
}
result["device_actions"][device_id] = {k: get_action_info(v, k) for k, v in device_info["actions"].items()}
# 更新全局变量
ros_node_info = result
except Exception as e: