merge: task/m104fix-rxbits 合入 v2-on-ui
任务 m104fix(1 个提交)经 task-worktree 回收: - fix(104主站工具): RX 解析按本仓 6 字节头取位——PN 在 cause 字节 bit6(道D 批2.5 R1)
This commit is contained in:
@@ -26,6 +26,9 @@ Exit codes: 0 = all checks PASS; 1 = any check FAIL; 2 = connect or
|
|||||||
protocol error. Output is pure ASCII (redirect stdout to archive the
|
protocol error. Output is pure ASCII (redirect stdout to archive the
|
||||||
round-trip log as bench evidence).
|
round-trip log as bench evidence).
|
||||||
|
|
||||||
|
RX decode fix (2026-10-25, task-21/m104fix): cause-byte semantics above;
|
||||||
|
offline sample-frame selftest: python test/host/test_iec104_master_rx.py
|
||||||
|
|
||||||
Usage: python iec104_master.py [--ip 192.168.1.100] [--port 2404]
|
Usage: python iec104_master.py [--ip 192.168.1.100] [--port 2404]
|
||||||
[--addr 0x8858] [--low 0.05]
|
[--addr 0x8858] [--low 0.05]
|
||||||
[--restore 10.0]
|
[--restore 10.0]
|
||||||
@@ -61,7 +64,15 @@ T_C_IC_NA_1, T_C_RD_NA_1 = 100, 102
|
|||||||
|
|
||||||
COT_BG, COT_SPONT, COT_INIT, COT_REQ = 2, 3, 4, 5
|
COT_BG, COT_SPONT, COT_INIT, COT_REQ = 2, 3, 4, 5
|
||||||
COT_ACT, COT_ACTCON, COT_ACTTERM = 6, 7, 8
|
COT_ACT, COT_ACTCON, COT_ACTTERM = 6, 7, 8
|
||||||
COT_PN = 0x40 # COT high byte bit6: negative confirm
|
COT_PN = 0x40 # cause byte bit6: negative confirm
|
||||||
|
# NOTE(2026-10-25 m104fix): THIS SLAVE USES THE B4 IN-HOUSE 6-BYTE ASDU HEAD --
|
||||||
|
# T/PN/RBT FLAGS LIVE IN THE CAUSE BYTE (bit7/bit6/bit4), byte[3] IS OA+EVI(bit7).
|
||||||
|
# SEE App/protocol/iec104/iec104_asdu.c iec104_asdu_put_hdr(). The old RX path read
|
||||||
|
# PN from byte[3] (standard 2-byte-COT habit) -> pn was ALWAYS False and a rejected
|
||||||
|
# command logged cot=71 (7|0x40), so actcon_of(type, True) could never match.
|
||||||
|
# Bench proof 2026-10-25 (V5-gap replay): two out-of-range C_SE writes
|
||||||
|
# (51.0 @ 0x8030, 2.0 @ 0x8034) each answered type=50 cause=0x47 on the wire.
|
||||||
|
# Full standardization (real COT=2 head) is tracked separately, NOT fixed here.
|
||||||
RQT_GROUP1 = 20
|
RQT_GROUP1 = 20
|
||||||
CA = 1 # common address fixed to 1 in V0.1
|
CA = 1 # common address fixed to 1 in V0.1
|
||||||
SCAN_COTS = (COT_BG, COT_INIT)
|
SCAN_COTS = (COT_BG, COT_INIT)
|
||||||
@@ -132,6 +143,17 @@ def cp56_str(b):
|
|||||||
return ("IV " if iv else "") + s
|
return ("IV " if iv else "") + s
|
||||||
|
|
||||||
|
|
||||||
|
def decode_cot(cause_byte):
|
||||||
|
"""Split the in-house cause byte into (cot, pn).
|
||||||
|
|
||||||
|
Pure function so it can be pinned offline without a bench -- sample frames in
|
||||||
|
test/host/test_iec104_master_rx.py. Mirrors iec104_asdu.c put_hdr()/parse:
|
||||||
|
cot = cause & 0x3F, PN = cause bit6 (RBT bit4 / T bit7 stay inside the same
|
||||||
|
byte and are transparently masked out of cot). byte[3] (OA+EVI) is NOT COT hi.
|
||||||
|
"""
|
||||||
|
return (cause_byte & 0x3F, (cause_byte & COT_PN) != 0)
|
||||||
|
|
||||||
|
|
||||||
class Master(object):
|
class Master(object):
|
||||||
"""Minimal 104 master. k/w/t are covered by "S after every recv batch"
|
"""Minimal 104 master. k/w/t are covered by "S after every recv batch"
|
||||||
(far faster than t1=15s / t2=10s), so no timers are needed here."""
|
(far faster than t1=15s / t2=10s), so no timers are needed here."""
|
||||||
@@ -225,8 +247,8 @@ class Master(object):
|
|||||||
def _asdu(self, a, nr):
|
def _asdu(self, a, nr):
|
||||||
if len(a) < 6:
|
if len(a) < 6:
|
||||||
raise RuntimeError("short ASDU")
|
raise RuntimeError("short ASDU")
|
||||||
type_id, vsq, cot, cot_hi = a[0], a[1], a[2], a[3]
|
type_id, vsq = a[0], a[1]
|
||||||
pn = (cot_hi & COT_PN) != 0
|
cot, pn = decode_cot(a[2]) # flags live in the cause byte (see COT_PN note)
|
||||||
sq = (vsq & 0x80) != 0
|
sq = (vsq & 0x80) != 0
|
||||||
n = vsq & 0x7F
|
n = vsq & 0x7F
|
||||||
obj = a[6:]
|
obj = a[6:]
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Offline sample-frame selftest for iec104_master.py RX decode (task-21/m104fix).
|
||||||
|
|
||||||
|
Why offline (no bench): the 2026-10-25 big-window leg proved on the wire that the
|
||||||
|
slave answers an out-of-range C_SE_NC_1 with type=50 and cause byte 0x47 (cot 7 |
|
||||||
|
PN 0x40) -- two captured events: 51.0 @ 0x8030 and 2.0 @ 0x8034. The old RX path
|
||||||
|
looked for PN in byte[3] (OA byte, always 0) and did not mask the cause byte, so
|
||||||
|
such frames decoded as cot=71/pn=False and actcon_of(type, True) could never match
|
||||||
|
(negative-confirm PN was never asserted on hardware before that window).
|
||||||
|
|
||||||
|
These tests pin the FIXED decoder against reconstructed sample frames (identical to
|
||||||
|
the captured wire bytes: type, vsq=1, cause, oa, ca(LE16), echoed obj ioa3+float4+
|
||||||
|
qdc1). Pure stdlib, no socket, no hardware.
|
||||||
|
|
||||||
|
Known in-house-format limitation (NOT reachable on the wire today, kept visible):
|
||||||
|
the RBT bit4 lives inside the 6-bit cause field of the in-house 6-byte head, so a
|
||||||
|
cause value 16..23 would collide with RBT=1. The firmware never sets RBT on TX
|
||||||
|
(iec104_link.c passes cot_hi=0/0x40 only); proper fix belongs to the standard
|
||||||
|
COT=2 rework (registry: 104 ASDU head in-house-COT1 compression), not this task.
|
||||||
|
|
||||||
|
Usage: python test/host/test_iec104_master_rx.py [-v]
|
||||||
|
Exit code: 0 = all PASS.
|
||||||
|
Created 2026-10-25 (lane-d, task-21).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
import iec104_master as m104 # noqa: E402
|
||||||
|
|
||||||
|
CA = m104.CA
|
||||||
|
|
||||||
|
|
||||||
|
def actcon_frame(cause_byte, ioa, val):
|
||||||
|
"""Rebuild a slave C_SE actcon exactly as iec104_link.c s_cmd_con echoes it."""
|
||||||
|
obj = m104.put_ioa(ioa) + struct.pack("<f", val) + bytes([0x00])
|
||||||
|
return bytes([m104.T_C_SE_NC_1, 0x01, cause_byte, 0x00,
|
||||||
|
CA & 0xFF, (CA >> 8) & 0xFF]) + obj
|
||||||
|
|
||||||
|
|
||||||
|
class DecodeCot(unittest.TestCase):
|
||||||
|
"""Pure-cause-byte semantics: wire bytes captured in the 2026-10-25 window."""
|
||||||
|
|
||||||
|
def test_reject_cause_0x47(self):
|
||||||
|
# captured x2: "RX I type=50 cot=71" == 7|0x40 pre-fix display
|
||||||
|
self.assertEqual(m104.decode_cot(0x47), (7, True))
|
||||||
|
|
||||||
|
def test_accept_cause_0x07(self):
|
||||||
|
self.assertEqual(m104.decode_cot(0x07), (7, False))
|
||||||
|
|
||||||
|
def test_reject_frames_carry_expected_values(self):
|
||||||
|
f1 = actcon_frame(0x47, 0x8030, 51.0)
|
||||||
|
f2 = actcon_frame(0x47, 0x8034, 2.0)
|
||||||
|
self.assertEqual(f1[2], 71)
|
||||||
|
self.assertEqual(f2[2], 71)
|
||||||
|
# cause value survives masking, flags are gone from cot
|
||||||
|
self.assertEqual(m104.decode_cot(f1[2]), (7, True))
|
||||||
|
|
||||||
|
|
||||||
|
class EndToEndMatch(unittest.TestCase):
|
||||||
|
"""decoder output -> events tuple -> actcon_of matcher (what window waits on)."""
|
||||||
|
|
||||||
|
def test_reject_matches_negative_actcon(self):
|
||||||
|
f = actcon_frame(0x47, 0x8030, 51.0)
|
||||||
|
cot, pn = m104.decode_cot(f[2])
|
||||||
|
ev = ("asdu", (f[0], cot, pn, f[6:]))
|
||||||
|
self.assertTrue(m104.actcon_of(m104.T_C_SE_NC_1, True)(ev))
|
||||||
|
# old wrong decode would build (50, 71, False) and miss:
|
||||||
|
legacy = ("asdu", (f[0], f[2], (f[3] & m104.COT_PN) != 0, f[6:]))
|
||||||
|
self.assertFalse(m104.actcon_of(m104.T_C_SE_NC_1, True)(legacy))
|
||||||
|
|
||||||
|
def test_positive_still_matches_positive(self):
|
||||||
|
f = actcon_frame(0x07, 0x8030, 0.7)
|
||||||
|
cot, pn = m104.decode_cot(f[2])
|
||||||
|
ev = ("asdu", (f[0], cot, pn, f[6:]))
|
||||||
|
self.assertTrue(m104.actcon_of(m104.T_C_SE_NC_1, False)(ev))
|
||||||
|
self.assertFalse(m104.actcon_of(m104.T_C_SE_NC_1, True)(ev))
|
||||||
|
|
||||||
|
def test_matcher_none_wants_any_pn(self):
|
||||||
|
for cause in (0x07, 0x47):
|
||||||
|
cot, pn = m104.decode_cot(cause)
|
||||||
|
ev = ("asdu", (50, cot, pn, b""))
|
||||||
|
self.assertTrue(m104.actcon_of(m104.T_C_SE_NC_1)(ev))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
Reference in New Issue
Block a user