mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-18 05:21:19 +00:00
* unify liquid_handler definition * remove default values * Dev Sync (#25) * Update README and MQTTClient for installation instructions and code improvements * feat: 支持local_config启动 add: 增加对crt path的说明,为传入config.py的相对路径 move: web component * add: registry description * add 3d visualization * 完成在main中启动设备可视化 完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model 添加物料模型管理类,遍历物料与resource_model,完成TF数据收集 * 完成TF发布 * 修改模型方向,在yaml中添加变换属性 * 添加物料tf变化时,发送topic到前端 另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题 * 添加关节发布节点与物料可视化节点进入unilab * 使用json启动plr与3D模型仿真 * 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/ * 完成启动OT并联动rviz * add 3d visualization * 完成在main中启动设备可视化 完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model 添加物料模型管理类,遍历物料与resource_model,完成TF数据收集 * 完成TF发布 * 修改模型方向,在yaml中添加变换属性 * 添加物料tf变化时,发送topic到前端 另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题 * 添加关节发布节点与物料可视化节点进入unilab * 使用json启动plr与3D模型仿真 * 完成启动OT并联动rviz * fix: device.class possible null * fix: HPLC additions with online service * fix: slave mode spin not working * fix: slave mode spin not working * 修复rviz位置问题, 修复rviz位置问题, 在无tf变动时减缓发送频率 在backend中添加物料跟随方法 * feat: 多ProtocolNode 允许子设备ID相同 feat: 上报发现的ActionClient feat: Host重启动,通过discover机制要求slaveNode重新注册,实现信息及时上报 * feat: 支持env设置config * fix: running logic * fix: running logic * fix: missing ot * 在main中直接初始化republisher和物料的mesh节点 * 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中 * Device visualization (#14) * add 3d visualization * 完成在main中启动设备可视化 完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model 添加物料模型管理类,遍历物料与resource_model,完成TF数据收集 * 完成TF发布 * 修改模型方向,在yaml中添加变换属性 * 添加物料tf变化时,发送topic到前端 另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题 * 添加关节发布节点与物料可视化节点进入unilab * 使用json启动plr与3D模型仿真 * 完成启动OT并联动rviz * add 3d visualization * 完成在main中启动设备可视化 完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model 添加物料模型管理类,遍历物料与resource_model,完成TF数据收集 * 完成TF发布 * 修改模型方向,在yaml中添加变换属性 * 添加物料tf变化时,发送topic到前端 另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题 * 添加关节发布节点与物料可视化节点进入unilab * 使用json启动plr与3D模型仿真 * 完成启动OT并联动rviz * 修复rviz位置问题, 修复rviz位置问题, 在无tf变动时减缓发送频率 在backend中添加物料跟随方法 * fix: running logic * fix: running logic * fix: missing ot * 在main中直接初始化republisher和物料的mesh节点 * 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中 --------- Co-authored-by: zhangshixiang <@zhangshixiang> Co-authored-by: wznln <18435084+Xuwznln@users.noreply.github.com> * fix: missing hostname in devices_names fix: upload_file for model file * fix: missing paho-mqtt package bump version to 0.9.0 * fix startup add ResourceCreateFromOuter.action * fix type hint * update actions * update actions * host node add_resource_from_outer fix cmake list * pass device config to device class * add: bind_parent_ids to resource create action fix: message convert string * fix: host node should not be re_discovered * feat: resource tracker support dict * feat: add more necessary params * feat: fix boolean null in registry action data * feat: add outer resource * 编写mesh添加action * feat: append resource * add action * feat: vis 2d for plr * fix * fix: browser on rviz * fix: cloud bridge error fallback to local * fix: salve auto run rviz * 初始化两个plate * Device visualization (#22) * add 3d visualization * 完成在main中启动设备可视化 完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model 添加物料模型管理类,遍历物料与resource_model,完成TF数据收集 * 完成TF发布 * 修改模型方向,在yaml中添加变换属性 * 添加物料tf变化时,发送topic到前端 另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题 * 添加关节发布节点与物料可视化节点进入unilab * 使用json启动plr与3D模型仿真 * 完成启动OT并联动rviz * add 3d visualization * 完成在main中启动设备可视化 完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model 添加物料模型管理类,遍历物料与resource_model,完成TF数据收集 * 完成TF发布 * 修改模型方向,在yaml中添加变换属性 * 添加物料tf变化时,发送topic到前端 另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题 * 添加关节发布节点与物料可视化节点进入unilab * 使用json启动plr与3D模型仿真 * 完成启动OT并联动rviz * 修复rviz位置问题, 修复rviz位置问题, 在无tf变动时减缓发送频率 在backend中添加物料跟随方法 * fix: running logic * fix: running logic * fix: missing ot * 在main中直接初始化republisher和物料的mesh节点 * 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中 * 编写mesh添加action * add action * fix * fix: browser on rviz * fix: cloud bridge error fallback to local * fix: salve auto run rviz * 初始化两个plate --------- Co-authored-by: zhangshixiang <@zhangshixiang> Co-authored-by: wznln <18435084+Xuwznln@users.noreply.github.com> * fix: multi channel * fix: aspirate * fix: aspirate * fix: aspirate * fix: aspirate * 提交 * fix: jobadd * fix: jobadd * fix: msg converter * tijiao * add resource creat easy action * identify debug msg * mq client id --------- Co-authored-by: Harvey Que <Q-Query@outlook.com> Co-authored-by: zhangshixiang <@zhangshixiang> Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com> * remove default behavior for visualization * change liquidhandler name * resource create from outer easy * add easy resource creation * easy resource creation logic * remove wrongly debug msg from others * remove wrongly debug msg from others * add missing action clients * fix device_id * fix slot_on_deck * fix registry typo * complete require packages msg converter support array string implements create resource logic * 修复port输入 * 修复必须两次启动edge后端才有节点生成的bug 新增resources报送 * 新增延迟统计 --------- Co-authored-by: Junhan Chang <changjh@pku.edu.cn> Co-authored-by: Harvey Que <Q-Query@outlook.com> Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com>
214 lines
8.4 KiB
Python
214 lines
8.4 KiB
Python
import json
|
||
import time
|
||
import traceback
|
||
import uuid
|
||
|
||
import paho.mqtt.client as mqtt
|
||
import ssl
|
||
import base64
|
||
import hmac
|
||
from hashlib import sha1
|
||
import tempfile
|
||
import os
|
||
|
||
from unilabos.config.config import MQConfig
|
||
from unilabos.app.controler import job_add
|
||
from unilabos.app.model import JobAddReq
|
||
from unilabos.utils import logger
|
||
from unilabos.utils.type_check import TypeEncoder
|
||
|
||
from paho.mqtt.enums import CallbackAPIVersion
|
||
|
||
|
||
class MQTTClient:
|
||
mqtt_disable = True
|
||
|
||
def __init__(self):
|
||
self.mqtt_disable = not MQConfig.lab_id
|
||
self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}"
|
||
logger.info("[MQTT] Client_id: " + self.client_id)
|
||
self.client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
|
||
self._setup_callbacks()
|
||
|
||
def _setup_callbacks(self):
|
||
self.client.on_log = self._on_log
|
||
self.client.on_connect = self._on_connect
|
||
self.client.on_message = self._on_message
|
||
self.client.on_disconnect = self._on_disconnect
|
||
|
||
def _on_log(self, client, userdata, level, buf):
|
||
# logger.info(f"[MQTT] log: {buf}")
|
||
pass
|
||
|
||
def _on_connect(self, client, userdata, flags, rc, properties=None):
|
||
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}/pong/", 0)
|
||
|
||
def _on_message(self, client, userdata, msg) -> None:
|
||
# logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
|
||
try:
|
||
payload_str = msg.payload.decode("utf-8")
|
||
payload_json = json.loads(payload_str)
|
||
if msg.topic == f"labs/{MQConfig.lab_id}/job/start/":
|
||
if "data" not in payload_json:
|
||
payload_json["data"] = {}
|
||
if "action" in payload_json:
|
||
payload_json["data"]["action"] = payload_json.pop("action")
|
||
if "action_kwargs" in payload_json:
|
||
payload_json["data"]["action_kwargs"] = payload_json.pop("action_kwargs")
|
||
job_req = JobAddReq.model_validate(payload_json)
|
||
data = job_add(job_req)
|
||
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:
|
||
logger.error(f"[MQTT] JSON 解析错误: {e}")
|
||
logger.error(f"[MQTT] Raw message: {msg.payload}")
|
||
logger.error(traceback.format_exc())
|
||
except Exception as e:
|
||
logger.error(f"[MQTT] 处理消息时出错: {e}")
|
||
logger.error(traceback.format_exc())
|
||
|
||
def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None):
|
||
if rc != 0:
|
||
logger.error(f"[MQTT] Unexpected disconnection {rc}")
|
||
|
||
def _setup_ssl_context(self):
|
||
temp_files = []
|
||
try:
|
||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as ca_temp:
|
||
ca_temp.write(MQConfig.ca_content)
|
||
temp_files.append(ca_temp.name)
|
||
|
||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as cert_temp:
|
||
cert_temp.write(MQConfig.cert_content)
|
||
temp_files.append(cert_temp.name)
|
||
|
||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as key_temp:
|
||
key_temp.write(MQConfig.key_content)
|
||
temp_files.append(key_temp.name)
|
||
|
||
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
||
context.load_verify_locations(cafile=temp_files[0])
|
||
context.load_cert_chain(certfile=temp_files[1], keyfile=temp_files[2])
|
||
self.client.tls_set_context(context)
|
||
finally:
|
||
for temp_file in temp_files:
|
||
try:
|
||
os.unlink(temp_file)
|
||
except Exception as e:
|
||
pass
|
||
|
||
def start(self):
|
||
if self.mqtt_disable:
|
||
logger.warning("MQTT is disabled, skipping connection.")
|
||
return
|
||
userName = f"Signature|{MQConfig.access_key}|{MQConfig.instance_id}"
|
||
password = base64.b64encode(
|
||
hmac.new(MQConfig.secret_key.encode(), self.client_id.encode(), sha1).digest()
|
||
).decode()
|
||
|
||
self.client.username_pw_set(userName, password)
|
||
self._setup_ssl_context()
|
||
|
||
# 创建连接线程
|
||
def connect_thread_func():
|
||
try:
|
||
self.client.connect(MQConfig.broker_url, MQConfig.port, 60)
|
||
self.client.loop_start()
|
||
|
||
# 添加连接超时检测
|
||
max_attempts = 5
|
||
attempt = 0
|
||
while not self.client.is_connected() and attempt < max_attempts:
|
||
logger.info(
|
||
f"[MQTT] 正在连接到 {MQConfig.broker_url}:{MQConfig.port},尝试 {attempt+1}/{max_attempts}"
|
||
)
|
||
time.sleep(3)
|
||
attempt += 1
|
||
|
||
if self.client.is_connected():
|
||
logger.info(f"[MQTT] 已成功连接到 {MQConfig.broker_url}:{MQConfig.port}")
|
||
else:
|
||
logger.error(f"[MQTT] 连接超时,可能是账号密码错误或网络问题")
|
||
self.client.loop_stop()
|
||
except Exception as e:
|
||
logger.error(f"[MQTT] 连接失败: {str(e)}")
|
||
|
||
connect_thread_func()
|
||
# connect_thread = threading.Thread(target=connect_thread_func)
|
||
# connect_thread.daemon = True
|
||
# connect_thread.start()
|
||
|
||
def stop(self):
|
||
if self.mqtt_disable:
|
||
return
|
||
self.client.disconnect()
|
||
self.client.loop_stop()
|
||
|
||
def publish_device_status(self, device_status: dict, device_id, property_name):
|
||
# status = device_status.get(device_id, {})
|
||
if self.mqtt_disable:
|
||
return
|
||
status = {"data": device_status.get(device_id, {}), "device_id": device_id}
|
||
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}")
|
||
|
||
def publish_job_status(self, feedback_data: dict, job_id: str, status: str):
|
||
if self.mqtt_disable:
|
||
return
|
||
jobdata = {"job_id": job_id, "data": feedback_data, "status": status}
|
||
self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2)
|
||
|
||
def publish_registry(self, device_id: str, device_info: dict):
|
||
if self.mqtt_disable:
|
||
return
|
||
address = f"labs/{MQConfig.lab_id}/registry/"
|
||
registry_data = json.dumps({device_id: device_info}, ensure_ascii=False, cls=TypeEncoder)
|
||
self.client.publish(address, registry_data, qos=2)
|
||
logger.debug(f"Registry data published: address: {address}, {registry_data}")
|
||
|
||
def publish_actions(self, action_id: str, action_info: dict):
|
||
if self.mqtt_disable:
|
||
return
|
||
address = f"labs/{MQConfig.lab_id}/actions/"
|
||
self.client.publish(address, json.dumps(action_info), qos=2)
|
||
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()
|
||
|
||
if __name__ == "__main__":
|
||
mqtt_client.start()
|