#!/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)