v1.7.0: spurious end-of-decode hardening + session-resilient start.sh

The decoder could end decode() early on an idle/busy live line; the frontend
symptom was a flood of "sr_session_stop: session was NULL" after the session
stopped.  Harden the EOF detection and stop the capture from dropping.

pd.py:
- _at_eof(): a step that consumes only queued bytes (_replay / peeked_byte,
  e.g. the invalid-INS re-frame) no longer counts as end-of-capture; bounded
  to 64 consecutive no-advance steps.
- _stall_check(track=False): the idle-LEVEL wait in wait_data_falling returns
  immediately at the same sample on an idle line -- not EOF.  Only "pins is
  None" ends the wait there; sample-stall detection is kept for the edge wait.
- resync_idle(): guard wait()==None (was an unguarded pins[...] TypeError at
  stream end).

Effect: samsung_phone_sample.sr had halted at ~21.9M/320M samples with 1 APDU;
it now decodes through ~293M to a true end (939 APDUs, CHKSUM 176).  The 6
CONCAT packets are real mis-framed STK bursts now reachable (known Samsung
de-concatenation family).  All other traces unchanged.

start.sh:
- Auto-restart is now the default (--no-loop disables) so an intermittent
  FX2 USB stop reconnects instead of ending the session.
- Per-session pcap suffix (FILE.N.ext) so a restart never overwrites the
  previous capture.
- Filter the repetitive "sr_session_stop: session was NULL" teardown line
  from stderr (real decoder errors still pass).

Validation: test_8 56/0/OK, test_7 65/0/OK, xiaomi_phone 640, xiaomi_cold
473, 4gmodem 327, A55 407/395, S21p 399/375, target S21p 369; 0 CONCAT;
65/65 unit tests (10 new).  VERSION -> 1.7.0.
This commit is contained in:
Трошин Антон Валерьевич committed 2026-09-11 17:55:56 +03:00
1 parent bca1b85b94
commit 690cc132ca
5 files changed
+205 -23

No files matched your search

+83
View File
@@ -6,6 +6,7 @@ the real pd.py / gsmtap_stream.py sources under the `iso7816` package
name (mirroring how libsigrokdecode loads a decoder directory).
"""
import collections
import importlib.util
import os
import sys
@@ -447,5 +448,87 @@ class TestEmbeddedExchanges(unittest.TestCase):
self.assertFalse(self.pd.is_desynced(bytes.fromhex(a55u1)))
class TestStallCheck(unittest.TestCase):
"""_stall_check distinguishes true EOF (None pins) from a live line that
is merely idle (a level wait returns immediately at the same sample)."""
def setUp(self):
_, pd = _load_iso7816()
self.inst = object.__new__(pd.Decoder)
self.inst.samplenum = 100
self.inst._prev_wait_sn = None
self.inst._stall = 0
def test_none_pins_is_eof(self):
self.assertTrue(self.inst._stall_check(None))
def test_advancing_sample_is_not_stall(self):
for sn in range(100, 110):
self.inst.samplenum = sn
self.assertFalse(self.inst._stall_check([1]))
def test_repeated_same_sample_is_bounded_stall(self):
results = []
for _ in range(10):
self.inst.samplenum = 100
results.append(self.inst._stall_check([1]))
self.assertFalse(results[0])
self.assertIn(True, results)
def test_untracked_level_wait_never_stalls(self):
# An idle-level wait repeats at the same sample on a live idle line;
# with track=False it must not be flagged as EOF.
for _ in range(10):
self.inst.samplenum = 100
self.assertFalse(self.inst._stall_check([1], track=False))
self.assertEqual(self.inst._stall, 0)
self.assertIsNone(self.inst._prev_wait_sn)
def test_untracked_resets_stall_window(self):
for _ in range(6):
self.inst.samplenum = 100
self.inst._stall_check([1]) # would trip the tracked stall
self.assertTrue(self.inst._stall > 4)
self.inst._stall_check([1], track=False)
self.assertFalse(self.inst._stall_check([1])) # window reset
class TestAtEof(unittest.TestCase):
"""decode_step must not treat a replay-only step (no sample progress) as
end-of-capture, but must still terminate a genuinely stuck stream."""
def setUp(self):
_, pd = _load_iso7816()
self.inst = object.__new__(pd.Decoder)
self.inst.samplenum = 500
self.inst._last_sn = 500
self.inst._no_advance = 0
self.inst._eof = False
self.inst._replay = collections.deque()
self.inst.peeked_byte = None
def test_advancing_is_not_eof(self):
self.inst.samplenum = 501
self.assertFalse(self.inst._at_eof())
def test_stuck_with_nothing_queued_is_eof(self):
self.assertTrue(self.inst._at_eof())
def test_pending_replay_is_not_eof(self):
self.inst._replay.append(0x00)
self.assertFalse(self.inst._at_eof())
def test_pending_replay_is_bounded(self):
self.inst._replay.append(0x00)
for _ in range(64):
self.assertFalse(self.inst._at_eof())
self.assertTrue(self.inst._at_eof())
def test_explicit_eof_wins(self):
self.inst._eof = True
self.inst.samplenum = 600
self.assertTrue(self.inst._at_eof())
if __name__ == '__main__':
unittest.main()