拯救迷失的荧光溶解氧传感器:从“三无”到“复活”的全记录

0 阅读4分钟

📖 背景:尘封的挑战

手里有一个荧光溶解氧(DO)探头,因为存放时间太久,所有相关信息都丢失了:没有文档、忘记了指令、甚至连 RS485 的 A/B 线序都分不清楚。这在硬件开发中是典型的“死局”。

🛠️ 第一阶段:物理探测与初步扫描

线序好解决,大不了对调尝试。真正的难点在于波特率设备地址(Slave ID)

我借助了 AI 工具 OpenClaw 生成了一段暴力扫描代码,重点探测工业设备最常用的波特率(9600, 4800等)以及几个厂商预设的 ID(1, 2, 4, 8, 255)。

初战告捷:

在整整 3 个多小时的反复调换和测试后,终于在 9600 8N1 和地址 0xFF 下看到了回传:

发送: FF 03 00 00 00 08 51 D2

响应: FF 03 08 26 00 00 00 00 00 81 D7

🕯️ 第二阶段:深夜的挫败与转机

虽然有了响应,但无论把传感器放进冷水还是热水,读出来的数据始终一模一样。

当时已经是深夜,研究了很久却毫无进展,我一度怀疑传感器已经损坏,甚至打算放弃。

💡 第三阶段:洞察真相——“广播地址”的陷阱

第二天复盘代码时,我意识到一个关键问题:之前的代码只扫描了几个“特定”地址。0xFF (255) 虽然能通,但它只是一个通用广播地址,并不一定是设备采集数据时的真实地址。

于是我修改逻辑:从 0 到 255,全地址地毯式扫描。

很快,在地址 0x26 上,我捕捉到了完全不同的数据流:

发送: 26 03 00 00 00 08 42 DB

响应: 26 03 08 D9 94 A0 40 10 A6 BB 41

🧪 第四阶段:指令逻辑解析

通过对比 0xFF 和 0x26 的返回数据,我理清了这个传感器的通信逻辑:

  1. 0xFF(广播地址): 用于探测设备,并引导出设备的真实 ID。
  2. 0x26(真实地址): 真正的业务地址,通过它才能读到实时的传感器物理量。

利用 struct 库解析这串十六进制数据,成功换算出:

  • 溶解氧: 5.018 mg/L

  • 温度: 23.45 °C

    数值完全符合当前环境,传感器就地复活!


🚀 最终复活代码(生产就绪版)

import serial
import struct
import time

# --- 硬件环境配置 ---
PORT = '/dev/cu.usbserial-120'
BAUD = 9600
TIMEOUT = 1.0

class DOSensor:
    def __init__(self, port):
        self.ser = serial.Serial(port, BAUD, timeout=TIMEOUT)
        self.slave_id = 0xFF 

    @staticmethod
    def crc16(data):
        """标准 Modbus RTU CRC16 校验"""
        crc = 0xFFFF
        for pos in data:
            crc ^= pos
            for _ in range(8):
                if (crc & 1) != 0:
                    crc >>= 1
                    crc ^= 0xA001
                else:
                    crc >>= 1
        return crc.to_bytes(2, 'little')

    def find_slave_id(self):
        """利用广播地址 0xFF 自动获取设备真实 Slave ID"""
        print("🔍 正在探测设备真实 ID...")
        req = b'\xFF\x03\x00\x00\x00\x01'
        req += self.crc16(req)

        self.ser.reset_input_buffer()
        self.ser.write(req)
        time.sleep(0.5)

        res = self.ser.read(self.ser.in_waiting)
        if len(res) >= 5:
            # 核心发现:响应帧的第一个字节通常就是真实 ID
            self.slave_id = res[0]
            print(f"✅ 探测成功!设备真实 ID: 0x{self.slave_id:02X}")
            return True
        print("❌ 探测失败,请检查接线。")
        return False

    def read_data(self):
        """读取并解析实时溶解氧与温度数据"""
        # 使用探测到的真实 ID 构造读取指令 (0x0000 寄存器,长度 8)
        req = bytes([self.slave_id, 0x03, 0x00, 0x00, 0x00, 0x08])
        req += self.crc16(req)

        self.ser.reset_input_buffer()
        self.ser.write(req)
        time.sleep(0.5)

        res = self.ser.read(self.ser.in_waiting)
        if len(res) >= 13:
            raw_data = res[3:11]
            # 解析 Little-endian Float32 数据
            do_val = struct.unpack('<f', raw_data[0:4])[0]
            temp_val = struct.unpack('<f', raw_data[4:8])[0]
            return do_val, temp_val
        return None

def main():
    sensor = DOSensor(PORT)
    if not sensor.find_slave_id(): return

    print("\n🚀 开始实时数据采集...")
    try:
        while True:
            result = sensor.read_data()
            if result:
                do, temp = result
                print(f"[{time.strftime('%H:%M:%S')}] 🌊 溶解氧: {do:.3f} mg/L | 🌡️ 温度: {temp:.2f} °C")
            time.sleep(2)
    except KeyboardInterrupt:
        print("\n👋 已停止采集。")
    finally:
        sensor.ser.close()

if __name__ == "__main__":
    main()

📝 经验总结:

  • 不要轻信广播地址: 广播地址能通并不代表能读到正确数据,它往往只是一个“寻人启事”。
  • 坚持就是胜利: 硬件调试中,从“怀疑坏了”到“完美复活”,往往只差一个全地址扫描。
  • 善用 AI 辅助: 编写这种底层协议解析和暴力扫描脚本,AI 极大地节省了查阅 Modbus 规范的时间。