108 lines
4.0 KiB
Python
108 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""dlpc_rs485 协议自检(无硬件): 对照上游 protocols.md 的实测帧向量做回归
|
||
|
||
用法: python selftest_rs485.py (全部通过退出码 0)
|
||
"""
|
||
import sys
|
||
|
||
import dlpc_rs485 as d
|
||
|
||
fails = []
|
||
|
||
|
||
def check(name, got, want):
|
||
ok = got == want
|
||
print(('PASS' if ok else 'FAIL'), name, '->', got, '' if ok else f'(期望 {want})')
|
||
if not ok:
|
||
fails.append(name)
|
||
|
||
|
||
# 1) 帧构造: protocols.md 实测示例 M730S0T2000
|
||
f = d.build_frame('M730S0T2000')
|
||
check('build_frame(M730S0T2000)', f.hex(' ').upper(),
|
||
'D5 01 0B 4D 37 33 30 53 30 54 32 30 30 30 8C')
|
||
|
||
# 2) 增量解析: ok 应答跨两个半包喂入
|
||
p = d.FrameParser()
|
||
r1 = p.feed(bytes.fromhex('B5 F0'))
|
||
r2 = p.feed(bytes.fromhex('02 6F 6B CC'))
|
||
check('split-feed frames', len(r1) + len(r2), 1)
|
||
fr = (r1 + r2)[0]
|
||
check('type_code', fr.type_code, 0xF0)
|
||
check('payload', fr.payload, 'ok')
|
||
check('describe(ok)', d.describe(fr), 'ok')
|
||
|
||
# 3) 前导噪声 + 0xFD err:OL 译码(字符串标记, 不附 DLPC 释义)
|
||
body = bytes.fromhex('FD 06 65 72 72 3A 4F 4C') # "err:OL"
|
||
good = bytes([0xB5]) + body + bytes([d.sum_crc(body)])
|
||
frames = d.FrameParser().feed(b'\x01\x02' + good)
|
||
check('noise+err-frame count', len(frames), 1)
|
||
check('describe(err:OL)', d.describe(frames[0]), '参数越界/读回复核不一致 err:OL')
|
||
|
||
# 4) 0xFA + 数字错误码 err:3 追加 DLPC 释义
|
||
body = bytes.fromhex('FA 05') + b'err:3'
|
||
fa = bytes([0xB5]) + body + bytes([d.sum_crc(body)])
|
||
frames = d.FrameParser().feed(fa)
|
||
check('describe(err:3)', d.describe(frames[0]),
|
||
'开光机通信失败 err:3(I2C NACK(器件无应答))')
|
||
|
||
# 5) CRC 坏帧被丢弃后继续同步到下一帧
|
||
bad = bytearray(good)
|
||
bad[-1] ^= 0xFF
|
||
frames = d.FrameParser().feed(bytes(bad) + good)
|
||
check('bad-crc then good', len(frames), 1)
|
||
check('recovered type', frames[0].type_code, 0xFD)
|
||
|
||
# 6) 非法输入拒绝
|
||
for bad_cmd in ('', ' '):
|
||
try:
|
||
d.build_frame(bad_cmd)
|
||
fails.append('empty-cmd-not-rejected')
|
||
except ValueError:
|
||
pass
|
||
try:
|
||
d.build_frame('M' * 40)
|
||
fails.append('long-cmd-not-rejected')
|
||
except ValueError:
|
||
pass
|
||
print('空/超长命令拒绝: PASS')
|
||
|
||
# 7) 预设命令全部可组帧
|
||
for _name, cmd, _note in d.PRESETS:
|
||
d.build_frame(cmd)
|
||
print(f'预设 {len(d.PRESETS)} 条全部可组帧: PASS')
|
||
|
||
# 8) M303/M304(SQ52205 电源实测)回归向量: 上游真机抓取帧
|
||
check('build_frame(M303)', d.build_frame('M303').hex(' ').upper(),
|
||
'D5 01 04 4D 33 30 33 E8')
|
||
check('build_frame(M304)', d.build_frame('M304').hex(' ').upper(),
|
||
'D5 01 04 4D 33 30 34 E9')
|
||
frames = d.FrameParser().feed(bytes.fromhex('B5 F0 02 2E F4 14'))
|
||
check('M303 应答帧', len(frames), 1)
|
||
check('M303 电压(0x2EF4=12020mV)', d.format_power('M303', frames[0]),
|
||
'12020 mV (12.02 V)')
|
||
frames = d.FrameParser().feed(bytes.fromhex('B5 F0 02 00 34 26'))
|
||
check('M304 应答帧', len(frames), 1)
|
||
check('M304 电流(0x0034=+52mA)', d.format_power('M304', frames[0]), '+52 mA')
|
||
check('M304 负值(FF06=-250mA 补码)', d.payload_s16_be(bytes.fromhex('FF 06')), -250)
|
||
|
||
# 9) M305(SQ52205 实测功率)回归向量: u32 大端 mW(LEN=4, 算法推演帧)
|
||
check('build_frame(M305)', d.build_frame('M305').hex(' ').upper(),
|
||
'D5 01 04 4D 33 30 35 EA')
|
||
frames = d.FrameParser().feed(bytes.fromhex('B5 F0 04 00 00 8C A0 20'))
|
||
check('M305 应答帧', len(frames), 1)
|
||
check('M305 功率(0x8CA0=36000mW)', d.format_power('M305', frames[0]),
|
||
'36000 mW (36.00 W)')
|
||
check('M305 拒绝双字节载荷', d.format_power(
|
||
'M305', d.FrameParser().feed(bytes.fromhex('B5 F0 02 8C A0 1E'))[0]), None)
|
||
|
||
# 10) 二进制载荷的日志行安全: 无控制字符(NUL 会截断 Windows 剪贴板), 以 hex 展示
|
||
frames = d.FrameParser().feed(bytes.fromhex('B5 F0 02 00 34 26')) # M304 应答(含 NUL)
|
||
desc = d.describe(frames[0])
|
||
check('二进制载荷 hex 展示', desc, '[二进制载荷 00 34]')
|
||
check('日志行无控制字符', any(ord(c) < 0x20 for c in desc), False)
|
||
|
||
print('RESULT:', 'FAIL:' + ','.join(fails) if fails else 'ALL PASS')
|
||
sys.exit(1 if fails else 0)
|