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

+45 -8
View File
@@ -28,7 +28,7 @@ from .gsmtap_stream import (GsmtapStreamSender,
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
GSMTAP_FLAG_BAD_FCS)
VERSION = '1.6.0'
VERSION = '1.7.0'
@@ -452,6 +452,11 @@ class Decoder(srd.Decoder):
self._start_fall = None
self._edge_read = True
self._last_sn = -1
# Bounded allowance for steps that consume only queued bytes
# (_replay / peeked_byte) and therefore do not advance the sample
# counter. Without it the "stuck sample number == EOF" test below
# would mistake a re-frame step for end-of-capture.
self._no_advance = 0
self._eof = False
self._min_pulse_samples = 2 # tuned for 16 MHz sample rate
self._glitch_rejects = 0
@@ -1389,7 +1394,7 @@ class Decoder(srd.Decoder):
cond.append({self.VCC_IDX: 'e'})
while True:
pins = self.wait(cond)
if self._stall_check(pins):
if self._stall_check(pins, track=False):
self._eof = True
return
if (self.line_events(pins)):
@@ -1427,12 +1432,22 @@ class Decoder(srd.Decoder):
self._start_fall = self.samplenum
return
def _stall_check(self, pins):
def _stall_check(self, pins, track=True):
'''Detect end-of-capture: libsigrokdecode stops advancing the sample
counter once the feed is exhausted, so a wait that returns the same
sample number repeatedly means there is no more data.'''
sample number repeatedly means there is no more data.
`track=False` is used for idle-LEVEL waits: a level condition that is
already satisfied returns immediately at the current sample without
advancing, which is normal on an idle line and NOT end-of-capture.
Only a ``None`` pin set (true end-of-stream) is treated as EOF there;
the stall window is reset so a following edge wait starts fresh.'''
if pins is None:
return True
if not track:
self._prev_wait_sn = None
self._stall = 0
return False
if self._prev_wait_sn is not None and self.samplenum == self._prev_wait_sn:
self._stall += 1
else:
@@ -1556,6 +1571,11 @@ class Decoder(srd.Decoder):
idle_level = 0 if self._inverse_convention else 1
for _ in range(loops):
pins = self.wait({'skip': 0})
if pins is None:
# End of capture / session stopped mid-resync: stop cleanly
# instead of raising TypeError on the None pin state.
self._eof = True
return
if pins[self.DATA_IDX] == idle_level:
return
# Could not find an idle gap -- resume normal decode anyway.
@@ -2002,12 +2022,29 @@ class Decoder(srd.Decoder):
self.emit_packet(GSMTAP_SIM_PPS, bytes(pps_req + pps_rsp), ss, self.samplenum)
def _at_eof(self):
'''True when the sample stream is exhausted.
libsigrokdecode stops advancing the sample counter once the feed is
gone, so a stuck sample number normally means end-of-capture. But a
step may legitimately consume only queued bytes (_replay /
peeked_byte) without touching the stream (e.g. the invalid-INS
one-byte re-frame); allow a bounded number of those so a re-frame is
never mistaken for EOF.'''
if self._eof:
return True
if self.samplenum != self._last_sn:
self._no_advance = 0
return False
if ((self._replay or self.peeked_byte is not None)
and self._no_advance < 64):
self._no_advance += 1
return False
return True
def decode_step(self):
'''Run one state-machine step. Returns False when decoding ends.'''
# End-of-capture detection: libsigrokdecode stops advancing the sample
# counter once the feed is exhausted, so a stuck sample number means
# we have consumed everything (and any pending wait is non-blocking).
if self.samplenum == self._last_sn or self._eof:
if self._at_eof():
return False
self._last_sn = self.samplenum
# Prime the initial RST/VCC levels from the actual first sample (no