From 7953b3820ec4c8f3c93ec859094d85ba92b1fa69 Mon Sep 17 00:00:00 2001 From: WenzheG Date: Wed, 5 Nov 2025 19:58:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Raman=E5=92=8Cxrd=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unilabos/devices/opsky_Raman/devices.json | 20 + unilabos/devices/opsky_Raman/dmqfengzhuang.py | 71 ++ .../devices/opsky_Raman/opsky_ATR30007.py | 398 ++++++++ unilabos/devices/opsky_Raman/raman_module.py | 180 ++++ unilabos/devices/opsky_Raman/test2.py | 209 ++++ unilabos/devices/xrd_d7mate/device.json | 26 + unilabos/devices/xrd_d7mate/xrd_d7mate.py | 939 ++++++++++++++++++ unilabos/registry/devices/opsky_ATR30007.yaml | 86 ++ unilabos/registry/devices/xrd_d7mate.yaml | 557 +++++++++++ 9 files changed, 2486 insertions(+) create mode 100644 unilabos/devices/opsky_Raman/devices.json create mode 100644 unilabos/devices/opsky_Raman/dmqfengzhuang.py create mode 100644 unilabos/devices/opsky_Raman/opsky_ATR30007.py create mode 100644 unilabos/devices/opsky_Raman/raman_module.py create mode 100644 unilabos/devices/opsky_Raman/test2.py create mode 100644 unilabos/devices/xrd_d7mate/device.json create mode 100644 unilabos/devices/xrd_d7mate/xrd_d7mate.py create mode 100644 unilabos/registry/devices/opsky_ATR30007.yaml create mode 100644 unilabos/registry/devices/xrd_d7mate.yaml diff --git a/unilabos/devices/opsky_Raman/devices.json b/unilabos/devices/opsky_Raman/devices.json new file mode 100644 index 00000000..1ab5398c --- /dev/null +++ b/unilabos/devices/opsky_Raman/devices.json @@ -0,0 +1,20 @@ +{ + "nodes": [ + { + "id": "opsky_ATR30007", + "name": "opsky_ATR30007", + "parent": null, + "type": "device", + "class": "opsky_ATR30007", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": {}, + "data": {}, + "children": [] + } + ], + "links": [] +} \ No newline at end of file diff --git a/unilabos/devices/opsky_Raman/dmqfengzhuang.py b/unilabos/devices/opsky_Raman/dmqfengzhuang.py new file mode 100644 index 00000000..fce22c71 --- /dev/null +++ b/unilabos/devices/opsky_Raman/dmqfengzhuang.py @@ -0,0 +1,71 @@ +import socket +import time +import csv +from datetime import datetime +import threading + +csv_lock = threading.Lock() # 防止多线程写CSV冲突 + +def scan_once(ip="192.168.1.50", port_in=2001, port_out=2002, + csv_file="scan_results.csv", timeout=5, retries=3): + """ + 改进版扫码函数: + - 自动重试 + - 全程超时保护 + - 更安全的socket关闭 + - 文件写入加锁 + """ + def save_result(qrcode): + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with csv_lock: + with open(csv_file, mode="a", newline="") as f: + writer = csv.writer(f) + writer.writerow([timestamp, qrcode]) + print(f"✅ 已保存结果: {timestamp}, {qrcode}") + + result = None + + for attempt in range(1, retries + 1): + print(f"\n🟡 扫码尝试 {attempt}/{retries} ...") + + try: + # -------- Step 1: 触发拍照 -------- + with socket.create_connection((ip, port_in), timeout=2) as client_in: + cmd = "start" + client_in.sendall(cmd.encode("ascii")) #把字符串转为byte字节流规则是ascii码 + print(f"→ 已发送触发指令: {cmd}") + + # -------- Step 2: 等待识别结果 -------- + with socket.create_connection((ip, port_out), timeout=timeout) as client_out: + print(f" 已连接相机输出端口 {port_out},等待结果...") + + # recv最多阻塞timeout秒 + client_out.settimeout(timeout) + data = client_out.recv(2048).decode("ascii", errors="ignore").strip() #结果输出为ascii字符串,遇到无法解析的字节则忽略 + # .strip():去掉字符串头尾的空白字符(包括 \n, \r, 空格等),便于后续判断是否为空或写入 CSV。 + if data: + print(f"📷 识别结果: {data}") + save_result(data) #调用 save_result(data) 把时间戳 + 识别字符串写入 CSV(线程安全)。 + result = data #把局部变量 result 设为 data,用于函数返回值 + break #如果读取成功跳出重试循环(for attempt in ...),不再进行后续重试。 + else: + print("⚠️ 相机返回空数据,重试中...") + + except socket.timeout: + print("⏰ 超时未收到识别结果,重试中...") + except ConnectionRefusedError: + print("❌ 无法连接到扫码器端口,请检查设备是否在线。") + except OSError as e: + print(f"⚠️ 网络错误: {e}") + except Exception as e: + print(f"❌ 未知异常: {e}") + + time.sleep(0.5) # 两次扫描之间稍作延时 + + # -------- Step 3: 返回最终结果 -------- + if result: + print(f"✅ 扫码成功:{result}") + else: + print("❌ 多次尝试后仍未获取二维码结果") + + return result diff --git a/unilabos/devices/opsky_Raman/opsky_ATR30007.py b/unilabos/devices/opsky_Raman/opsky_ATR30007.py new file mode 100644 index 00000000..8eee2bab --- /dev/null +++ b/unilabos/devices/opsky_Raman/opsky_ATR30007.py @@ -0,0 +1,398 @@ +# opsky_atr30007.py +import logging +import time as time_mod +import csv +from datetime import datetime +from typing import Optional, Dict, Any + +# 兼容 pymodbus 在不同版本中的位置与 API +try: + from pymodbus.client import ModbusTcpClient +except Exception: + ModbusTcpClient = None + +# 导入 run_raman_test(假定与本文件同目录) +# 如果你的项目是包结构且原先使用相对导入,请改回 `from .raman_module import run_raman_test` +try: + from .raman_module import run_raman_test +except Exception: + # 延迟导入失败不会阻止主流程(在 run 时会再尝试) + run_raman_test = None + +logger = logging.getLogger("opsky") +logger.setLevel(logging.INFO) +ch = logging.StreamHandler() +formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", "%y-%m-%d %H:%M:%S") +ch.setFormatter(formatter) +logger.addHandler(ch) + + +class opsky_ATR30007: + """ + 封装 UniLabOS 设备动作逻辑,兼容 pymodbus 2.x / 3.x。 + 放在独立文件中:opsky_atr30007.py + """ + + def __init__( + self, + plc_ip: str = "192.168.1.88", + plc_port: int = 502, + robot_ip: str = "192.168.1.200", + robot_port: int = 502, + scan_csv_file: str = "scan_results.csv", + ): + self.plc_ip = plc_ip + self.plc_port = plc_port + self.robot_ip = robot_ip + self.robot_port = robot_port + self.scan_csv_file = scan_csv_file + + # ----------------- 参数字符串转换 helpers ----------------- + @staticmethod + def _str_to_int(s, default): + try: + return int(float(str(s).strip())) + except Exception: + return int(default) + + @staticmethod + def _str_to_float(s, default): + try: + return float(str(s).strip()) + except Exception: + return float(default) + + @staticmethod + def _str_to_bool(s, default): + try: + v = str(s).strip().lower() + if v in ("true", "1", "yes", "y", "t"): + return True + if v in ("false", "0", "no", "n", "f"): + return False + return default + except Exception: + return default + + # ----------------- Modbus / 安全读写 ----------------- + @staticmethod + def _adapt_req_kwargs_for_read(func_name: str, args: tuple, kwargs: dict): + # 如果调用方传的是 (address, count) positional,在新版接口可能是 address=..., count=... + if len(args) == 2 and func_name.startswith("read_"): + address, count = args + args = () + kwargs.setdefault("address", address) + kwargs.setdefault("count", count) + return args, kwargs + + @staticmethod + def _adapt_req_kwargs_for_write(func_name: str, args: tuple, kwargs: dict): + if len(args) == 2 and func_name.startswith("write_"): + address, value = args + args = () + kwargs.setdefault("address", address) + kwargs.setdefault("value", value) + return args, kwargs + + def ensure_connected(self, client, name, ip, port): + """确保连接存在,失败则尝试重连并返回新的 client 或 None""" + if client is None: + return None + try: + # 不同 pymodbus 版本可能有不同方法检测 socket + is_open = False + try: + is_open = bool(client.is_socket_open()) + except Exception: + # fallback: try to read nothing or attempt connection test + try: + # 轻试一次 + is_open = client.connected if hasattr(client, "connected") else False + except Exception: + is_open = False + + if not is_open: + logger.warning("%s 掉线,尝试重连...", name) + try: + client.close() + except Exception: + pass + time_mod.sleep(0.5) + if ModbusTcpClient: + new_client = ModbusTcpClient(ip, port=port) + try: + if new_client.connect(): + logger.info("%s 重新连接成功 (%s:%s)", name, ip, port) + return new_client + except Exception: + pass + logger.warning("%s 重连失败", name) + time_mod.sleep(1) + return None + return client + except Exception as e: + logger.exception("%s 连接检查异常: %s", name, e) + return None + + def safe_read(self, client, name, func, *args, retries=3, delay=0.3, **kwargs): + """兼容 pymodbus 2.x/3.x 的读函数,返回 response 或 None""" + if client is None: + return None + for attempt in range(1, retries + 1): + try: + # adapt args/kwargs for different API styles + args, kwargs = self._adapt_req_kwargs_for_read(func.__name__, args, kwargs) + # unit->slave compatibility + if "unit" in kwargs: + kwargs["slave"] = kwargs.pop("unit") + res = func(*args, **kwargs) + # pymodbus Response 在不同版本表现不同,尽量检测错误 + if res is None: + raise RuntimeError("返回 None") + if hasattr(res, "isError") and res.isError(): + raise RuntimeError("Modbus 返回 isError()") + return res + except Exception as e: + logger.warning("%s 读异常 (尝试 %d/%d): %s", name, attempt, retries, e) + time_mod.sleep(delay) + logger.error("%s 连续读取失败 %d 次", name, retries) + return None + + def safe_write(self, client, name, func, *args, retries=3, delay=0.3, **kwargs): + """兼容 pymodbus 2.x/3.x 的写函数,返回 True/False""" + if client is None: + return False + for attempt in range(1, retries + 1): + try: + args, kwargs = self._adapt_req_kwargs_for_write(func.__name__, args, kwargs) + if "unit" in kwargs: + kwargs["slave"] = kwargs.pop("unit") + res = func(*args, **kwargs) + if res is None: + raise RuntimeError("返回 None") + if hasattr(res, "isError") and res.isError(): + raise RuntimeError("Modbus 返回 isError()") + return True + except Exception as e: + logger.warning("%s 写异常 (尝试 %d/%d): %s", name, attempt, retries, e) + time_mod.sleep(delay) + logger.error("%s 连续写入失败 %d 次", name, retries) + return False + + def wait_with_quit_check(self, robot, seconds, addr_quit=270): + """等待指定时间,同时每 0.2s 检查 R270 是否为 1(立即退出)""" + if robot is None: + time_mod.sleep(seconds) + return False + checks = max(1, int(seconds / 0.2)) + for _ in range(checks): + rr = self.safe_read(robot, "机器人", robot.read_holding_registers, address=addr_quit, count=1) + if rr and getattr(rr, "registers", [None])[0] == 1: + logger.info("检测到 R270=1,立即退出等待") + return True + time_mod.sleep(0.2) + return False + + # ----------------- 主流程 run_once ----------------- + def run_once( + self, + integration_time: str = "5000", + laser_power: str = "200", + save_csv: str = "true", + save_plot: str = "true", + normalize: str = "true", + norm_max: str = "1.0", + **_: Any, + ) -> Dict[str, Any]: + result: Dict[str, Any] = {"success": False, "event": "none", "details": {}} + + integration_time_v = self._str_to_int(integration_time, 5000) + laser_power_v = self._str_to_int(laser_power, 200) + save_csv_v = self._str_to_bool(save_csv, True) + save_plot_v = self._str_to_bool(save_plot, True) + normalize_v = self._str_to_bool(normalize, True) + norm_max_v = None if norm_max in (None, "", "none", "null") else self._str_to_float(norm_max, 1.0) + + if ModbusTcpClient is None: + result["details"]["error"] = "未安装 pymodbus,无法执行连接" + logger.error(result["details"]["error"]) + return result + + # 建立连接 + plc = ModbusTcpClient(self.plc_ip, port=self.plc_port) + robot = ModbusTcpClient(self.robot_ip, port=self.robot_port) + try: + if not plc.connect(): + result["details"]["error"] = "无法连接 PLC" + logger.error(result["details"]["error"]) + return result + if not robot.connect(): + plc.close() + result["details"]["error"] = "无法连接 机器人" + logger.error(result["details"]["error"]) + return result + + logger.info("✅ PLC 与 机器人连接成功") + time_mod.sleep(0.2) + + # 伺服使能 (coil 写示例) + if self.safe_write(plc, "PLC", plc.write_coil, 10, True): + logger.info("✅ 伺服使能成功 (M10=True)") + else: + logger.warning("⚠️ 伺服使能失败") + + # 初始化 CSV 文件 + try: + with open(self.scan_csv_file, "w", newline="", encoding="utf-8") as f: + csv.writer(f).writerow(["Bottle_No", "Scan_Result", "Time"]) + except Exception as e: + logger.warning("⚠️ 初始化CSV失败: %s", e) + + bottle_count = 0 + logger.info("🟢 等待机器人触发信号... (R260=1扫码 / R256=1拉曼 / R270=1退出)") + + # 主循环:仅响应事件(每次循环后短暂 sleep) + while True: + plc = self.ensure_connected(plc, "PLC", self.plc_ip, self.plc_port) or plc + robot = self.ensure_connected(robot, "机器人", self.robot_ip, self.robot_port) or robot + + # 检查退出寄存器 + quit_signal = self.safe_read(robot, "机器人", robot.read_holding_registers, 270, 1) + if quit_signal and getattr(quit_signal, "registers", [None])[0] == 1: + logger.info("🟥 检测到 R270=1,准备退出...") + result["event"] = "quit" + result["success"] = True + break + + # 读取关键寄存器(256..260) + rr = self.safe_read(robot, "机器人", robot.read_holding_registers, 256, 5) + if not rr or not hasattr(rr, "registers"): + time_mod.sleep(0.3) + continue + + r256, r257, r258, r259, r260 = (rr.registers + [0, 0, 0, 0, 0])[:5] + + # ---------- 扫码逻辑 ---------- + if r260 == 1: + bottle_count += 1 + logger.info("📸 第 %d 瓶触发扫码 (R260=1)", bottle_count) + try: + # 调用外部扫码函数(用户实现) + from .dmqfengzhuang import scan_once as scan_once_local + scan_result = scan_once_local(ip="192.168.1.50", port_in=2001, port_out=2002) + if scan_result: + logger.info("✅ 扫码成功: %s", scan_result) + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(self.scan_csv_file, "a", newline="", encoding="utf-8") as f: + csv.writer(f).writerow([bottle_count, scan_result, timestamp]) + else: + logger.warning("⚠️ 扫码失败或无返回") + except Exception as e: + logger.exception("❌ 扫码异常: %s", e) + + # 写 R260->0, R261->1 + self.safe_write(robot, "机器人", robot.write_register, 260, 0) + time_mod.sleep(0.15) + self.safe_write(robot, "机器人", robot.write_register, 261, 1) + logger.info("➡️ 扫码完成 (R260→0, R261→1)") + result["event"] = "scan" + result["success"] = True + + # ---------- 拉曼逻辑 ---------- + if r256 == 1: + logger.info("⚙️ 检测到 R256=1(放瓶完成)") + # PLC 电机右转指令 + self.safe_write(plc, "PLC", plc.write_register, 1199, 1) + self.safe_write(plc, "PLC", plc.write_register, 1200, 1) + logger.info("➡️ 电机右转中...") + if self.wait_with_quit_check(robot, 3): + result["event"] = "quit" + break + self.safe_write(plc, "PLC", plc.write_register, 1199, 0) + logger.info("✅ 电机右转完成") + + # 调用拉曼测试(尽量捕获异常并记录) + logger.info("🧪 开始拉曼测试...") + try: + # 尝试使用模块导入好的 run_raman_test,否则再动态导入 + rr_func = run_raman_test + if rr_func is None: + from raman_module import run_raman_test as rr_func + success, file_prefix, df = rr_func( + integration_time=integration_time_v, + laser_power=laser_power_v, + save_csv=save_csv_v, + save_plot=save_plot_v, + normalize=normalize_v, + norm_max=norm_max_v, + ) + if success: + logger.info("✅ 拉曼测试完成: %s", file_prefix) + result["event"] = "raman" + result["success"] = True + else: + logger.warning("⚠️ 拉曼测试失败") + except Exception as e: + logger.exception("❌ 拉曼模块异常: %s", e) + + # 电机左转回位 + self.safe_write(plc, "PLC", plc.write_register, address=1299, value=1) + self.safe_write(plc, "PLC", plc.write_register, address=1300, value=1) + logger.info("⬅️ 电机左转中...") + if self.wait_with_quit_check(robot, 3): + result["event"] = "quit" + break + self.safe_write(plc, "PLC", plc.write_register, address=1299, value=0) + logger.info("✅ 电机左转完成") + + # 通知机器人拉曼完成 R257=1 + self.safe_write(robot, "机器人", robot.write_register, address=257, value=1) + logger.info("✅ 已写入 R257=1(拉曼完成)") + + # 延迟后清零 R256 + logger.info("⏳ 延迟4秒后清零 R256") + if self.wait_with_quit_check(robot, 4): + result["event"] = "quit" + break + self.safe_write(robot, "机器人", robot.write_register, address=256, value=0) + logger.info("✅ 已清零 R256") + + # 等待机器人清 R257 + logger.info("等待 R257 清零中...") + while True: + rr2 = self.safe_read(robot, "机器人", robot.read_holding_registers, address=257, count=1) + if rr2 and getattr(rr2, "registers", [None])[0] == 0: + logger.info("✅ 检测到 R257=0,准备下一循环") + break + if self.wait_with_quit_check(robot, 1): + result["event"] = "quit" + break + time_mod.sleep(0.2) + + time_mod.sleep(0.25) + + finally: + logger.info("🧹 开始清理...") + try: + self.safe_write(plc, "PLC", plc.write_coil, address=10, value=False) + except Exception: + pass + for addr in [256, 257, 260, 261, 270]: + try: + self.safe_write(robot, "机器人", robot.write_register, address=addr, value=0) + except Exception: + pass + + try: + if plc: + plc.close() + except Exception: + pass + try: + if robot: + robot.close() + except Exception: + pass + logger.info("🔚 已关闭所有连接") + + return result diff --git a/unilabos/devices/opsky_Raman/raman_module.py b/unilabos/devices/opsky_Raman/raman_module.py new file mode 100644 index 00000000..2bf76d27 --- /dev/null +++ b/unilabos/devices/opsky_Raman/raman_module.py @@ -0,0 +1,180 @@ +# raman_module.py +import os +import time as time_mod +import numpy as np +import pandas as pd + +# clr / ATRWrapper 依赖:在真实环境中使用 Windows + .NET wrapper +# 本模块对缺少 clr 或 Wrapper 的情况提供“仿真”回退,方便离线/调试运行。 +try: + import clr + has_clr = True +except Exception: + clr = None + has_clr = False + +# 本函数返回 (success: bool, file_prefix: str|None, df: pandas.DataFrame|None) +def run_raman_test(integration_time=5000, laser_power=200, + save_csv=True, save_plot=True, + normalize=False, norm_max=None, + max_wavenum=1300): + """ + 拉曼测试流程(更稳健的实现): + - 若能加载 ATRWrapper 则使用之 + - 否则生成模拟光谱(方便调试) + 返回 (success, file_prefix, df) + """ + timestamp = time_mod.strftime("%Y%m%d_%H%M%S") + file_prefix = f"raman_{timestamp}" + + wrapper = None + used_real_device = False + + try: + if has_clr: + try: + # 请根据你的 DLL 路径调整 + dll_path = r"D:\Raman\Raman_RS\ATRWrapper\ATRWrapper.dll" + if os.path.exists(dll_path): + clr.AddReference(dll_path) + else: + # 试图直接 AddReference 名称(若已在 PATH) + try: + clr.AddReference("ATRWrapper") + except Exception: + pass + + from Optosky.Wrapper import ATRWrapper # May raise + wrapper = ATRWrapper() + used_real_device = True + except Exception as e: + # 无法加载真实 wrapper -> fallback + print("⚠️ 未能加载 ATRWrapper,使用模拟数据。详细:", e) + wrapper = None + + if wrapper is None: + # 生成模拟光谱(方便调试) + # 模拟波数轴 50..1300 + WaveNum = np.linspace(50, max_wavenum, 1024) + # 合成几条高斯峰 + 噪声 + def gauss(x, mu, sig, A): + return A * np.exp(-0.5 * ((x - mu) / sig) ** 2) + Spect_data = (gauss(WaveNum, 200, 8, 1000) + + gauss(WaveNum, 520, 12, 600) + + gauss(WaveNum, 810, 20, 400) + + 50 * np.random.normal(scale=1.0, size=WaveNum.shape)) + Spect_bLC = Spect_data - np.min(Spect_data) * 0.05 # 简单 baseline + Spect_smooth = np.convolve(Spect_bLC, np.ones(3) / 3, mode="same") + df = pd.DataFrame({ + "WaveNum": WaveNum, + "Raw_Spect": Spect_data, + "BaseLineCorrected": Spect_bLC, + "Smooth_Spect": Spect_smooth + }) + success = True + file_prefix = f"raman_sim_{timestamp}" + # 保存 CSV / 绘图 等同真实设备 + else: + # 使用真实设备 API(根据你提供的 wrapper 调用) + On_flag = wrapper.OpenDevice() + print("通讯连接状态:", On_flag) + if not On_flag: + wrapper.CloseDevice() + return False, None, None + + wrapper.SetIntegrationTime(int(integration_time)) + wrapper.SetLdPower(int(laser_power), 1) + # 可能的冷却设置(如果 wrapper 支持) + try: + wrapper.SetCool(-5) + except Exception: + pass + + Spect = wrapper.AcquireSpectrum() + Spect_data = np.array(Spect.get_Data()) + if not Spect.get_Success(): + print("光谱采集失败") + try: + wrapper.CloseDevice() + except Exception: + pass + return False, None, None + WaveNum = np.array(wrapper.GetWaveNum()) + Spect_bLC = np.array(wrapper.BaseLineCorrect(Spect_data)) + Spect_smooth = np.array(wrapper.SmoothBoxcar(Spect_bLC, 3)) + df = pd.DataFrame({ + "WaveNum": WaveNum, + "Raw_Spect": Spect_data, + "BaseLineCorrected": Spect_bLC, + "Smooth_Spect": Spect_smooth + }) + wrapper.CloseDevice() + success = True + + # 如果需要限定波数范围 + mask = df["WaveNum"] <= max_wavenum + df = df[mask].reset_index(drop=True) + + # 可选归一化 + if normalize: + arr = df["Smooth_Spect"].values + mn, mx = arr.min(), arr.max() + if mx == mn: + df["Smooth_Spect"] = 0.0 + else: + scale = 1.0 if norm_max is None else float(norm_max) + df["Smooth_Spect"] = (arr - mn) / (mx - mn) * scale + # 同时处理其它列(可选) + arr_raw = df["Raw_Spect"].values + mn_r, mx_r = arr_raw.min(), arr_raw.max() + if mx_r == mn_r: + df["Raw_Spect"] = 0.0 + else: + scale = 1.0 if norm_max is None else float(norm_max) + df["Raw_Spect"] = (arr_raw - mn_r) / (mx_r - mn_r) * scale + + # 保存 CSV + if save_csv: + csv_filename = f"{file_prefix}.csv" + df.to_csv(csv_filename, index=False) + print("✅ CSV 文件已生成:", csv_filename) + + # 绘图(使用 matplotlib),注意:不要启用 GUI 后台 + if save_plot: + try: + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + plt.figure(figsize=(8, 5)) + plt.plot(df["WaveNum"], df["Raw_Spect"], linestyle='-', alpha=0.6, label="原始") + plt.plot(df["WaveNum"], df["BaseLineCorrected"], linestyle='--', alpha=0.8, label="基线校正") + plt.plot(df["WaveNum"], df["Smooth_Spect"], linewidth=1.2, label="平滑") + plt.xlabel("WaveNum (cm^-1)") + plt.ylabel("Intensity (a.u.)") + plt.title(f"Raman {file_prefix}") + plt.grid(True) + plt.legend() + plt.tight_layout() + plot_filename = f"{file_prefix}.png" + plt.savefig(plot_filename, dpi=300, bbox_inches="tight") + plt.close() + # 小短暂等待以确保文件系统刷新 + time_mod.sleep(0.2) + print("✅ 图像已生成:", plot_filename) + except Exception as e: + print("⚠️ 绘图失败:", e) + + return success, file_prefix, df + + except Exception as e: + print("拉曼测试异常:", e) + try: + if wrapper is not None: + try: + wrapper.CloseDevice() + except Exception: + pass + except Exception: + pass + return False, None, None diff --git a/unilabos/devices/opsky_Raman/test2.py b/unilabos/devices/opsky_Raman/test2.py new file mode 100644 index 00000000..7646439b --- /dev/null +++ b/unilabos/devices/opsky_Raman/test2.py @@ -0,0 +1,209 @@ +import time +import csv +from datetime import datetime +from pymodbus.client import ModbusTcpClient +from dmqfengzhuang import scan_once +from raman_module import run_raman_test + +# =================== 配置 =================== +PLC_IP = "192.168.1.88" +PLC_PORT = 502 +ROBOT_IP = "192.168.1.200" +ROBOT_PORT = 502 +SCAN_CSV_FILE = "scan_results.csv" + +# =================== 通用函数 =================== +def ensure_connected(client, name, ip, port): + if not client.is_socket_open(): + print(f"{name} 掉线,正在重连...") + client.close() + time.sleep(1) + + new_client = ModbusTcpClient(ip, port=port) + if new_client.connect(): + print(f"{name} 重新连接成功 ({ip}:{port})") + return new_client + else: + print(f"{name} 重连失败,稍后重试...") + time.sleep(3) + return None + return client + +def safe_read(client, name, func, *args, retries=3, delay=0.3, **kwargs): + for _ in range(retries): + try: + res = func(*args, **kwargs) + if res and not (hasattr(res, "isError") and res.isError()): + return res + except Exception as e: + print(f"{name} 读异常: {e}") + time.sleep(delay) + print(f"{name} 连续读取失败 {retries} 次") + return None + +def safe_write(client, name, func, *args, retries=3, delay=0.3, **kwargs): + for _ in range(retries): + try: + res = func(*args, **kwargs) + if res and not (hasattr(res, "isError") and res.isError()): + return True + except Exception as e: + print(f"{name} 写异常: {e}") + time.sleep(delay) + print(f"{name} 连续写入失败 {retries} 次") + return False + +def wait_with_quit_check(robot, seconds, addr_quit=270): + for _ in range(int(seconds / 0.2)): + rr = safe_read(robot, "机器人", robot.read_holding_registers, + address=addr_quit, count=1) + if rr and rr.registers[0] == 1: + print("检测到 R270=1,立即退出循环") + return True + time.sleep(0.2) + return False + +# =================== 初始化 =================== +plc = ModbusTcpClient(PLC_IP, port=PLC_PORT) +robot = ModbusTcpClient(ROBOT_IP, port=ROBOT_PORT) + +if not plc.connect(): + print("无法连接 PLC") + exit() +if not robot.connect(): + print("无法连接 机器人") + plc.close() + exit() + +print("✅ PLC 与 机器人连接成功") +time.sleep(0.5) + +# 伺服使能 +if safe_write(plc, "PLC", plc.write_coil, address=10, value=True): + print("✅ 伺服使能成功 (M10=True)") +else: + print("⚠️ 伺服使能失败") + +# 初始化扫码 CSV +with open(SCAN_CSV_FILE, "w", newline="", encoding="utf-8") as f: + csv.writer(f).writerow(["Bottle_No", "Scan_Result", "Time"]) + +bottle_count = 0 +print("🟢 等待机器人触发信号... (R260=1扫码 / R256=1拉曼 / R270=1退出)") + +# =================== 主监听循环 =================== +while True: + plc = ensure_connected(plc, "PLC", PLC_IP, PLC_PORT) or plc + robot = ensure_connected(robot, "机器人", ROBOT_IP, ROBOT_PORT) or robot + + # 退出命令检测 + quit_signal = safe_read(robot, "机器人", robot.read_holding_registers, + address=270, count=1) + if quit_signal and quit_signal.registers[0] == 1: + print("🟥 检测到 R270=1,准备退出程序...") + break + + # 读取关键寄存器 + rr = safe_read(robot, "机器人", robot.read_holding_registers, + address=256, count=5) + if not rr: + time.sleep(0.3) + continue + + r256, _, r258, r259, r260 = rr.registers[:5] + + # ----------- 扫码部分 (R260=1) ----------- + if r260 == 1: + bottle_count += 1 + print(f"📸 第 {bottle_count} 瓶触发扫码 (R260=1)") + + try: + result = scan_once(ip="192.168.1.50", port_in=2001, port_out=2002) + if result: + print(f"✅ 扫码成功: {result}") + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(SCAN_CSV_FILE, "a", newline="", encoding="utf-8") as f: + csv.writer(f).writerow([bottle_count, result, timestamp]) + else: + print("⚠️ 扫码失败或无返回") + except Exception as e: + print(f"❌ 扫码异常: {e}") + + safe_write(robot, "机器人", robot.write_register, address=260, value=0) + time.sleep(0.2) + safe_write(robot, "机器人", robot.write_register, address=261, value=1) + print("➡️ 扫码完成 (R260→0, R261→1)") + + # ----------- 拉曼 + 电机部分 (R256=1) ----------- + if r256 == 1: + print("⚙️ 检测到 R256=1(放瓶完成)") + + # 电机右转 + safe_write(plc, "PLC", plc.write_register, address=1199, value=1) + safe_write(plc, "PLC", plc.write_register, address=1200, value=1) + print("➡️ 电机右转中...") + if wait_with_quit_check(robot, 3): + break + safe_write(plc, "PLC", plc.write_register, address=1199, value=0) + print("✅ 电机右转完成") + + # 拉曼测试 + print("🧪 开始拉曼测试...") + try: + success, file_prefix, df = run_raman_test( + integration_time=5000, + laser_power=200, + save_csv=True, + save_plot=True, + normalize=True, + norm_max=1.0 + ) + if success: + print(f"✅ 拉曼完成:{file_prefix}.csv / .png") + else: + print("⚠️ 拉曼失败") + except Exception as e: + print(f"❌ 拉曼测试异常: {e}") + + # 电机左转 + safe_write(plc, "PLC", plc.write_register, address=1299, value=1) + safe_write(plc, "PLC", plc.write_register, address=1300, value=1) + print("⬅️ 电机左转中...") + if wait_with_quit_check(robot, 3): + break + safe_write(plc, "PLC", plc.write_register, address=1299, value=0) + print("✅ 电机左转完成") + + # 写入拉曼完成信号 + safe_write(robot, "机器人", robot.write_register, address=257, value=1) + print("✅ 已写入 R257=1(拉曼完成)") + + # 延迟清零 R256 + print("⏳ 延迟4秒后清零 R256") + if wait_with_quit_check(robot, 4): + break + safe_write(robot, "机器人", robot.write_register, address=256, value=0) + print("✅ 已清零 R256") + + # 等待机器人清零 R257 + print("等待 R257 清零中...") + while True: + rr2 = safe_read(robot, "机器人", robot.read_holding_registers, address=257, count=1) + if rr2 and rr2.registers[0] == 0: + print("✅ 检测到 R257=0,准备下一循环") + break + if wait_with_quit_check(robot, 1): + break + time.sleep(0.2) + + time.sleep(0.2) + +# =================== 程序退出清理 =================== +print("🧹 开始清理...") +safe_write(plc, "PLC", plc.write_coil, address=10, value=False) +for addr in [256, 257, 260, 261, 270]: + safe_write(robot, "机器人", robot.write_register, address=addr, value=0) + +plc.close() +robot.close() +print("✅ 程序已退出,设备全部复位。") diff --git a/unilabos/devices/xrd_d7mate/device.json b/unilabos/devices/xrd_d7mate/device.json new file mode 100644 index 00000000..4aebd228 --- /dev/null +++ b/unilabos/devices/xrd_d7mate/device.json @@ -0,0 +1,26 @@ +{ + "nodes": [ + { + "id": "XRD_D7MATE_STATION", + "name": "XRD_D7MATE", + "parent": null, + "type": "device", + "class": "xrd_d7mate", + "position": { + "x": 720.0, + "y": 200.0, + "z": 0 + }, + "config": { + "host": "127.0.0.1", + "port": 6001, + "timeout": 10.0 + }, + "data": { + "input_hint": "start 支持单字符串输入:'sample_name 样品A start_theta 10.0 end_theta 80.0 increment 0.02 exp_time 0.1 [wait_minutes 3]';也支持等号形式 'sample_id=样品A start_theta=10.0 end_theta=80.0 increment=0.02 exp_time=0.1 wait_minutes=3'" + }, + "children": [] + } + ], + "links": [] +} \ No newline at end of file diff --git a/unilabos/devices/xrd_d7mate/xrd_d7mate.py b/unilabos/devices/xrd_d7mate/xrd_d7mate.py new file mode 100644 index 00000000..f68baf4d --- /dev/null +++ b/unilabos/devices/xrd_d7mate/xrd_d7mate.py @@ -0,0 +1,939 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +XRD D7-Mate设备驱动 + +支持XRD D7-Mate设备的TCP通信协议,包括自动模式控制、上样流程、数据获取、下样流程和高压电源控制等功能。 +通信协议版本:1.0.0 +""" + +import json +import socket +import struct +import time +from typing import Dict, List, Optional, Tuple, Any, Union + + +class XRDClient: + def __init__(self, host='127.0.0.1', port=6001, timeout=10.0): + """ + 初始化XRD D7-Mate客户端 + + Args: + host (str): 设备IP地址 + port (int): 通信端口,默认6001 + timeout (float): 超时时间,单位秒 + """ + self.host = host + self.port = port + self.timeout = timeout + self.sock = None + self._ros_node = None # ROS节点引用,由框架设置 + + def post_init(self, ros_node): + """ + ROS节点初始化后的回调方法,保存ROS节点引用但不自动连接 + + Args: + ros_node: ROS节点实例 + """ + self._ros_node = ros_node + ros_node.lab_logger().info(f"XRD D7-Mate设备已初始化,将在需要时连接: {self.host}:{self.port}") + # 不自动连接,只有在调用具体功能时才建立连接 + + def connect(self): + """ + 建立TCP连接到XRD D7-Mate设备 + + Raises: + ConnectionError: 连接失败时抛出 + """ + try: + self.sock = socket.create_connection((self.host, self.port), timeout=self.timeout) + self.sock.settimeout(self.timeout) + except Exception as e: + raise ConnectionError(f"Failed to connect to {self.host}:{self.port} - {str(e)}") + + def close(self): + """ + 关闭与XRD D7-Mate设备的TCP连接 + """ + if self.sock: + try: + self.sock.close() + except Exception: + pass # 忽略关闭时的错误 + finally: + self.sock = None + + def _ensure_connection(self) -> bool: + """ + 确保连接存在,如果不存在则尝试建立连接 + + Returns: + bool: 连接是否成功建立 + """ + if self.sock is None: + try: + self.connect() + return True + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"建立连接失败: {e}") + return False + return True + + def _receive_with_length_prefix(self) -> dict: + """ + 使用长度前缀协议接收数据 + + Returns: + dict: 解析后的JSON响应数据 + + Raises: + ConnectionError: 连接错误 + TimeoutError: 超时错误 + """ + try: + # 首先接收4字节的长度信息 + length_data = bytearray() + while len(length_data) < 4: + chunk = self.sock.recv(4 - len(length_data)) + if not chunk: + raise ConnectionError("Connection closed while receiving length prefix") + length_data.extend(chunk) + + # 解析长度(大端序无符号整数) + data_length = struct.unpack('>I', length_data)[0] + + if self._ros_node: + self._ros_node.lab_logger().info(f"接收到数据长度: {data_length} 字节") + + # 根据长度接收实际数据 + json_data = bytearray() + while len(json_data) < data_length: + remaining = data_length - len(json_data) + chunk = self.sock.recv(min(4096, remaining)) + if not chunk: + raise ConnectionError("Connection closed while receiving JSON data") + json_data.extend(chunk) + + # 解码JSON数据,优先使用UTF-8,失败时尝试GBK + try: + json_str = json_data.decode('utf-8') + except UnicodeDecodeError: + json_str = json_data.decode('gbk') + + # 解析JSON + result = json.loads(json_str) + + if self._ros_node: + self._ros_node.lab_logger().info(f"成功解析JSON响应: {result}") + + return result + + except socket.timeout: + if self._ros_node: + self._ros_node.lab_logger().warning(f"接收超时") + raise TimeoutError(f"recv() timed out after {self.timeout:.1f}s") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"接收数据失败: {e}") + raise ConnectionError(f"Failed to receive data: {str(e)}") + + def _send_command(self, cmd: dict) -> dict: + """ + 使用长度前缀协议发送命令到XRD D7-Mate设备并接收响应 + + Args: + cmd (dict): 要发送的命令字典 + + Returns: + dict: 设备响应的JSON数据 + + Raises: + ConnectionError: 连接错误 + TimeoutError: 超时错误 + """ + # 确保连接存在,如果不存在则建立连接 + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info(f"为命令重新建立连接") + except Exception as e: + raise ConnectionError(f"Failed to establish connection: {str(e)}") + + try: + # 序列化命令为JSON + json_str = json.dumps(cmd, ensure_ascii=False) + payload = json_str.encode('utf-8') + + # 计算JSON数据长度并打包为4字节大端序无符号整数 + length_prefix = struct.pack('>I', len(payload)) + + if self._ros_node: + self._ros_node.lab_logger().info(f"发送JSON命令到XRD D7-Mate: {json_str}") + self._ros_node.lab_logger().info(f"发送数据长度: {len(payload)} 字节") + + # 发送长度前缀 + self.sock.sendall(length_prefix) + + # 发送JSON数据 + self.sock.sendall(payload) + + # 使用长度前缀协议接收响应 + response = self._receive_with_length_prefix() + + return response + + except Exception as e: + # 如果是连接错误,尝试重新连接一次 + if "远程主机强迫关闭了一个现有的连接" in str(e) or "10054" in str(e): + if self._ros_node: + self._ros_node.lab_logger().warning(f"连接被远程主机关闭,尝试重新连接: {e}") + try: + self.close() + self.connect() + # 重新发送命令 + json_str = json.dumps(cmd, ensure_ascii=False) + payload = json_str.encode('utf-8') + if self._ros_node: + self._ros_node.lab_logger().info(f"重新发送JSON命令到XRD D7-Mate: {json_str}") + self.sock.sendall(payload) + + # 重新接收响应 + buffer = bytearray() + start = time.time() + while True: + try: + chunk = self.sock.recv(4096) + if not chunk: + break + buffer.extend(chunk) + + # 尝试解码和解析JSON + try: + text = buffer.decode('utf-8', errors='strict') + text = text.strip() + if text.startswith('{'): + brace_count = 0 + json_end = -1 + for i, char in enumerate(text): + if char == '{': + brace_count += 1 + elif char == '}': + brace_count -= 1 + if brace_count == 0: + json_end = i + 1 + break + if json_end > 0: + text = text[:json_end] + result = json.loads(text) + + if self._ros_node: + self._ros_node.lab_logger().info(f"重连后成功解析JSON响应: {result}") + return result + + except (UnicodeDecodeError, json.JSONDecodeError): + pass + + except socket.timeout: + if self._ros_node: + self._ros_node.lab_logger().warning(f"重连后接收超时") + raise TimeoutError(f"recv() timed out after reconnection") + + if time.time() - start > self.timeout * 2: + raise TimeoutError(f"No complete JSON received after reconnection") + + except Exception as retry_e: + if self._ros_node: + self._ros_node.lab_logger().error(f"重连失败: {retry_e}") + raise ConnectionError(f"Connection retry failed: {str(retry_e)}") + + if isinstance(e, (ConnectionError, TimeoutError)): + raise + else: + raise ConnectionError(f"Command send failed: {str(e)}") + + # ==================== 自动模式控制 ==================== + + def start_auto_mode(self, status: bool) -> dict: + """ + 启动或停止自动模式 + + Args: + status (bool): True-启动自动模式,False-停止自动模式 + + Returns: + dict: 响应结果,包含status、timestamp、message + """ + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}") + return {"status": False, "message": "设备连接异常"} + + try: + # 按协议要求,content 直接为布尔值,使用传入的status参数 + cmd = { + "command": "START_AUTO_MODE", + "content": { + "status": bool(True) + } + } + + if self._ros_node: + self._ros_node.lab_logger().info(f"发送自动模式控制命令: {cmd}") + + response = self._send_command(cmd) + if self._ros_node: + self._ros_node.lab_logger().info(f"收到自动模式控制响应: {response}") + + return response + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"自动模式控制失败: {e}") + return {"status": False, "message": f"自动模式控制失败: {str(e)}"} + + # ==================== 上样流程 ==================== + + def get_sample_request(self) -> dict: + """ + 上样请求,检查是否允许上样 + + Returns: + dict: 响应结果,包含status、timestamp、message + """ + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}") + return {"status": False, "message": "设备连接异常"} + + try: + cmd = { + "command": "GET_SAMPLE_REQUEST", + } + + if self._ros_node: + self._ros_node.lab_logger().info(f"发送上样请求命令: {cmd}") + + response = self._send_command(cmd) + if self._ros_node: + self._ros_node.lab_logger().info(f"收到上样请求响应: {response}") + + return response + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"上样请求失败: {e}") + return {"status": False, "message": f"上样请求失败: {str(e)}"} + + def send_sample_ready(self, sample_id: str, start_theta: float, end_theta: float, + increment: float, exp_time: float) -> dict: + """ + 送样完成后,发送样品信息和采集参数 + + Args: + sample_id (str): 样品标识符 + start_theta (float): 起始角度(≥5°) + end_theta (float): 结束角度(≥5.5°,且必须大于start_theta) + increment (float): 角度增量(≥0.005) + exp_time (float): 曝光时间(0.1-5.0秒) + + Returns: + dict: 响应结果,包含status、timestamp、message等 + """ + # 参数验证 + if start_theta < 5.0: + return {"status": False, "message": "起始角度必须≥5°"} + if end_theta < 5.5: + return {"status": False, "message": "结束角度必须≥5.5°"} + if end_theta <= start_theta: + return {"status": False, "message": "结束角度必须大于起始角度"} + if increment < 0.005: + return {"status": False, "message": "角度增量必须≥0.005"} + if not (0.1 <= exp_time <= 5.0): + return {"status": False, "message": "曝光时间必须在0.1-5.0秒之间"} + + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}") + return {"status": False, "message": "设备连接异常"} + + try: + cmd = { + "command": "SEND_SAMPLE_READY", + "content": { + "sample_id": sample_id, + "start_theta": start_theta, + "end_theta": end_theta, + "increment": increment, + "exp_time": exp_time + } + } + + if self._ros_node: + self._ros_node.lab_logger().info(f"发送样品准备完成命令: {cmd}") + + response = self._send_command(cmd) + if self._ros_node: + self._ros_node.lab_logger().info(f"收到样品准备完成响应: {response}") + + return response + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"样品准备完成失败: {e}") + return {"status": False, "message": f"样品准备完成失败: {str(e)}"} + + # ==================== 数据获取 ==================== + + def get_current_acquire_data(self) -> dict: + """ + 获取当前正在采集的样品数据 + + Returns: + dict: 响应结果,包含status、timestamp、sample_id、Energy、Intensity等 + """ + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}") + return {"status": False, "message": "设备连接异常"} + + try: + cmd = { + "command": "GET_CURRENT_ACQUIRE_DATA", + } + + if self._ros_node: + self._ros_node.lab_logger().info(f"发送获取采集数据命令: {cmd}") + + response = self._send_command(cmd) + if self._ros_node: + self._ros_node.lab_logger().info(f"收到获取采集数据响应: {response}") + + return response + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"获取采集数据失败: {e}") + return {"status": False, "message": f"获取采集数据失败: {str(e)}"} + + def get_sample_status(self) -> dict: + """ + 获取工位样品状态及设备状态 + + Returns: + dict: 响应结果,包含status、timestamp、Station等 + """ + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}") + return {"status": False, "message": "设备连接异常"} + + try: + cmd = { + "command": "GET_SAMPLE_STATUS", + } + + if self._ros_node: + self._ros_node.lab_logger().info(f"发送获取样品状态命令: {cmd}") + + response = self._send_command(cmd) + if self._ros_node: + self._ros_node.lab_logger().info(f"收到获取样品状态响应: {response}") + + return response + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"获取样品状态失败: {e}") + return {"status": False, "message": f"获取样品状态失败: {str(e)}"} + + # ==================== 下样流程 ==================== + + def get_sample_down(self, sample_station: int) -> dict: + """ + 下样请求 + + Args: + sample_station (int): 下样工位(1, 2, 3) + + Returns: + dict: 响应结果,包含status、timestamp、sample_info等 + """ + # 参数验证 + if sample_station not in [1, 2, 3]: + return {"status": False, "message": "下样工位必须是1、2或3"} + + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}") + return {"status": False, "message": "设备连接异常"} + + try: + # 按协议要求,content 直接为整数工位号 + cmd = { + "command": "GET_SAMPLE_DOWN", + "content": { + "Sample station":int(3) + } + } + + if self._ros_node: + self._ros_node.lab_logger().info(f"发送下样请求命令: {cmd}") + + response = self._send_command(cmd) + if self._ros_node: + self._ros_node.lab_logger().info(f"收到下样请求响应: {response}") + + return response + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"下样请求失败: {e}") + return {"status": False, "message": f"下样请求失败: {str(e)}"} + + def send_sample_down_ready(self) -> dict: + """ + 下样完成命令 + + Returns: + dict: 响应结果,包含status、timestamp、message + """ + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}") + return {"status": False, "message": "设备连接异常"} + + try: + cmd = { + "command": "SEND_SAMPLE_DOWN_READY", + } + + if self._ros_node: + self._ros_node.lab_logger().info(f"发送下样完成命令: {cmd}") + + response = self._send_command(cmd) + if self._ros_node: + self._ros_node.lab_logger().info(f"收到下样完成响应: {response}") + + return response + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"下样完成失败: {e}") + return {"status": False, "message": f"下样完成失败: {str(e)}"} + + # ==================== 高压电源控制 ==================== + + def set_power_on(self) -> dict: + """ + 高压电源开启 + + Returns: + dict: 响应结果,包含status、timestamp、message + """ + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}") + return {"status": False, "message": "设备连接异常"} + + try: + cmd = { + "command": "SET_POWER_ON", + } + + if self._ros_node: + self._ros_node.lab_logger().info(f"发送高压电源开启命令: {cmd}") + + response = self._send_command(cmd) + if self._ros_node: + self._ros_node.lab_logger().info(f"收到高压开启响应: {response}") + + return response + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"高压开启失败: {e}") + return {"status": False, "message": f"高压开启失败: {str(e)}"} + + def set_power_off(self) -> dict: + """ + 高压电源关闭 + + Returns: + dict: 响应结果,包含status、timestamp、message + """ + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}") + return {"status": False, "message": "设备连接异常"} + + try: + cmd = { + "command": "SET_POWER_OFF", + } + + if self._ros_node: + self._ros_node.lab_logger().info(f"发送高压电源关闭命令: {cmd}") + + response = self._send_command(cmd) + if self._ros_node: + self._ros_node.lab_logger().info(f"收到高压关闭响应: {response}") + + return response + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"高压关闭失败: {e}") + return {"status": False, "message": f"高压关闭失败: {str(e)}"} + + def set_voltage_current(self, voltage: float, current: float) -> dict: + """ + 设置高压电源电压和电流 + + Args: + voltage (float): 电压值(kV) + current (float): 电流值(mA) + + Returns: + dict: 响应结果,包含status、timestamp、message + """ + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}") + return {"status": False, "message": "设备连接异常"} + + try: + cmd = { + "command": "SET_VOLTAGE_CURRENT", + "content": { + "voltage": voltage, + "current": current + } + } + + if self._ros_node: + self._ros_node.lab_logger().info(f"发送设置电压电流命令: {cmd}") + + response = self._send_command(cmd) + if self._ros_node: + self._ros_node.lab_logger().info(f"收到设置电压电流响应: {response}") + + return response + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"设置电压电流失败: {e}") + return {"status": False, "message": f"设置电压电流失败: {str(e)}"} + + def start(self, sample_id: str = "", start_theta: float = 10.0, end_theta: float = 80.0, + increment: float = 0.05, exp_time: float = 0.1, wait_minutes: float = 3.0, + string: str = "") -> dict: + """ + Start 主流程: + 1) 启动自动模式; + 2) 发送上样请求并等待允许; + 3) 等待指定分钟后发送样品准备完成(携带采集参数); + 4) 周期性轮询采集数据与工位状态; + 5) 一旦任一下样位变为 True,执行下样流程(GET_SAMPLE_DOWN + SEND_SAMPLE_DOWN_READY)。 + + Args: + sample_id: 样品名称 + start_theta: 起始角度(≥5°) + end_theta: 结束角度(≥5.5°,且必须大于 start_theta) + increment: 角度增量(≥0.005) + exp_time: 曝光时间(0.1-5.0 秒) + wait_minutes: 在允许上样后、发送样品准备完成前的等待分钟数(默认 3 分钟) + string: 字符串格式的参数输入,如果提供则优先解析使用 + + Returns: + dict: {"return_info": str, "success": bool} + """ + try: + # 强制类型转换:除 sample_id 外的所有输入均转换为 float(若为字符串) + def _to_float(v, default): + try: + return float(v) + except (TypeError, ValueError): + return float(default) + + if not isinstance(sample_id, str): + sample_id = str(sample_id) + if isinstance(start_theta, str): + start_theta = _to_float(start_theta, 10.0) + if isinstance(end_theta, str): + end_theta = _to_float(end_theta, 80.0) + if isinstance(increment, str): + increment = _to_float(increment, 0.05) + if isinstance(exp_time, str): + exp_time = _to_float(exp_time, 0.1) + if isinstance(wait_minutes, str): + wait_minutes = _to_float(wait_minutes, 3.0) + + # 不再从 string 参数解析覆盖;保留参数但忽略字符串解析,统一使用结构化输入 + + # 确保设备连接 + if not self.sock: + try: + self.connect() + if self._ros_node: + self._ros_node.lab_logger().info("XRD D7-Mate设备连接成功,开始执行start流程") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"XRD D7-Mate设备连接失败: {e}") + return {"return_info": f"设备连接失败: {str(e)}", "success": False} + + # 1) 启动自动模式 + r_auto = self.start_auto_mode(True) + if not r_auto.get("status", False): + return {"return_info": f"启动自动模式失败: {r_auto.get('message', '未知')}", "success": False} + if self._ros_node: + self._ros_node.lab_logger().info(f"自动模式已启动: {r_auto}") + + # 2) 上样请求 + r_req = self.get_sample_request() + if not r_req.get("status", False): + return {"return_info": f"上样请求未允许: {r_req.get('message', '未知')}", "success": False} + if self._ros_node: + self._ros_node.lab_logger().info(f"上样已允许: {r_req}") + + # 3) 等待指定分钟后发送样品准备完成 + wait_seconds = max(0.0, float(wait_minutes)) * 60.0 + if self._ros_node: + self._ros_node.lab_logger().info(f"等待 {wait_minutes} 分钟后发送样品准备完成") + time.sleep(wait_seconds) + + r_ready = self.send_sample_ready(sample_id=sample_id, + start_theta=start_theta, + end_theta=end_theta, + increment=increment, + exp_time=exp_time) + if not r_ready.get("status", False): + return {"return_info": f"样品准备完成失败: {r_ready.get('message', '未知')}", "success": False} + if self._ros_node: + self._ros_node.lab_logger().info(f"样品准备完成已发送: {r_ready}") + + # 4) 轮询采集数据与工位状态 + polling_interval = 5.0 # 秒 + down_station_idx: Optional[int] = None + while True: + try: + r_data = self.get_current_acquire_data() + if self._ros_node: + self._ros_node.lab_logger().info(f"采集中数据: {r_data}") + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"获取采集数据失败: {e}") + + try: + r_status = self.get_sample_status() + if self._ros_node: + self._ros_node.lab_logger().info(f"工位状态: {r_status}") + + station = r_status.get("Station", {}) + if isinstance(station, dict): + for idx in (1, 2, 3): + key = f"DownStation{idx}" + val = station.get(key) + if isinstance(val, bool) and val: + down_station_idx = idx + break + if down_station_idx is not None: + break + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().warning(f"获取工位状态失败: {e}") + + time.sleep(polling_interval) + + if down_station_idx is None: + return {"return_info": "未检测到任一下样位 True,流程未完成", "success": False} + + # 5) 下样流程 + r_down = self.get_sample_down(down_station_idx) + if not r_down.get("status", False): + return {"return_info": f"下样请求失败(工位 {down_station_idx}): {r_down.get('message', '未知')}", "success": False} + if self._ros_node: + self._ros_node.lab_logger().info(f"下样请求成功(工位 {down_station_idx}): {r_down}") + + r_ready_down = self.send_sample_down_ready() + if not r_ready_down.get("status", False): + return {"return_info": f"下样完成发送失败: {r_ready_down.get('message', '未知')}", "success": False} + if self._ros_node: + self._ros_node.lab_logger().info(f"下样完成已发送: {r_ready_down}") + + return {"return_info": f"Start流程完成,工位 {down_station_idx} 已下样", "success": True} + + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"Start流程异常: {e}") + return {"return_info": f"Start流程异常: {str(e)}", "success": False} + + def _parse_start_params(self, params: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """ + 解析UI输入参数为 Start 流程参数。 + - 从UI字典中读取各个字段的字符串值 + - 将数值字段从字符串转换为 float 类型 + - 保留 sample_id 为字符串类型 + + 返回: + dict: {sample_id, start_theta, end_theta, increment, exp_time, wait_minutes} + """ + # 如果传入为字典,则直接按键读取;否则给出警告并使用空字典 + if isinstance(params, dict): + p = params + else: + p = {} + if self._ros_node: + self._ros_node.lab_logger().warning("start 参数应为结构化字典") + + def _to_float(v, default): + """将UI输入的字符串值转换为float,处理空值和无效值""" + if v is None or v == '': + return float(default) + try: + # 处理字符串输入(来自UI) + if isinstance(v, str): + v = v.strip() + if v == '': + return float(default) + return float(v) + except (TypeError, ValueError): + return float(default) + + # 从UI输入字典中读取参数 + sample_id = p.get('sample_id') or p.get('sample_name') or '样品名称' + if not isinstance(sample_id, str): + sample_id = str(sample_id) + + # 将UI字符串输入转换为float + result: Dict[str, Any] = { + 'sample_id': sample_id, + 'start_theta': _to_float(p.get('start_theta'), 10.0), + 'end_theta': _to_float(p.get('end_theta'), 80.0), + 'increment': _to_float(p.get('increment'), 0.05), + 'exp_time': _to_float(p.get('exp_time'), 0.1), + 'wait_minutes': _to_float(p.get('wait_minutes'), 3.0), + } + + return result + + def start_from_string(self, params: Union[str, Dict[str, Any]]) -> dict: + """ + 从UI输入参数执行 Start 主流程。 + 接收来自用户界面的参数字典,其中数值字段为字符串格式,自动转换为正确的类型。 + + 参数: + params: UI输入参数字典,例如: + { + 'sample_id': 'teste', + 'start_theta': '10.0', # UI字符串输入 + 'end_theta': '25.0', # UI字符串输入 + 'increment': '0.05', # UI字符串输入 + 'exp_time': '0.10', # UI字符串输入 + 'wait_minutes': '0.5' # UI字符串输入 + } + + 返回: + dict: 执行结果 + """ + parsed = self._parse_start_params(params) + + sample_id = parsed.get('sample_id', '样品名称') + start_theta = float(parsed.get('start_theta', 10.0)) + end_theta = float(parsed.get('end_theta', 80.0)) + increment = float(parsed.get('increment', 0.05)) + exp_time = float(parsed.get('exp_time', 0.1)) + wait_minutes = float(parsed.get('wait_minutes', 3.0)) + + return self.start( + sample_id=sample_id, + start_theta=start_theta, + end_theta=end_theta, + increment=increment, + exp_time=exp_time, + wait_minutes=wait_minutes, + ) + +# 测试函数 +def test_xrd_client(): + """ + 测试XRD客户端功能 + """ + client = XRDClient(host='127.0.0.1', port=6001) + + try: + # 测试连接 + client.connect() + print("连接成功") + + # 测试启动自动模式 + result = client.start_auto_mode(True) + print(f"启动自动模式: {result}") + + # 测试上样请求 + result = client.get_sample_request() + print(f"上样请求: {result}") + + # 测试获取样品状态 + result = client.get_sample_status() + print(f"样品状态: {result}") + + # 测试高压开启 + result = client.set_power_on() + print(f"高压开启: {result}") + + except Exception as e: + print(f"测试失败: {e}") + finally: + client.close() + + +if __name__ == "__main__": + test_xrd_client() + +# 为了兼容性,提供别名 +XRD_D7Mate = XRDClient \ No newline at end of file diff --git a/unilabos/registry/devices/opsky_ATR30007.yaml b/unilabos/registry/devices/opsky_ATR30007.yaml new file mode 100644 index 00000000..a2a1532e --- /dev/null +++ b/unilabos/registry/devices/opsky_ATR30007.yaml @@ -0,0 +1,86 @@ +opsky_ATR30007: + category: + - characterization_optic + - opsky_ATR30007 + class: + action_value_mappings: + auto-run_once: + feedback: {} + goal: {} + goal_default: + integration_time: '5000' + laser_power: '200' + norm_max: '1.0' + normalize: 'true' + save_csv: 'true' + save_plot: 'true' + handles: {} + result: {} + schema: + description: 执行一次站控-扫码-拉曼流程的大函数入口,参数以字符串形式传入。 + properties: + feedback: {} + goal: + properties: + integration_time: + default: '5000' + type: string + laser_power: + default: '200' + type: string + norm_max: + default: '1.0' + type: string + normalize: + default: 'true' + type: string + save_csv: + default: 'true' + type: string + save_plot: + default: 'true' + type: string + required: [] + type: object + result: {} + required: [] + title: run_once 参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.opsky_Raman.opsky_ATR30007:opsky_ATR30007 + status_types: {} + type: python + config_info: [] + description: OPSKY ATR30007 光纤拉曼模块,提供单一入口大函数以执行一次完整流程。 + handles: [] + icon: '' + init_param_schema: + config: + properties: + plc_ip: + default: 192.168.1.88 + type: string + plc_port: + default: 502 + type: integer + robot_ip: + default: 192.168.1.200 + type: string + robot_port: + default: 502 + type: integer + scan_csv_file: + default: scan_results.csv + type: string + required: + - plc_ip + - plc_port + - robot_ip + - robot_port + - scan_csv_file + type: object + data: + properties: {} + required: [] + type: object + version: 1.0.0 diff --git a/unilabos/registry/devices/xrd_d7mate.yaml b/unilabos/registry/devices/xrd_d7mate.yaml new file mode 100644 index 00000000..9e6ee670 --- /dev/null +++ b/unilabos/registry/devices/xrd_d7mate.yaml @@ -0,0 +1,557 @@ +xrd_d7mate: + category: + - xrd_d7mate + class: + action_value_mappings: + auto-close: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: 安全关闭与XRD D7-Mate设备的TCP连接,释放网络资源。 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: close参数 + type: object + type: UniLabJsonCommand + auto-connect: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: 与XRD D7-Mate设备建立TCP连接,配置超时参数。 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: connect参数 + type: object + type: UniLabJsonCommand + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: string + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + get_current_acquire_data: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + get_sample_down: + feedback: {} + goal: + sample_station: 1 + goal_default: + int_input: 0 + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: IntSingleInput_Feedback + type: object + goal: + properties: + int_input: + maximum: 2147483647 + minimum: -2147483648 + type: integer + required: + - int_input + title: IntSingleInput_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: IntSingleInput_Result + type: object + required: + - goal + title: IntSingleInput + type: object + type: IntSingleInput + get_sample_request: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + get_sample_status: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + send_sample_down_ready: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + send_sample_ready: + feedback: {} + goal: + end_theta: 80.0 + exp_time: 0.5 + increment: 0.02 + sample_id: '' + start_theta: 10.0 + goal_default: + end_theta: 80.0 + exp_time: 0.5 + increment: 0.02 + sample_id: Sample001 + start_theta: 10.0 + handles: {} + result: {} + schema: + description: 送样完成后,发送样品信息和采集参数 + properties: + feedback: + properties: {} + required: [] + title: SampleReadyInput_Feedback + type: object + goal: + properties: + end_theta: + description: 结束角度(≥5.5°,且必须大于start_theta) + minimum: 5.5 + type: number + exp_time: + description: 曝光时间(0.1-5.0秒) + maximum: 5.0 + minimum: 0.1 + type: number + increment: + description: 角度增量(≥0.005) + minimum: 0.005 + type: number + sample_id: + description: 样品标识符 + type: string + start_theta: + description: 起始角度(≥5°) + minimum: 5.0 + type: number + required: + - sample_id + - start_theta + - end_theta + - increment + - exp_time + title: SampleReadyInput_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: SampleReadyInput_Result + type: object + required: + - goal + title: SampleReadyInput + type: object + type: UniLabJsonCommand + set_power_off: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + set_power_on: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + set_voltage_current: + feedback: {} + goal: + current: 30.0 + voltage: 40.0 + goal_default: + current: 30.0 + voltage: 40.0 + handles: {} + result: {} + schema: + description: 设置高压电源电压和电流 + properties: + feedback: + properties: {} + required: [] + title: VoltageCurrentInput_Feedback + type: object + goal: + properties: + current: + description: 电流值(mA) + type: number + voltage: + description: 电压值(kV) + type: number + required: + - voltage + - current + title: VoltageCurrentInput_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: VoltageCurrentInput_Result + type: object + required: + - goal + title: VoltageCurrentInput + type: object + type: UniLabJsonCommand + start: + feedback: {} + goal: {} + goal_default: + end_theta: 80.0 + exp_time: 0.1 + increment: 0.05 + sample_id: 样品名称 + start_theta: 10.0 + string: '' + wait_minutes: 3.0 + handles: {} + result: {} + schema: + description: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。 + properties: + feedback: {} + goal: + properties: + end_theta: + description: 结束角度(≥5.5°,且必须大于start_theta) + minimum: 5.5 + type: string + exp_time: + description: 曝光时间(0.1-5.0秒) + maximum: 5.0 + minimum: 0.1 + type: string + increment: + description: 角度增量(≥0.005) + minimum: 0.005 + type: string + sample_id: + description: 样品标识符 + type: string + start_theta: + description: 起始角度(≥5°) + minimum: 5.0 + type: string + string: + description: 字符串格式的参数输入,如果提供则优先解析使用 + type: string + wait_minutes: + description: 允许上样后等待分钟数 + minimum: 0.0 + type: number + required: + - sample_id + - start_theta + - end_theta + - increment + - exp_time + title: StartWorkflow_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: StartWorkflow_Result + type: object + required: + - goal + title: StartWorkflow + type: object + type: UniLabJsonCommand + start_auto_mode: + feedback: {} + goal: + status: true + goal_default: + status: true + handles: {} + result: {} + schema: + description: 启动或停止自动模式 + properties: + feedback: + properties: {} + required: [] + title: BoolSingleInput_Feedback + type: object + goal: + properties: + status: + description: True-启动自动模式,False-停止自动模式 + type: boolean + required: + - status + title: BoolSingleInput_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: BoolSingleInput_Result + type: object + required: + - goal + title: BoolSingleInput + type: object + type: UniLabJsonCommand + module: unilabos.devices.xrd_d7mate.xrd_d7mate:XRDClient + status_types: {} + type: python + config_info: [] + description: XRD D7-Mate X射线衍射分析设备,通过TCP通信实现远程控制与状态监控,支持自动模式控制、上样流程、数据获取、下样流程和高压电源控制等功能。 + handles: [] + icon: '' + init_param_schema: + config: + properties: + host: + default: 127.0.0.1 + type: string + port: + default: 6001 + type: string + timeout: + default: 10.0 + type: string + required: [] + type: object + data: + properties: {} + required: [] + type: object + version: 1.0.0