feat: add relay status query and mesh test cli
This commit is contained in:
@@ -61,6 +61,11 @@ _App_Reset:
|
|||||||
bset 0x5007, #5
|
bset 0x5007, #5
|
||||||
bset 0x5008, #5
|
bset 0x5008, #5
|
||||||
bres 0x5009, #5
|
bres 0x5009, #5
|
||||||
|
; PA2/PWRC:低电平进入JDY-28M AT指令窗口,高电平恢复Mesh通信。
|
||||||
|
bres 0x5000, #2
|
||||||
|
bset 0x5002, #2
|
||||||
|
bset 0x5003, #2
|
||||||
|
bres 0x5004, #2
|
||||||
|
|
||||||
; TIM4:16MHz/128/125,目标约1ms更新一次。
|
; TIM4:16MHz/128/125,目标约1ms更新一次。
|
||||||
mov 0x5340, #0x00
|
mov 0x5340, #0x00
|
||||||
@@ -91,6 +96,13 @@ _App_Reset:
|
|||||||
; 防止刚开启接收中断时立即进入一次“伪接收”或把残留字节当作有效数据。
|
; 防止刚开启接收中断时立即进入一次“伪接收”或把残留字节当作有效数据。
|
||||||
ld a, 0x5230
|
ld a, 0x5230
|
||||||
ld a, 0x5231
|
ld a, 0x5231
|
||||||
|
; 地址查询阶段先使用普通收发模式,避免AT响应进入Mesh环形缓冲区。
|
||||||
|
mov 0x5235, #0x0C
|
||||||
|
; PWRC拉低后等待约10ms,给JDY-28M预留AT指令窗口建立时间。
|
||||||
|
call _App_Delay10ms
|
||||||
|
call _App_QueryModuleAddress
|
||||||
|
; 查询结束后释放PWRC,模块恢复正常Mesh通信。
|
||||||
|
bset 0x5000, #2
|
||||||
; UART接收环形缓冲区使用RAM 0x0020~0x002F,共16字节。
|
; UART接收环形缓冲区使用RAM 0x0020~0x002F,共16字节。
|
||||||
; 0x0030为写索引,0x0031为读索引,0x0032为溢出计数。
|
; 0x0030为写索引,0x0031为读索引,0x0032为溢出计数。
|
||||||
mov 0x0030, #0x00
|
mov 0x0030, #0x00
|
||||||
@@ -139,6 +151,157 @@ _App_MainLoop:
|
|||||||
mov 0x0036, #0x00
|
mov 0x0036, #0x00
|
||||||
jra _App_MainLoop
|
jra _App_MainLoop
|
||||||
|
|
||||||
|
;-------------------------------------------------------------------------------
|
||||||
|
; 查询JDY-28M本机Mesh短地址。
|
||||||
|
; 发送AT+MADDR\r\n,解析+MADDR=XXXX。地址保存到0x0046/0x0047,
|
||||||
|
; 0x004A为有效标志。查询失败时地址标志保持0,不阻止后续Mesh运行。
|
||||||
|
;-------------------------------------------------------------------------------
|
||||||
|
_App_QueryModuleAddress:
|
||||||
|
mov 0x0048, #0x00
|
||||||
|
mov 0x0049, #0x00
|
||||||
|
mov 0x004A, #0x00
|
||||||
|
mov 0x003E, #0x41
|
||||||
|
call _App_SendByte
|
||||||
|
mov 0x003E, #0x54
|
||||||
|
call _App_SendByte
|
||||||
|
mov 0x003E, #0x2B
|
||||||
|
call _App_SendByte
|
||||||
|
mov 0x003E, #0x4D
|
||||||
|
call _App_SendByte
|
||||||
|
mov 0x003E, #0x41
|
||||||
|
call _App_SendByte
|
||||||
|
mov 0x003E, #0x44
|
||||||
|
call _App_SendByte
|
||||||
|
mov 0x003E, #0x44
|
||||||
|
call _App_SendByte
|
||||||
|
mov 0x003E, #0x52
|
||||||
|
call _App_SendByte
|
||||||
|
mov 0x003E, #0x0D
|
||||||
|
call _App_SendByte
|
||||||
|
mov 0x003E, #0x0A
|
||||||
|
call _App_SendByte
|
||||||
|
ldw x, #0xFFFF
|
||||||
|
_App_AddressWait:
|
||||||
|
ld a, 0x5230
|
||||||
|
and a, #0x20
|
||||||
|
jrne _App_AddressRead
|
||||||
|
decw x
|
||||||
|
jrne _App_AddressWait
|
||||||
|
ret
|
||||||
|
_App_AddressRead:
|
||||||
|
ld a, 0x5231
|
||||||
|
ld 0x003E, a
|
||||||
|
call _App_AddressParseByte
|
||||||
|
tnz 0x004A
|
||||||
|
jrne _App_AddressDone
|
||||||
|
decw x
|
||||||
|
jrne _App_AddressWait
|
||||||
|
ret
|
||||||
|
_App_AddressDone:
|
||||||
|
ret
|
||||||
|
|
||||||
|
; 解析单个AT响应字节,状态0~6匹配+MADDR=,状态7接收4位十六进制地址。
|
||||||
|
_App_AddressParseByte:
|
||||||
|
ld a, 0x003E
|
||||||
|
ld 0x003F, a
|
||||||
|
ld a, 0x0048
|
||||||
|
cp a, #0x00
|
||||||
|
jreq _App_AddrPlus
|
||||||
|
cp a, #0x01
|
||||||
|
jreq _App_AddrM
|
||||||
|
cp a, #0x02
|
||||||
|
jreq _App_AddrA
|
||||||
|
cp a, #0x03
|
||||||
|
jreq _App_AddrD1
|
||||||
|
cp a, #0x04
|
||||||
|
jreq _App_AddrD2
|
||||||
|
cp a, #0x05
|
||||||
|
jreq _App_AddrR
|
||||||
|
cp a, #0x06
|
||||||
|
jreq _App_AddrEqual
|
||||||
|
; 状态7:转换十六进制字符并左移累加。
|
||||||
|
call _App_HexNibble
|
||||||
|
ld 0x003E, a
|
||||||
|
sll 0x0047
|
||||||
|
rlc 0x0046
|
||||||
|
sll 0x0047
|
||||||
|
rlc 0x0046
|
||||||
|
sll 0x0047
|
||||||
|
rlc 0x0046
|
||||||
|
sll 0x0047
|
||||||
|
rlc 0x0046
|
||||||
|
ld a, 0x003E
|
||||||
|
or a, 0x0047
|
||||||
|
ld 0x0047, a
|
||||||
|
inc 0x0049
|
||||||
|
ld a, 0x0049
|
||||||
|
cp a, #0x04
|
||||||
|
jrne _App_AddrReturn
|
||||||
|
mov 0x004A, #0x01
|
||||||
|
ret
|
||||||
|
_App_AddrPlus:
|
||||||
|
ld a, 0x003F
|
||||||
|
cp a, #0x2B
|
||||||
|
jrne _App_AddrReset
|
||||||
|
inc 0x0048
|
||||||
|
ret
|
||||||
|
_App_AddrM:
|
||||||
|
ld a, 0x003F
|
||||||
|
cp a, #0x4D
|
||||||
|
jrne _App_AddrReset
|
||||||
|
inc 0x0048
|
||||||
|
ret
|
||||||
|
_App_AddrA:
|
||||||
|
ld a, 0x003F
|
||||||
|
cp a, #0x41
|
||||||
|
jrne _App_AddrReset
|
||||||
|
inc 0x0048
|
||||||
|
ret
|
||||||
|
_App_AddrD1:
|
||||||
|
ld a, 0x003F
|
||||||
|
cp a, #0x44
|
||||||
|
jrne _App_AddrReset
|
||||||
|
inc 0x0048
|
||||||
|
ret
|
||||||
|
_App_AddrD2:
|
||||||
|
ld a, 0x003F
|
||||||
|
cp a, #0x44
|
||||||
|
jrne _App_AddrReset
|
||||||
|
inc 0x0048
|
||||||
|
ret
|
||||||
|
_App_AddrR:
|
||||||
|
ld a, 0x003F
|
||||||
|
cp a, #0x52
|
||||||
|
jrne _App_AddrReset
|
||||||
|
inc 0x0048
|
||||||
|
ret
|
||||||
|
_App_AddrEqual:
|
||||||
|
ld a, 0x003F
|
||||||
|
cp a, #0x3D
|
||||||
|
jrne _App_AddrReset
|
||||||
|
mov 0x0046, #0x00
|
||||||
|
mov 0x0047, #0x00
|
||||||
|
mov 0x0049, #0x00
|
||||||
|
mov 0x0048, #0x07
|
||||||
|
ret
|
||||||
|
_App_AddrReset:
|
||||||
|
mov 0x0048, #0x00
|
||||||
|
_App_AddrReturn:
|
||||||
|
ret
|
||||||
|
|
||||||
|
; 将ASCII十六进制字符转换为0~15;非法字符返回0。
|
||||||
|
_App_HexNibble:
|
||||||
|
ld a, 0x003F
|
||||||
|
sub a, #0x30
|
||||||
|
cp a, #0x0A
|
||||||
|
jrult _App_HexReturn
|
||||||
|
ld a, 0x003F
|
||||||
|
sub a, #0x41
|
||||||
|
add a, #0x0A
|
||||||
|
ret
|
||||||
|
_App_HexReturn:
|
||||||
|
ret
|
||||||
|
|
||||||
;-------------------------------------------------------------------------------
|
;-------------------------------------------------------------------------------
|
||||||
; JDY-28M Mesh外层帧状态机。
|
; JDY-28M Mesh外层帧状态机。
|
||||||
;
|
;
|
||||||
@@ -167,7 +330,9 @@ _App_MeshParseByte:
|
|||||||
_App_ParseWaitF1:
|
_App_ParseWaitF1:
|
||||||
ld a, 0x0020
|
ld a, 0x0020
|
||||||
cp a, #0xF1
|
cp a, #0xF1
|
||||||
jrne _App_ParseReturn
|
jreq _App_F1OK
|
||||||
|
jpf _App_ParseReturn
|
||||||
|
_App_F1OK:
|
||||||
mov 0x0034, #0x01
|
mov 0x0034, #0x01
|
||||||
ret
|
ret
|
||||||
|
|
||||||
@@ -180,8 +345,15 @@ _App_ParseWaitDD:
|
|||||||
|
|
||||||
_App_ParseWait09:
|
_App_ParseWait09:
|
||||||
ld a, 0x0020
|
ld a, 0x0020
|
||||||
|
; 当前JDY-28M实测接收帧固定头为F1 DD 08。
|
||||||
|
cp a, #0x08
|
||||||
|
jreq _App_MeshHeaderOK
|
||||||
cp a, #0x09
|
cp a, #0x09
|
||||||
jrne _App_ParseReset
|
jreq _App_MeshHeaderOK
|
||||||
|
cp a, #0x0A
|
||||||
|
jreq _App_MeshHeaderOK
|
||||||
|
jpf _App_ParseReset
|
||||||
|
_App_MeshHeaderOK:
|
||||||
mov 0x0034, #0x03
|
mov 0x0034, #0x03
|
||||||
mov 0x0035, #0x00
|
mov 0x0035, #0x00
|
||||||
ret
|
ret
|
||||||
@@ -255,21 +427,42 @@ _App_ProcessBusiness:
|
|||||||
; 先计算CMD、LEN、DATA的CRC,并与业务帧末尾两个字节比较;
|
; 先计算CMD、LEN、DATA的CRC,并与业务帧末尾两个字节比较;
|
||||||
; CRC错误时静默丢弃,绝不执行继电器动作。
|
; CRC错误时静默丢弃,绝不执行继电器动作。
|
||||||
call _App_CrcBusiness3
|
call _App_CrcBusiness3
|
||||||
|
; CRC结束时0x003F保存CMD+LEN+DATA的总长度,动态定位帧尾CRC。
|
||||||
|
ld a, 0x003F
|
||||||
|
add a, #0x40
|
||||||
|
ldw x, #0x0000
|
||||||
|
ld xl, a
|
||||||
|
ld a, (x)
|
||||||
|
ld 0x003E, a
|
||||||
ld a, 0x003D
|
ld a, 0x003D
|
||||||
cp a, 0x0043
|
cp a, 0x003E
|
||||||
jreq _App_CrcHighOK
|
jreq _App_CrcHighOK
|
||||||
jpf _App_BusinessReturn
|
jpf _App_BusinessReturn
|
||||||
_App_CrcHighOK:
|
_App_CrcHighOK:
|
||||||
|
incw x
|
||||||
|
ld a, (x)
|
||||||
|
ld 0x003E, a
|
||||||
ld a, 0x003C
|
ld a, 0x003C
|
||||||
cp a, 0x0044
|
cp a, 0x003E
|
||||||
jreq _App_CrcLowOK
|
jreq _App_CrcLowOK
|
||||||
jpf _App_BusinessReturn
|
jpf _App_BusinessReturn
|
||||||
_App_CrcLowOK:
|
_App_CrcLowOK:
|
||||||
ld a, 0x0040
|
ld a, 0x0040
|
||||||
|
cp a, #0x05
|
||||||
|
jreq _App_AddressCommand
|
||||||
|
cp a, #0x02
|
||||||
|
jreq _App_StatusCommand
|
||||||
cp a, #0x01
|
cp a, #0x01
|
||||||
jreq _App_CmdOK
|
jreq _App_ControlCommand
|
||||||
jpf _App_BusinessReturn
|
jpf _App_BusinessReturn
|
||||||
_App_CmdOK:
|
|
||||||
|
_App_AddressCommand:
|
||||||
|
ld a, 0x0041
|
||||||
|
cp a, #0x00
|
||||||
|
jreq _App_AddressQuery
|
||||||
|
jpf _App_BusinessReturn
|
||||||
|
|
||||||
|
_App_ControlCommand:
|
||||||
ld a, 0x0041
|
ld a, 0x0041
|
||||||
cp a, #0x01
|
cp a, #0x01
|
||||||
jreq _App_LenOK
|
jreq _App_LenOK
|
||||||
@@ -284,13 +477,72 @@ _App_LenOK:
|
|||||||
; 只有DATA=00或01允许控制继电器。
|
; 只有DATA=00或01允许控制继电器。
|
||||||
_App_RelayOff:
|
_App_RelayOff:
|
||||||
mov 0x5000, #0x00
|
mov 0x5000, #0x00
|
||||||
|
mov 0x0045, #0x81
|
||||||
jra _App_RelayRespond
|
jra _App_RelayRespond
|
||||||
_App_RelayOn:
|
_App_RelayOn:
|
||||||
mov 0x5000, #0x02
|
mov 0x5000, #0x02
|
||||||
|
mov 0x0045, #0x81
|
||||||
|
jra _App_RelayRespond
|
||||||
|
|
||||||
|
; -----------------------------------------------------------------------------
|
||||||
|
; 函数/处理分支:_App_StatusCommand
|
||||||
|
; 功能:处理业务命令CMD=0x02,即主机查询当前继电器状态。
|
||||||
|
;
|
||||||
|
; 输入:
|
||||||
|
; 0x0041:业务LEN,必须为0,查询命令不携带DATA字段。
|
||||||
|
; 0x5000:继电器控制状态寄存器,当前设计中0x02表示吸合,0x00表示断开。
|
||||||
|
;
|
||||||
|
; 输出:
|
||||||
|
; 返回业务命令CMD=0x82;LEN=1;DATA=0x01表示吸合,0x00表示断开。
|
||||||
|
; 返回帧仍由_App_RelayRespond统一计算CRC并封装为JDY-28M发送帧。
|
||||||
|
;
|
||||||
|
; 说明:
|
||||||
|
; 这里查询的是单片机实际输出的继电器控制状态,而不是触点反馈。
|
||||||
|
; 当前硬件没有继电器触点反馈,所以该状态不能证明继电器触点一定
|
||||||
|
; 已经机械吸合;触点异常仍由ACS712电流检测和故障上报逻辑判断。
|
||||||
|
; -----------------------------------------------------------------------------
|
||||||
|
_App_StatusCommand:
|
||||||
|
ld a, 0x0041
|
||||||
|
cp a, #0x00
|
||||||
|
jreq _App_StatusQuery
|
||||||
|
jpf _App_BusinessReturn
|
||||||
|
|
||||||
|
_App_StatusQuery:
|
||||||
|
mov 0x0045, #0x82
|
||||||
|
jra _App_RelayRespond
|
||||||
|
|
||||||
|
_App_AddressQuery:
|
||||||
|
; CMD=05、LEN=00:返回CMD=85、LEN=02和本机MADDR。
|
||||||
|
ld a, 0x004A
|
||||||
|
jrne _App_AddressValid
|
||||||
|
; 上电查询可能因模块尚未进入AT窗口而失败;收到地址查询时再重试一次。
|
||||||
|
bres 0x5000, #2
|
||||||
|
call _App_Delay10ms
|
||||||
|
mov 0x5235, #0x0C
|
||||||
|
call _App_QueryModuleAddress
|
||||||
|
bset 0x5000, #2
|
||||||
|
mov 0x5235, #0x2C
|
||||||
|
ld a, 0x004A
|
||||||
|
jrne _App_AddressValid
|
||||||
|
; 地址仍未读取成功时,使用FFFF作为诊断地址继续回复;
|
||||||
|
; 主机可据此确认广播命令已经到达设备,但MADDR读取失败。
|
||||||
|
mov 0x0046, #0xFF
|
||||||
|
mov 0x0047, #0xFF
|
||||||
|
_App_AddressValid:
|
||||||
|
mov 0x0040, #0x85
|
||||||
|
mov 0x0041, #0x02
|
||||||
|
ld a, 0x0046
|
||||||
|
ld 0x0042, a
|
||||||
|
ld a, 0x0047
|
||||||
|
ld 0x0043, a
|
||||||
|
jra _App_ResponseAddressCrc
|
||||||
|
|
||||||
_App_RelayRespond:
|
_App_RelayRespond:
|
||||||
; 业务区改写为CMD=81、LEN=1、DATA=当前控制状态。
|
; 业务区改写为应答CMD、LEN=1、DATA=当前控制状态。
|
||||||
mov 0x0040, #0x81
|
; 控制命令使用0x81,状态查询命令使用0x82;应答命令由调用分支
|
||||||
|
; 预先写入0x0045,避免复制两套完全相同的CRC和发送代码。
|
||||||
|
ld a, 0x0045
|
||||||
|
ld 0x0040, a
|
||||||
mov 0x0041, #0x01
|
mov 0x0041, #0x01
|
||||||
ld a, 0x5000
|
ld a, 0x5000
|
||||||
and a, #0x02
|
and a, #0x02
|
||||||
@@ -305,7 +557,16 @@ _App_ResponseCrc:
|
|||||||
ld 0x0043, a
|
ld 0x0043, a
|
||||||
ld a, 0x003C
|
ld a, 0x003C
|
||||||
ld 0x0044, a
|
ld 0x0044, a
|
||||||
|
jra _App_SendResponse
|
||||||
|
_App_ResponseAddressCrc:
|
||||||
|
call _App_CrcBusiness3
|
||||||
|
ld a, 0x003D
|
||||||
|
ld 0x0044, a
|
||||||
|
ld a, 0x003C
|
||||||
|
ld 0x0045, a
|
||||||
; JDY-28M发送格式:41 54 2B AA A1 | 目标地址 | A0 | 业务区 | 0D 0A。
|
; JDY-28M发送格式:41 54 2B AA A1 | 目标地址 | A0 | 业务区 | 0D 0A。
|
||||||
|
; 目标地址使用外层源地址,即发起查询的主机地址。
|
||||||
|
_App_SendResponse:
|
||||||
mov 0x003E, #0x41
|
mov 0x003E, #0x41
|
||||||
call _App_SendByte
|
call _App_SendByte
|
||||||
mov 0x003E, #0x54
|
mov 0x003E, #0x54
|
||||||
@@ -324,8 +585,10 @@ _App_ResponseCrc:
|
|||||||
call _App_SendByte
|
call _App_SendByte
|
||||||
mov 0x003E, #0xA0
|
mov 0x003E, #0xA0
|
||||||
call _App_SendByte
|
call _App_SendByte
|
||||||
ld a, #0x00
|
ld a, 0x0041
|
||||||
ld 0x003F, a
|
add a, #0x04
|
||||||
|
ld 0x0048, a
|
||||||
|
mov 0x003F, #0x00
|
||||||
_App_SendResponseBusiness:
|
_App_SendResponseBusiness:
|
||||||
ld a, 0x003F
|
ld a, 0x003F
|
||||||
add a, #0x40
|
add a, #0x40
|
||||||
@@ -336,7 +599,7 @@ _App_SendResponseBusiness:
|
|||||||
call _App_SendByte
|
call _App_SendByte
|
||||||
inc 0x003F
|
inc 0x003F
|
||||||
ld a, 0x003F
|
ld a, 0x003F
|
||||||
cp a, #0x05
|
cp a, 0x0048
|
||||||
jrne _App_SendResponseBusiness
|
jrne _App_SendResponseBusiness
|
||||||
; 按JDY-28M Mesh发送协议保留完整帧尾0D 0A。
|
; 按JDY-28M Mesh发送协议保留完整帧尾0D 0A。
|
||||||
mov 0x003E, #0x0D
|
mov 0x003E, #0x0D
|
||||||
@@ -375,7 +638,10 @@ _App_CrcNoPoly:
|
|||||||
jrne _App_CrcBit
|
jrne _App_CrcBit
|
||||||
inc 0x003F
|
inc 0x003F
|
||||||
ld a, 0x003F
|
ld a, 0x003F
|
||||||
cp a, #0x03
|
ld 0x003E, a
|
||||||
|
ld a, 0x0041
|
||||||
|
add a, #0x02
|
||||||
|
cp a, 0x003E
|
||||||
jrne _App_CrcByte
|
jrne _App_CrcByte
|
||||||
ret
|
ret
|
||||||
|
|
||||||
@@ -387,6 +653,17 @@ _App_SendByte:
|
|||||||
ld 0x5231, a
|
ld 0x5231, a
|
||||||
ret
|
ret
|
||||||
|
|
||||||
|
;-------------------------------------------------------------------------------
|
||||||
|
; 粗略延时约10ms。
|
||||||
|
; 主频为16MHz,本延时只用于蓝牙模块PWRC窗口建立,不作为业务定时基准。
|
||||||
|
;-------------------------------------------------------------------------------
|
||||||
|
_App_Delay10ms:
|
||||||
|
ldw x, #0xFFFF
|
||||||
|
_App_Delay10msLoop:
|
||||||
|
decw x
|
||||||
|
jrne _App_Delay10msLoop
|
||||||
|
ret
|
||||||
|
|
||||||
;-------------------------------------------------------------------------------
|
;-------------------------------------------------------------------------------
|
||||||
; TIM4中断服务函数。
|
; TIM4中断服务函数。
|
||||||
; 每500次约1ms中断翻转一次PB5,因此目标周期约500ms。计数变量放在RAM中,
|
; 每500次约1ms中断翻转一次PB5,因此目标周期约500ms。计数变量放在RAM中,
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""JDY-28M Mesh 继电器测试工具。
|
||||||
|
|
||||||
|
本工具通过电脑串口连接一个处于 Mesh 指令模式的 JDY-28M 模块,提供以下
|
||||||
|
测试功能:
|
||||||
|
|
||||||
|
1. 广播查询网络内设备短地址;
|
||||||
|
2. 将收到的设备短地址整理成编号列表;
|
||||||
|
3. 按编号选择目标设备;
|
||||||
|
4. 发送继电器吸合、断开和状态查询命令;
|
||||||
|
5. 自动计算 CRC-16/MODBUS;
|
||||||
|
6. 自动解析 JDY-28M 外层接收帧和设备业务应答。
|
||||||
|
|
||||||
|
业务帧格式:
|
||||||
|
|
||||||
|
CMD | LEN | DATA | CRC16_H | CRC16_L
|
||||||
|
|
||||||
|
主机发送给 JDY-28M 的 Mesh 指令格式:
|
||||||
|
|
||||||
|
41 54 2B AA A1 | DST_H DST_L | A0 | BUSINESS | 0D 0A
|
||||||
|
|
||||||
|
电脑串口收到的 Mesh 数据格式:
|
||||||
|
|
||||||
|
F1 DD 09 | SRC_H SRC_L | DST_H DST_L | BUSINESS | 0A
|
||||||
|
|
||||||
|
依赖:
|
||||||
|
|
||||||
|
python -m pip install pyserial
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from serial.tools import list_ports
|
||||||
|
|
||||||
|
|
||||||
|
# JDY-28M Mesh 主机发送帧的固定部分。
|
||||||
|
TX_PREFIX = bytes((0x41, 0x54, 0x2B, 0xAA, 0xA1))
|
||||||
|
TX_DATA_FLAG = 0xA0
|
||||||
|
|
||||||
|
# JDY-28M Mesh 接收帧的固定部分。
|
||||||
|
# 根据当前JDY-28M实测,接收帧固定头为F1 DD 0A。
|
||||||
|
RX_PREFIXES = (
|
||||||
|
bytes((0xF1, 0xDD, 0x08)),
|
||||||
|
bytes((0xF1, 0xDD, 0x09)),
|
||||||
|
bytes((0xF1, 0xDD, 0x0A)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mesh 广播目标地址。
|
||||||
|
BROADCAST_ADDRESS = 0xFFFF
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MeshFrame:
|
||||||
|
"""一帧已经拆分完成的 JDY-28M Mesh 接收数据。"""
|
||||||
|
|
||||||
|
source: int
|
||||||
|
destination: int
|
||||||
|
business: bytes
|
||||||
|
|
||||||
|
|
||||||
|
def crc16_modbus(data: bytes) -> int:
|
||||||
|
"""计算 CRC-16/MODBUS,并返回16位整数结果。
|
||||||
|
|
||||||
|
算法参数与设备固件一致:多项式为0xA001,初始值为0xFFFF,
|
||||||
|
输入和输出均按 MODBUS 反射规则处理。返回值内部按整数保存,
|
||||||
|
发送时由调用方拆成高字节、低字节。
|
||||||
|
"""
|
||||||
|
|
||||||
|
crc = 0xFFFF
|
||||||
|
for value in data:
|
||||||
|
crc ^= value
|
||||||
|
for _ in range(8):
|
||||||
|
if crc & 0x0001:
|
||||||
|
crc = (crc >> 1) ^ 0xA001
|
||||||
|
else:
|
||||||
|
crc >>= 1
|
||||||
|
return crc & 0xFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def build_business(command: int, data: bytes = b"") -> bytes:
|
||||||
|
"""构造业务帧,并按高字节在前追加CRC16。"""
|
||||||
|
|
||||||
|
if not 0 <= command <= 0xFF:
|
||||||
|
raise ValueError("命令必须是0~255")
|
||||||
|
if len(data) > 0xFF:
|
||||||
|
raise ValueError("业务数据不能超过255字节")
|
||||||
|
|
||||||
|
body = bytes((command, len(data))) + data
|
||||||
|
crc = crc16_modbus(body)
|
||||||
|
return body + bytes((crc >> 8, crc & 0xFF))
|
||||||
|
|
||||||
|
|
||||||
|
def build_mesh_command(destination: int, command: int, data: bytes = b"") -> bytes:
|
||||||
|
"""封装一帧发送给JDY-28M的Mesh指令。"""
|
||||||
|
|
||||||
|
if not 0 <= destination <= 0xFFFF:
|
||||||
|
raise ValueError("目标地址必须是0x0000~0xFFFF")
|
||||||
|
|
||||||
|
business = build_business(command, data)
|
||||||
|
return (
|
||||||
|
TX_PREFIX
|
||||||
|
+ bytes((destination >> 8, destination & 0xFF, TX_DATA_FLAG))
|
||||||
|
+ business
|
||||||
|
+ bytes((0x0D, 0x0A))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_mesh_frame(buffer: bytearray) -> MeshFrame | None:
|
||||||
|
"""从接收缓存中提取一帧完整 Mesh 数据。
|
||||||
|
|
||||||
|
返回值为:
|
||||||
|
|
||||||
|
- 成功提取时返回 MeshFrame;
|
||||||
|
- 当前缓存还不完整时返回 None,同时保留缓存内容;
|
||||||
|
- 发现无效前导数据时丢弃无效字节,继续等待下一帧。
|
||||||
|
"""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
matches = [(buffer.find(prefix), prefix) for prefix in RX_PREFIXES]
|
||||||
|
matches = [(index, prefix) for index, prefix in matches if index >= 0]
|
||||||
|
if not matches:
|
||||||
|
# 只保留可能构成半个帧头的末尾字节,防止缓存无限增长。
|
||||||
|
# 没有完整帧头时,保留最长帧头长度减1的尾部数据,
|
||||||
|
# 以便下一次串口读取时拼接出跨读取边界的帧头。
|
||||||
|
keep = min(len(buffer), max(len(prefix) for prefix in RX_PREFIXES) - 1)
|
||||||
|
if keep:
|
||||||
|
buffer[:] = buffer[-keep:]
|
||||||
|
else:
|
||||||
|
buffer.clear()
|
||||||
|
return None
|
||||||
|
|
||||||
|
prefix_index, _ = min(matches, key=lambda item: item[0])
|
||||||
|
if prefix_index:
|
||||||
|
del buffer[:prefix_index]
|
||||||
|
|
||||||
|
# 固定头3字节 + 源地址2字节 + 目标地址2字节。
|
||||||
|
if len(buffer) < 7:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 业务数据以0A结束。模块接收侧可能只显示0A,也可能保留0D 0A;
|
||||||
|
# 这里统一以0A作为接收帧结束字节。
|
||||||
|
tail_index = buffer.find(b"\x0A", 7)
|
||||||
|
if tail_index < 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
frame_bytes = bytes(buffer[:tail_index + 1])
|
||||||
|
del buffer[:tail_index + 1]
|
||||||
|
|
||||||
|
source = (frame_bytes[3] << 8) | frame_bytes[4]
|
||||||
|
destination = (frame_bytes[5] << 8) | frame_bytes[6]
|
||||||
|
business = frame_bytes[7:-1]
|
||||||
|
# 部分模块接收侧会显示0D 0A,0D属于外层结束符,不属于业务数据。
|
||||||
|
if business.endswith(b"\x0D"):
|
||||||
|
business = business[:-1]
|
||||||
|
return MeshFrame(source, destination, business)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_business(business: bytes) -> tuple[int, bytes] | None:
|
||||||
|
"""校验业务帧长度和CRC,成功时返回命令号及DATA。"""
|
||||||
|
|
||||||
|
if len(business) < 4:
|
||||||
|
return None
|
||||||
|
|
||||||
|
command = business[0]
|
||||||
|
data_length = business[1]
|
||||||
|
if len(business) != data_length + 4:
|
||||||
|
return None
|
||||||
|
|
||||||
|
body = business[:-2]
|
||||||
|
received_crc = (business[-2] << 8) | business[-1]
|
||||||
|
if crc16_modbus(body) != received_crc:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return command, business[2:2 + data_length]
|
||||||
|
|
||||||
|
|
||||||
|
def receive_frames(port: serial.Serial, duration: float) -> list[MeshFrame]:
|
||||||
|
"""在指定时间窗口内读取并解析所有完整 Mesh 帧。"""
|
||||||
|
|
||||||
|
frames: list[MeshFrame] = []
|
||||||
|
buffer = bytearray()
|
||||||
|
deadline = time.monotonic() + duration
|
||||||
|
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
waiting = port.in_waiting
|
||||||
|
if waiting:
|
||||||
|
received_bytes = port.read(waiting)
|
||||||
|
# 调试阶段完整打印串口收到的每一段原始数据,
|
||||||
|
# 包括无法组成完整Mesh帧或格式校验失败的数据。
|
||||||
|
print_hex("串口原始接收", received_bytes)
|
||||||
|
buffer.extend(received_bytes)
|
||||||
|
while True:
|
||||||
|
frame = parse_mesh_frame(buffer)
|
||||||
|
if frame is None:
|
||||||
|
break
|
||||||
|
frames.append(frame)
|
||||||
|
else:
|
||||||
|
time.sleep(0.005)
|
||||||
|
|
||||||
|
return frames
|
||||||
|
|
||||||
|
|
||||||
|
def print_hex(label: str, data: bytes) -> None:
|
||||||
|
"""以空格分隔的十六进制形式打印串口帧。"""
|
||||||
|
|
||||||
|
print(f"{label}: {data.hex(' ').upper()}")
|
||||||
|
|
||||||
|
|
||||||
|
def send_command(
|
||||||
|
port: serial.Serial,
|
||||||
|
destination: int,
|
||||||
|
command: int,
|
||||||
|
data: bytes = b"",
|
||||||
|
) -> None:
|
||||||
|
"""构造并发送一条Mesh业务命令。"""
|
||||||
|
|
||||||
|
frame = build_mesh_command(destination, command, data)
|
||||||
|
port.write(frame)
|
||||||
|
port.flush()
|
||||||
|
print_hex("已发送", frame)
|
||||||
|
|
||||||
|
|
||||||
|
def discover_devices(port: serial.Serial, timeout: float) -> list[int]:
|
||||||
|
"""广播查询设备地址,并返回去重后的设备地址列表。"""
|
||||||
|
|
||||||
|
port.reset_input_buffer()
|
||||||
|
frame = build_mesh_command(BROADCAST_ADDRESS, 0x05)
|
||||||
|
port.write(frame)
|
||||||
|
port.flush()
|
||||||
|
print_hex("地址查询广播", frame)
|
||||||
|
print(f"等待设备回复 {timeout:.1f} 秒...")
|
||||||
|
|
||||||
|
addresses: set[int] = set()
|
||||||
|
for received in receive_frames(port, timeout):
|
||||||
|
result = validate_business(received.business)
|
||||||
|
if result is None:
|
||||||
|
continue
|
||||||
|
command, data = result
|
||||||
|
if command == 0x85 and len(data) == 2:
|
||||||
|
address = (data[0] << 8) | data[1]
|
||||||
|
addresses.add(address)
|
||||||
|
print(
|
||||||
|
f"收到地址应答:来源={received.source:04X},"
|
||||||
|
f"设备地址={address:04X}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return sorted(addresses)
|
||||||
|
|
||||||
|
|
||||||
|
def select_device(devices: list[int]) -> int | None:
|
||||||
|
"""显示设备列表并让用户选择一个设备。"""
|
||||||
|
|
||||||
|
if not devices:
|
||||||
|
print("当前没有已发现的设备,请先执行地址广播查询。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print("设备列表:")
|
||||||
|
for index, address in enumerate(devices, start=1):
|
||||||
|
print(f" {index}. {address:04X}")
|
||||||
|
|
||||||
|
value = input("请输入设备编号:").strip()
|
||||||
|
try:
|
||||||
|
index = int(value)
|
||||||
|
except ValueError:
|
||||||
|
print("设备编号无效。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not 1 <= index <= len(devices):
|
||||||
|
print("设备编号超出范围。")
|
||||||
|
return None
|
||||||
|
return devices[index - 1]
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_response(port: serial.Serial, timeout: float, target: int) -> None:
|
||||||
|
"""接收并解析指定设备的业务应答。"""
|
||||||
|
|
||||||
|
for received in receive_frames(port, timeout):
|
||||||
|
result = validate_business(received.business)
|
||||||
|
if result is None:
|
||||||
|
print_hex("收到CRC或长度错误帧", received.business)
|
||||||
|
continue
|
||||||
|
|
||||||
|
command, data = result
|
||||||
|
if received.source != target:
|
||||||
|
print(f"收到其他设备应答:{received.source:04X}")
|
||||||
|
|
||||||
|
if command == 0x81 and len(data) == 1:
|
||||||
|
state = "吸合" if data[0] else "断开"
|
||||||
|
print(f"继电器控制成功:{state}(DATA={data[0]:02X})")
|
||||||
|
elif command == 0x82 and len(data) == 1:
|
||||||
|
state = "吸合" if data[0] else "断开"
|
||||||
|
print(f"继电器当前状态:{state}")
|
||||||
|
elif command == 0xE0 and len(data) == 1:
|
||||||
|
print(f"设备返回错误:错误码 0x{data[0]:02X}")
|
||||||
|
else:
|
||||||
|
print(f"收到应答 CMD=0x{command:02X}, DATA={data.hex(' ').upper()}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("等待设备应答超时。")
|
||||||
|
|
||||||
|
|
||||||
|
def run_menu(port: serial.Serial, response_timeout: float) -> None:
|
||||||
|
"""运行交互式菜单。"""
|
||||||
|
|
||||||
|
devices: list[int] = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\n===== STM8 Mesh 继电器测试工具 =====")
|
||||||
|
print("1. 广播查询设备地址")
|
||||||
|
print("2. 显示设备列表")
|
||||||
|
print("3. 选择设备并吸合继电器")
|
||||||
|
print("4. 选择设备并断开继电器")
|
||||||
|
print("5. 选择设备并查询继电器状态")
|
||||||
|
print("0. 退出")
|
||||||
|
|
||||||
|
choice = input("请选择:").strip()
|
||||||
|
if choice == "0":
|
||||||
|
return
|
||||||
|
if choice == "1":
|
||||||
|
devices = discover_devices(port, response_timeout)
|
||||||
|
print("发现设备:", ", ".join(f"{value:04X}" for value in devices) or "无")
|
||||||
|
continue
|
||||||
|
if choice == "2":
|
||||||
|
if devices:
|
||||||
|
print("、".join(f"{index}. {value:04X}" for index, value in enumerate(devices, 1)))
|
||||||
|
else:
|
||||||
|
print("设备列表为空。")
|
||||||
|
continue
|
||||||
|
if choice in {"3", "4", "5"}:
|
||||||
|
target = select_device(devices)
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if choice == "3":
|
||||||
|
send_command(port, target, 0x01, bytes((0x01,)))
|
||||||
|
elif choice == "4":
|
||||||
|
send_command(port, target, 0x01, bytes((0x00,)))
|
||||||
|
else:
|
||||||
|
send_command(port, target, 0x02)
|
||||||
|
wait_for_response(port, response_timeout, target)
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("未知选项。")
|
||||||
|
|
||||||
|
|
||||||
|
def select_serial_port() -> str:
|
||||||
|
"""扫描电脑可用串口并让用户选择一个串口。
|
||||||
|
|
||||||
|
同一个串口在不同操作系统上的描述字段可能不同,因此同时显示设备路径、
|
||||||
|
设备名称、描述和硬件信息,方便用户识别USB转串口适配器。
|
||||||
|
"""
|
||||||
|
|
||||||
|
ports = sorted(list_ports.comports(), key=lambda item: item.device)
|
||||||
|
if not ports:
|
||||||
|
raise RuntimeError("没有扫描到可用串口,请检查USB转串口设备和驱动。")
|
||||||
|
|
||||||
|
print("可用串口:")
|
||||||
|
for index, item in enumerate(ports, start=1):
|
||||||
|
description = item.description or "无描述"
|
||||||
|
manufacturer = item.manufacturer or "未知厂商"
|
||||||
|
print(f" {index}. {item.device} | {description} | {manufacturer}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
value = input("请选择串口编号:").strip()
|
||||||
|
try:
|
||||||
|
index = int(value)
|
||||||
|
except ValueError:
|
||||||
|
print("请输入有效的数字编号。")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if 1 <= index <= len(ports):
|
||||||
|
return ports[index - 1].device
|
||||||
|
print("串口编号超出范围,请重新选择。")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""解析命令行参数并启动测试菜单。"""
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="JDY-28M Mesh继电器测试工具")
|
||||||
|
parser.add_argument("--port", help="直接指定电脑串口;不指定时启动后扫描并选择")
|
||||||
|
parser.add_argument("--baudrate", type=int, default=9600, help="串口波特率,默认9600")
|
||||||
|
parser.add_argument("--timeout", type=float, default=0.2, help="串口读取超时,默认0.2秒")
|
||||||
|
parser.add_argument("--response-timeout", type=float, default=3.0, help="设备应答等待时间,默认10秒")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
selected_port = args.port or select_serial_port()
|
||||||
|
with serial.Serial(selected_port, args.baudrate, timeout=args.timeout) as port:
|
||||||
|
print(f"已打开串口:{selected_port},波特率:{args.baudrate}")
|
||||||
|
run_menu(port, args.response_timeout)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user