📖 背景:尘封的挑战
手里有一个荧光溶解氧(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 的返回数据,我理清了这个传感器的通信逻辑:
- 0xFF(广播地址): 用于探测设备,并引导出设备的真实 ID。
- 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 规范的时间。