v1.9.6: ETU lock validation, hunt recovery, resync + ATR acceptance
The audit's P1 robustness findings:
- _etu_candidate() validates a measured burst: requires >=2 usable
spacings, rejects a guard-dominated burst (all-zero data: 10-ETU LOW
runs + a 2-5 ETU HIGH guard) and a lone 10-ETU spacing. A failed
measurement leaves bit_samples unset instead of committing the
clock_skip*3 fallback.
- _confirm_etu() no longer counts a dropped frame (empty bits) as
parity-OK, and a rejected measurement clears bit_samples,
_etu_measured and _clock_skip_confident (was: kept sampling at the
rejected rate).
- Hunt ETU recovery keys off _etu_measured: handle_atr pre-seeds
bit_samples, so the old "bit_samples is None" condition never fired.
- resync_idle() waits on the DATA idle level (plus RST/VCC edges,
bounded) instead of spinning on wait({'skip': 0}) at one sample.
- ATR acceptance: a DATA-state 0x3B/0x3F is a candidate only before the
first APDU (_apdu_seen); a parsed ATR is ignored on any parity error
or when it is a bare TS+T0. A data run misread at a frame boundary
could fabricate an ATR and force clock_skip=372 for the capture.
- Inverse convention per ISO 7816-3 7.1/8.1: idle HIGH and a falling
start bit in both conventions, msb-first inverse data, raw moments
5-7 TS-pattern detection (a real inverse TS reads as 0x03 through
the direct reader), and a TS parity exemption (the inverse pattern
is odd over moments 2-10). Spec-verified; no inverse capture in the
corpus.
Validation: 117/117 unit tests (18 new); the 10-trace suite is
unchanged from v1.9.5 (APDU/CHKSUM/CONCAT/BAD_FCS identical,
RESULT: OK everywhere); compare_baseline.sh reports IDENTICAL.
VERSION -> 1.9.6.
This commit is contained in:
1 parent
b574f900a1
commit
3e39032802
6 files changed
+540
-85
No files matched your search
@@ -333,6 +333,63 @@ Known limitation: no on-disk fixture reproduces the exact live failure (all
|
|||||||
real captures contain a 372 ATR), so end-to-end proof needs a re-captured
|
real captures contain a 372 ATR), so end-to-end proof needs a re-captured
|
||||||
`.sr` from the failing phone; the unit tests cover the loop-escape logic.
|
`.sr` from the failing phone; the unit tests cover the loop-escape logic.
|
||||||
|
|
||||||
|
### v1.9.6: ETU lock validation, hunt recovery, resync + ATR acceptance
|
||||||
|
|
||||||
|
The audit's P1 findings. The validation helpers are unit-tested; the trace
|
||||||
|
suite stays at the v1.9.5 metrics.
|
||||||
|
|
||||||
|
- **ETU measurement validation (`_etu_candidate`).** The mid-session
|
||||||
|
estimator used to lock the inter-character guard as the ETU on all-zero
|
||||||
|
data: a 0x00 run gives 10-ETU LOW runs plus a short HIGH guard, so the
|
||||||
|
smallest recurring spacing is the guard (2x-3x the true ETU), and a lone
|
||||||
|
0x00 character's single 10-ETU spacing is 10x. A burst now needs at
|
||||||
|
least two usable spacings, and a burst whose minimum recurrence is a HIGH
|
||||||
|
guard with every other spacing >= 2x it is rejected -- the decoder waits
|
||||||
|
for a burst with bit transitions (this covers the 2-5 ETU guard range).
|
||||||
|
A failed measurement leaves `bit_samples` unset instead of committing the
|
||||||
|
old `clock_skip*3` guess.
|
||||||
|
- **Invalidation.** `_confirm_etu()` no longer counts a dropped frame (no
|
||||||
|
genuine start -> empty bits) as parity-OK, and a rejected measurement now
|
||||||
|
clears `bit_samples`, `_etu_measured` and `_clock_skip_confident` (the old
|
||||||
|
code kept sampling at the rejected rate).
|
||||||
|
- **Hunt ETU recovery.** `handle_atr()` pre-seeds `bit_samples` with the
|
||||||
|
default-rate guess while the recovery condition tested `bit_samples is
|
||||||
|
None`, so the v1.6.0 live-ETU recovery could never fire from the hunt.
|
||||||
|
The condition now keys off `_etu_measured` (a real DATA measurement); a
|
||||||
|
measured rate still suppresses the recovery.
|
||||||
|
- **`resync_idle()`** now actually waits: it used `wait({'skip': 0})` in a
|
||||||
|
loop (the same sample 48 times -> a no-op). It now waits on the DATA idle
|
||||||
|
level (plus RST/VCC edges, bounded) before the next peek.
|
||||||
|
- **ATR acceptance.** A DATA-state `0x3B`/`0x3F` is an ATR candidate only
|
||||||
|
before any APDU has been emitted (`_apdu_seen`); a parsed ATR is rejected
|
||||||
|
when any of its bytes had a parity error or when it is a bare TS+T0
|
||||||
|
(`3B 00`). A data run misread at a frame boundary used to be committed
|
||||||
|
as an ATR (a fabricated `3b80900000` appeared while developing this
|
||||||
|
change), forcing `clock_skip=372` for the rest of the capture. Real
|
||||||
|
warm-reset ATRs are parity-clean and commit as before.
|
||||||
|
- **Inverse convention (TS=0x3F).** The readers assumed an idle-LOW line
|
||||||
|
and a rising start bit; ISO 7816-3 7.1 keeps the line idle HIGH and the
|
||||||
|
start bit LOW in both conventions -- only the data polarity and order
|
||||||
|
change. `wait_data_falling`/`resync_idle` now use the HIGH idle for
|
||||||
|
inverse too, `get_bytes()` assembles the inverse data msb-first, and the
|
||||||
|
DATA state accepts `0x3F` alongside `0x3B`. The data readers already
|
||||||
|
inverted the bit polarity. A real inverse TS decodes as `0x03` through
|
||||||
|
the direct reader, so `ts_from_bits()` matches the raw ISO 7816-3 8.1
|
||||||
|
pattern (moments 1-4 LHHL, 5-7 LLL/HHH, 8-10 LLH) in both the hunt and
|
||||||
|
the DATA state instead of trusting the decoded byte. The readers exempt
|
||||||
|
a matching TS from the parity check: its moment 10 is fixed by the
|
||||||
|
pattern, and the inverse TS is legitimately odd over moments 2-10, so it
|
||||||
|
must not log a CHKSUM or reject the ATR. No inverse-convention capture
|
||||||
|
exists in the corpus, so this is spec-verified, not trace-verified.
|
||||||
|
|
||||||
|
Validation: 117/117 unit tests (18 new: the ETU candidate vectors, the
|
||||||
|
hunt-recovery seeded/measured cases, the resync level wait, the bare-ATR
|
||||||
|
and parity-ATR guards, the `_apdu_seen` gate both ways, the inverse
|
||||||
|
bit-order/start-edge/TS-pattern vectors); the 10-trace suite is unchanged
|
||||||
|
from v1.9.5 (identical APDU/CHKSUM/CONCAT/BAD_FCS counts, `RESULT: OK`
|
||||||
|
everywhere);
|
||||||
|
`compare_baseline.sh` reports IDENTICAL. VERSION -> 1.9.6.
|
||||||
|
|
||||||
### v1.9.5: T=0 framing — immediate SW, short responses, Le=256
|
### v1.9.5: T=0 framing — immediate SW, short responses, Le=256
|
||||||
|
|
||||||
The audit's P0 framing fixes, each pinned by a `TestDecodeStep` vector:
|
The audit's P0 framing fixes, each pinned by a `TestDecodeStep` vector:
|
||||||
@@ -914,7 +971,7 @@ append the measured CLK frequency when known
|
|||||||
|
|
||||||
### Versioning
|
### Versioning
|
||||||
|
|
||||||
The decoder version is defined in `pd.py` as `VERSION = '1.9.5'`.
|
The decoder version is defined in `pd.py` as `VERSION = '1.9.6'`.
|
||||||
The version is printed to the log on decoder startup.
|
The version is printed to the log on decoder startup.
|
||||||
|
|
||||||
### Testing after decoder changes
|
### Testing after decoder changes
|
||||||
@@ -1165,13 +1222,17 @@ discarded ideas. "Proven" = kept in `pd.py`; "Rejected" = do not re-add.
|
|||||||
native skip becomes F/D CLK cycles/bit. Reliable (verified live).
|
native skip becomes F/D CLK cycles/bit. Reliable (verified live).
|
||||||
- **T=1 block-chain follow-up bound.** Caps chained I-blocks so garbage
|
- **T=1 block-chain follow-up bound.** Caps chained I-blocks so garbage
|
||||||
cannot loop forever. Reliable.
|
cannot loop forever. Reliable.
|
||||||
- **Synthetic T=0 ATR fallback `[0x3B, 0x00]`.** After 8 failed ATR hunts,
|
- **Hunt give-up (no synthetic ATR).** After `ATR_HUNT_FAIL_LIMIT` failed
|
||||||
commit a minimal ATR and set `hasT0=True`. Reliable for mid-session starts
|
hunts the decoder resumes DATA without fabricating an ATR (v1.6.0 removed
|
||||||
(no ATR in the trace).
|
the old `[0x3B, 0x00]` fallback); the mid-session ETU estimator then
|
||||||
|
recovers the rate from DATA. Reliable.
|
||||||
- **ETU auto-detect from DATA via edge counting (`_measure_etu`).** Edge-waits
|
- **ETU auto-detect from DATA via edge counting (`_measure_etu`).** Edge-waits
|
||||||
to the first DATA start-bit fall, measures ETU from DATA-edge spacing, locks
|
to the first DATA start-bit fall, measures ETU from DATA-edge spacing, locks
|
||||||
it **once**. Reliable for gated/stopped CLK (phone, ~53 samples). Locking
|
it **once**. Reliable for gated/stopped CLK (phone, ~53 samples). Locking
|
||||||
once (not re-measuring) is drift-immune and avoids glitch re-anchors.
|
once (not re-measuring) is drift-immune and avoids glitch re-anchors. A
|
||||||
|
burst must yield >=2 usable spacings, and a guard-dominated burst (all-zero
|
||||||
|
data: long LOW runs + short HIGH guard) is rejected (`_etu_candidate`,
|
||||||
|
v1.9.6); a rate that fails parity is invalidated and re-measured.
|
||||||
- **Edge-list bit reader (`_read_byte_edges`).** Per-byte edge reconstruction
|
- **Edge-list bit reader (`_read_byte_edges`).** Per-byte edge reconstruction
|
||||||
from the DATA edge list (re-anchors the next byte via `_next_start_fall`).
|
from the DATA edge list (re-anchors the next byte via `_next_start_fall`).
|
||||||
Drift-immune. Reliable for mid-session/edge mode; gated by `_edge_read`.
|
Drift-immune. Reliable for mid-session/edge mode; gated by `_edge_read`.
|
||||||
@@ -1189,10 +1250,10 @@ discarded ideas. "Proven" = kept in `pd.py`; "Rejected" = do not re-add.
|
|||||||
bit_samples` distinguishes genuine inter-byte idle gaps (≥2 ETU per ISO
|
bit_samples` distinguishes genuine inter-byte idle gaps (≥2 ETU per ISO
|
||||||
7816-3) from intra-byte HIGH periods (≤1 ETU). Prevents byte misalignment
|
7816-3) from intra-byte HIGH periods (≤1 ETU). Prevents byte misalignment
|
||||||
after `_measure_etu()` consumption (v1.1.1 fix for post-ATR desync).
|
after `_measure_etu()` consumption (v1.1.1 fix for post-ATR desync).
|
||||||
- **Inverse-convention ATR detection in DATA state.** `firstByte == 0x3f`
|
- **Inverse-convention support (TS=0x3F, v1.9.6).** The hunt already read
|
||||||
alongside `0x3b` at line 1656, so inverse-convention ATRs (TS=0x3F) are
|
`0x3F`; the DATA state now accepts it too, the start bit stays a falling
|
||||||
parsed by the default state. Sets `clock_skip=372` correctly
|
edge from an idle-HIGH line (ISO 7816-3 7.1), and `get_bytes()` assembles
|
||||||
before PPS (v1.1.1 fix).
|
inverse data msb-first. Spec-verified; no inverse capture in the corpus.
|
||||||
- **`_resync = True` after PPS acceptance (native CLK path).** Forces
|
- **`_resync = True` after PPS acceptance (native CLK path).** Forces
|
||||||
`resync_idle()` before the first post-PPS command, preventing the decoder
|
`resync_idle()` before the first post-PPS command, preventing the decoder
|
||||||
from latching onto a mid-byte transition at the new speed (v1.1.1 fix).
|
from latching onto a mid-byte transition at the new speed (v1.1.1 fix).
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ this decoder's `pcap_file=<path>` option).
|
|||||||
| VCC tracking (optional) | `vcc_detect=true`: emits power on/off events (sub_type 0x11) |
|
| VCC tracking (optional) | `vcc_detect=true`: emits power on/off events (sub_type 0x11) |
|
||||||
| Direct PCAP file | `pcap_file=<path>` writes PCAP without shell redirection |
|
| Direct PCAP file | `pcap_file=<path>` writes PCAP without shell redirection |
|
||||||
| Start-bit deglitch | 2-sample deglitch in the start-bit detector eliminates sub-ETU LOW glitches from FX2LP pull-up artifacts, preventing byte misalignment on fast phone traces |
|
| Start-bit deglitch | 2-sample deglitch in the start-bit detector eliminates sub-ETU LOW glitches from FX2LP pull-up artifacts, preventing byte misalignment on fast phone traces |
|
||||||
|
| Mid-session ETU lock | The DATA-edge ETU estimator rejects guard-only (all-zero) and single-spacing bursts and invalidates a rate that fails parity, so a capture at a non-default F/D locks safely |
|
||||||
|
| ATR / resync guards | A DATA-state `0x3B`/`0x3F` is an ATR candidate only before the first APDU; parity-dirty or bare (TS+T0) frames are ignored, and `resync_idle` really waits for the idle level |
|
||||||
|
| Inverse convention | TS=0x3F ATRs follow ISO 7816-3 7.1: inverted data, msb first, start bit still LOW from an idle-HIGH line |
|
||||||
|
|
||||||
## Decoder options
|
## Decoder options
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ GSMTAP pcap-файлы (включая PCAP, который этот декод
|
|||||||
| Отслеживание VCC (опционально) | `vcc_detect=true`: события включения/выключения питания (sub_type 0x11) |
|
| Отслеживание VCC (опционально) | `vcc_detect=true`: события включения/выключения питания (sub_type 0x11) |
|
||||||
| PCAP сразу в файл | `pcap_file=<path>` пишет PCAP без перенаправления вывода оболочки |
|
| PCAP сразу в файл | `pcap_file=<path>` пишет PCAP без перенаправления вывода оболочки |
|
||||||
| Деглитч стартового бита | Деглитч в 2 отсчёта в детекторе стартового бита убирает LOW-глитчи короче ETU от артефактов подтяжки FX2LP и предотвращает смещение байтов на быстрых телефонных трассах |
|
| Деглитч стартового бита | Деглитч в 2 отсчёта в детекторе стартового бита убирает LOW-глитчи короче ETU от артефактов подтяжки FX2LP и предотвращает смещение байтов на быстрых телефонных трассах |
|
||||||
|
| Захват ETU в середине сессии | Оценщик ETU по фронтам DATA отбрасывает пачки только со сторожевым интервалом (нулевые данные) и одиночные интервалы, а также сбрасывает скорость, не прошедшую проверку чётности, поэтому трасса с нестандартным F/D захватывается надёжно |
|
||||||
|
| Защита ATR / ресинхронизации | `0x3B`/`0x3F` в состоянии DATA считается кандидатом в ATR только до первого APDU; кадры с ошибкой чётности или голый TS+T0 игнорируются, а `resync_idle` действительно ждёт уровень простоя |
|
||||||
|
| Инверсная конвенция | ATR с TS=0x3F разбирается по ISO 7816-3 7.1: инверсные данные, msb первым, стартовый бит по-прежнему LOW при простое линии HIGH |
|
||||||
|
|
||||||
## Опции декодера
|
## Опции декодера
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from .gsmtap_stream import (GsmtapStreamSender,
|
|||||||
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
|
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
|
||||||
GSMTAP_FLAG_BAD_FCS)
|
GSMTAP_FLAG_BAD_FCS)
|
||||||
|
|
||||||
VERSION = '1.9.5'
|
VERSION = '1.9.6'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -196,6 +196,26 @@ def plausible_ins(ins):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def ts_from_bits(bits):
|
||||||
|
'''Return the TS value (0x3B direct / 0x3F inverse) when the raw ten
|
||||||
|
character bits match an ISO 7816-3 8.1 initial-character pattern, else
|
||||||
|
None.
|
||||||
|
|
||||||
|
Both TS patterns share moments 1-4 LHHL and 8-10 LLH; moments 5-7 are
|
||||||
|
LLL (inverse: state L encodes 1, msb first) or HHH (direct: state H
|
||||||
|
encodes 1, lsb first). The decoded byte alone cannot tell them apart:
|
||||||
|
the inverse TS reads as 0x03 through a direct lsb-first assembly.'''
|
||||||
|
if len(bits) < 10:
|
||||||
|
return None
|
||||||
|
if bits[0:4] != [0, 1, 1, 0] or bits[7:10] != [0, 0, 1]:
|
||||||
|
return None
|
||||||
|
if not any(bits[4:7]):
|
||||||
|
return 0x3f
|
||||||
|
if all(bits[4:7]):
|
||||||
|
return 0x3b
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def is_idle_noise_byte(first_byte):
|
def is_idle_noise_byte(first_byte):
|
||||||
'''Check if a byte looks like an idle-line artifact rather than a
|
'''Check if a byte looks like an idle-line artifact rather than a
|
||||||
command class byte.
|
command class byte.
|
||||||
@@ -533,6 +553,11 @@ class Decoder(srd.Decoder):
|
|||||||
# handling -- native mode never measures it explicitly.
|
# handling -- native mode never measures it explicitly.
|
||||||
self.detected_clock_skip = 372
|
self.detected_clock_skip = 372
|
||||||
self.bit_samples = None
|
self.bit_samples = None
|
||||||
|
# True only when bit_samples came from a real DATA-edge
|
||||||
|
# measurement (not a protocol/fallback guess). The hunt-side ETU
|
||||||
|
# recovery and the clock_skip commit key off this, so a pre-seeded
|
||||||
|
# default cannot mask a failed measurement.
|
||||||
|
self._etu_measured = False
|
||||||
self._etu_confirm_count = 0
|
self._etu_confirm_count = 0
|
||||||
self._etu_fail_count = 0
|
self._etu_fail_count = 0
|
||||||
self._edge_spacings = deque(maxlen=128)
|
self._edge_spacings = deque(maxlen=128)
|
||||||
@@ -566,6 +591,11 @@ class Decoder(srd.Decoder):
|
|||||||
# (e.g. GSM TX bursts coupling into the probe wiring).
|
# (e.g. GSM TX bursts coupling into the probe wiring).
|
||||||
self.parity_hist = deque(maxlen=64)
|
self.parity_hist = deque(maxlen=64)
|
||||||
self.sig_warned = False
|
self.sig_warned = False
|
||||||
|
# Total parity failures (ATR validation) and "APDU traffic has
|
||||||
|
# started" (a DATA-state 0x3B is only an ATR before the first APDU;
|
||||||
|
# afterwards it is data misread at a frame boundary).
|
||||||
|
self._parity_errors = 0
|
||||||
|
self._apdu_seen = False
|
||||||
# Stuck-line watchdog: consecutive all-zero frames usually mean
|
# Stuck-line watchdog: consecutive all-zero frames usually mean
|
||||||
# the I/O probe lost contact (floating low), not data.
|
# the I/O probe lost contact (floating low), not data.
|
||||||
self.zero_run = 0
|
self.zero_run = 0
|
||||||
@@ -819,6 +849,8 @@ class Decoder(srd.Decoder):
|
|||||||
self._inverse_convention = False
|
self._inverse_convention = False
|
||||||
self.clock_skip = 372
|
self.clock_skip = 372
|
||||||
self.bit_samples = None
|
self.bit_samples = None
|
||||||
|
self._etu_measured = False
|
||||||
|
self._apdu_seen = False
|
||||||
self.state = 'FIND START'
|
self.state = 'FIND START'
|
||||||
self._atr_hunt_count = 0
|
self._atr_hunt_count = 0
|
||||||
self._pps_attempted = False
|
self._pps_attempted = False
|
||||||
@@ -852,6 +884,7 @@ class Decoder(srd.Decoder):
|
|||||||
self._atr_hunt_count = 0
|
self._atr_hunt_count = 0
|
||||||
self._pps_attempted = False
|
self._pps_attempted = False
|
||||||
self.bit_samples = None
|
self.bit_samples = None
|
||||||
|
self._etu_measured = False
|
||||||
self._etu_confirm_count = 0
|
self._etu_confirm_count = 0
|
||||||
self._etu_fail_count = 0
|
self._etu_fail_count = 0
|
||||||
self.clock_skip = 372
|
self.clock_skip = 372
|
||||||
@@ -887,8 +920,16 @@ class Decoder(srd.Decoder):
|
|||||||
return h
|
return h
|
||||||
|
|
||||||
def get_bytes(self, bits):
|
def get_bytes(self, bits):
|
||||||
|
bits = list(bits)
|
||||||
|
if self._inverse_convention:
|
||||||
|
# ISO 7816-3 7.1: direct sends the lsb first, inverse the msb
|
||||||
|
# first; the readers have already applied the polarity.
|
||||||
|
byte = 0
|
||||||
|
for bit in bits:
|
||||||
|
byte = (byte << 1) | bit
|
||||||
|
return byte
|
||||||
byte = 0
|
byte = 0
|
||||||
for bit in list(reversed(bits)):
|
for bit in reversed(bits):
|
||||||
byte = (byte << 1) | bit
|
byte = (byte << 1) | bit
|
||||||
return byte
|
return byte
|
||||||
|
|
||||||
@@ -911,6 +952,8 @@ class Decoder(srd.Decoder):
|
|||||||
monitor; warn once per degradation episode when >= 8 of the last
|
monitor; warn once per degradation episode when >= 8 of the last
|
||||||
64 frames failed parity.'''
|
64 frames failed parity.'''
|
||||||
self.parity_hist.append(err)
|
self.parity_hist.append(err)
|
||||||
|
if err:
|
||||||
|
self._parity_errors += 1
|
||||||
if len(self.parity_hist) < 32:
|
if len(self.parity_hist) < 32:
|
||||||
return
|
return
|
||||||
errs = sum(self.parity_hist)
|
errs = sum(self.parity_hist)
|
||||||
@@ -993,12 +1036,14 @@ class Decoder(srd.Decoder):
|
|||||||
|
|
||||||
For a mid-session capture (no ATR) the card may already be PPS'd to a
|
For a mid-session capture (no ATR) the card may already be PPS'd to a
|
||||||
fast ETU, so the default clock_skip=372 is wrong. Once _measure_etu
|
fast ETU, so the default clock_skip=372 is wrong. Once _measure_etu
|
||||||
has recovered bit_samples (samples per ETU) and _measure_clock_period
|
has recovered bit_samples (samples per ETU) from real DATA edges and
|
||||||
has recovered _samples_per_clock (samples per CLK cycle), clock_skip
|
_measure_clock_period has recovered _samples_per_clock (samples per
|
||||||
= bit_samples / _samples_per_clock. Sets _clock_skip_confident on
|
CLK cycle), clock_skip = bit_samples / _samples_per_clock. Only a
|
||||||
success so the CLK-sync reader is used; leaves it clear (edge-list
|
real DATA measurement is trusted (_etu_measured); a failed or
|
||||||
fallback) if the CLK period could not be measured.'''
|
rejected measurement never commits a rate. A later parity failure
|
||||||
if self._samples_per_clock and self.bit_samples:
|
invalidates it (_confirm_etu).'''
|
||||||
|
if (self._etu_measured and self._samples_per_clock
|
||||||
|
and self.bit_samples):
|
||||||
cs = int(round(float(self.bit_samples) / float(self._samples_per_clock)))
|
cs = int(round(float(self.bit_samples) / float(self._samples_per_clock)))
|
||||||
if cs >= 1:
|
if cs >= 1:
|
||||||
if cs != self.clock_skip:
|
if cs != self.clock_skip:
|
||||||
@@ -1010,6 +1055,38 @@ class Decoder(srd.Decoder):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _etu_candidate(self, pairs):
|
||||||
|
'''Return (etu, ok) from (spacing, level) edge pairs.
|
||||||
|
|
||||||
|
Filters glitches, drops a leading burst-boundary outlier, and
|
||||||
|
rejects a burst whose minimum recurrence is the inter-character
|
||||||
|
guard rather than a bit period: all-zero data produces long LOW
|
||||||
|
runs and a short HIGH guard, so the smallest recurring spacing is
|
||||||
|
the guard (2x or more the true ETU). A burst with bit transitions
|
||||||
|
has a smaller spacing somewhere, so the guard pattern is
|
||||||
|
distinguishable when every non-minimum spacing is at least 2x the
|
||||||
|
minimum.'''
|
||||||
|
MIN_ETU = 12
|
||||||
|
valid = [p for p in pairs if p[0] >= MIN_ETU]
|
||||||
|
if len(valid) >= 2:
|
||||||
|
med = sorted(p[0] for p in valid)[len(valid) // 2]
|
||||||
|
# If the first valid spacing is >10x the median, the scan
|
||||||
|
# started at a burst boundary — drop leading outliers.
|
||||||
|
while valid and valid[0][0] > med * 10:
|
||||||
|
valid.pop(0)
|
||||||
|
if len(valid) < 2:
|
||||||
|
# A single spacing cannot prove a recurring bit period (a lone
|
||||||
|
# 0x00 character yields one 10-ETU run -> 10x the true ETU).
|
||||||
|
return (None, False)
|
||||||
|
spacings = [p[0] for p in valid]
|
||||||
|
etu = self._robust_min(spacings)
|
||||||
|
guard_dominated = (
|
||||||
|
all(lvl == 1 for sp, lvl in valid if sp == etu)
|
||||||
|
and all(sp >= 2 * etu for sp, lvl in valid if sp != etu))
|
||||||
|
if guard_dominated or not (4 <= etu <= 100000):
|
||||||
|
return (etu, False)
|
||||||
|
return (etu, True)
|
||||||
|
|
||||||
def _measure_etu(self):
|
def _measure_etu(self):
|
||||||
'''Recover the live bit period (ETU, in samples) from the DATA line for
|
'''Recover the live bit period (ETU, in samples) from the DATA line for
|
||||||
a mid-session capture that has no ATR to derive timing from.
|
a mid-session capture that has no ATR to derive timing from.
|
||||||
@@ -1056,35 +1133,24 @@ class Decoder(srd.Decoder):
|
|||||||
break
|
break
|
||||||
edges.append((sn, d))
|
edges.append((sn, d))
|
||||||
if len(edges) >= 2:
|
if len(edges) >= 2:
|
||||||
spacings = [edges[i + 1][0] - edges[i][0]
|
pairs = [(edges[i + 1][0] - edges[i][0], edges[i][1])
|
||||||
for i in range(len(edges) - 1)]
|
for i in range(len(edges) - 1)]
|
||||||
# Filter glitches (< MIN_ETU) then check if the first spacing
|
etu, ok = self._etu_candidate(pairs)
|
||||||
# is an outlier (scan started in a noisy pre-burst region).
|
if ok:
|
||||||
MIN_ETU = 12
|
|
||||||
valid = [s for s in spacings if s >= MIN_ETU]
|
|
||||||
if len(valid) >= 2:
|
|
||||||
med = sorted(valid)[len(valid) // 2]
|
|
||||||
# If the first valid spacing is >10x the median, the scan
|
|
||||||
# started at a burst boundary — drop leading outliers.
|
|
||||||
while valid and valid[0] > med * 10:
|
|
||||||
valid.pop(0)
|
|
||||||
spacings = valid if valid else spacings
|
|
||||||
etu = self._robust_min(spacings)
|
|
||||||
# Sanity clamp: a real ISO 7816 etu is >= a few samples and below
|
|
||||||
# ~100k samples even at extreme F/D. Anything else is a glitch.
|
|
||||||
if 4 <= etu <= 100000:
|
|
||||||
self.bit_samples = etu
|
self.bit_samples = etu
|
||||||
|
self._etu_measured = True
|
||||||
self._etu_confirm_count = 0
|
self._etu_confirm_count = 0
|
||||||
self._etu_fail_count = 0
|
self._etu_fail_count = 0
|
||||||
if self.bit_samples is None:
|
else:
|
||||||
self.bit_samples = self._compute_bit_samples()
|
self.log("ETU measurement rejected", etu,
|
||||||
self._etu_confirm_count = 0
|
"spacings", [p[0] for p in pairs][:8])
|
||||||
self._etu_fail_count = 0
|
|
||||||
# The live line is gated/stopped CLK; use the edge-list bit reader
|
# The live line is gated/stopped CLK; use the edge-list bit reader
|
||||||
# (immune to the drift a fixed sample-skip cadence would suffer) for
|
# (immune to the drift of fixed sample-skip timing) for every byte.
|
||||||
# every byte from here on.
|
# The CLK-sync reader is used once _derive_clock_skip_from_etu()
|
||||||
|
# has a real measurement; a parity failure invalidates it.
|
||||||
self._edge_read = True
|
self._edge_read = True
|
||||||
self.log("etu recovered (samples):", self.bit_samples)
|
if self._etu_measured:
|
||||||
|
self.log("etu recovered (samples):", self.bit_samples)
|
||||||
|
|
||||||
def _recover_etu_in_hunt(self):
|
def _recover_etu_in_hunt(self):
|
||||||
'''Recover the live bit period from the DATA line while still in the
|
'''Recover the live bit period from the DATA line while still in the
|
||||||
@@ -1122,7 +1188,10 @@ class Decoder(srd.Decoder):
|
|||||||
so it is unit-testable without libsigrokdecode.'''
|
so it is unit-testable without libsigrokdecode.'''
|
||||||
self._atr_fail_total += 1
|
self._atr_fail_total += 1
|
||||||
# Live-ETU recovery every 2 failures, up to ATR_HUNT_ETU_ATTEMPTS.
|
# Live-ETU recovery every 2 failures, up to ATR_HUNT_ETU_ATTEMPTS.
|
||||||
if (self.bit_samples is None
|
# A real DATA measurement is required: bit_samples may be a
|
||||||
|
# pre-seeded default from handle_atr, which must not mask the need
|
||||||
|
# to recover the live rate.
|
||||||
|
if (not self._etu_measured
|
||||||
and self._hunt_etu_attempts < ATR_HUNT_ETU_ATTEMPTS
|
and self._hunt_etu_attempts < ATR_HUNT_ETU_ATTEMPTS
|
||||||
and self._atr_fail_total >=
|
and self._atr_fail_total >=
|
||||||
2 * (self._hunt_etu_attempts + 1)):
|
2 * (self._hunt_etu_attempts + 1)):
|
||||||
@@ -1149,12 +1218,19 @@ class Decoder(srd.Decoder):
|
|||||||
self._etu_confirm_count += 1
|
self._etu_confirm_count += 1
|
||||||
if self._etu_confirm_count >= 3:
|
if self._etu_confirm_count >= 3:
|
||||||
self.log("ETU confirmed:", self.bit_samples)
|
self.log("ETU confirmed:", self.bit_samples)
|
||||||
|
if self._etu_measured:
|
||||||
|
# The CLK-sync reader may trust clock_skip now.
|
||||||
|
self._clock_skip_confident = True
|
||||||
else:
|
else:
|
||||||
self._etu_fail_count += 1
|
self._etu_fail_count += 1
|
||||||
if self._etu_fail_count >= 2:
|
if self._etu_fail_count >= 2:
|
||||||
self.log("ETU re-measure (bad initial measurement):",
|
self.log("ETU re-measure (bad initial measurement):",
|
||||||
self.bit_samples)
|
self.bit_samples)
|
||||||
self.bit_samples = None
|
self.bit_samples = None
|
||||||
|
self._etu_measured = False
|
||||||
|
# Never keep sampling at a rejected rate: drop back to the
|
||||||
|
# edge reader until a fresh measurement is confirmed.
|
||||||
|
self._clock_skip_confident = False
|
||||||
self._etu_confirm_count = 0
|
self._etu_confirm_count = 0
|
||||||
self._etu_fail_count = 0
|
self._etu_fail_count = 0
|
||||||
|
|
||||||
@@ -1349,13 +1425,17 @@ class Decoder(srd.Decoder):
|
|||||||
bit = 1 - bit
|
bit = 1 - bit
|
||||||
self.bits.append(bit)
|
self.bits.append(bit)
|
||||||
self.es = self.samplenum
|
self.es = self.samplenum
|
||||||
self.signal_quality(1 if (self.bits.count(1) % 2 != 0) else 0)
|
# A TS's moment 10 is fixed by the ISO 7816-3 8.1 pattern (the
|
||||||
parity_ok = (self.bits.count(1) % 2 == 0)
|
# inverse TS is odd over moments 2-10); it is not a parity-checked
|
||||||
if (self.bits.count(1) % 2 != 0):
|
# data character, so it must neither warn nor feed the ETU check.
|
||||||
|
is_ts = ts_from_bits(self.bits) is not None
|
||||||
|
parity_ok = is_ts or (self.bits.count(1) % 2 == 0)
|
||||||
|
self.signal_quality(0 if parity_ok else 1)
|
||||||
|
if not parity_ok:
|
||||||
self.log(self.samplenum, "CHKSUM ERROR: ", 0, "bits: ", self.bits)
|
self.log(self.samplenum, "CHKSUM ERROR: ", 0, "bits: ", self.bits)
|
||||||
self.put(self.ss, self.samplenum, self.out_ann,
|
self.put(self.ss, self.samplenum, self.out_ann,
|
||||||
[0, ["CHKSUM ERROR bits={bits}".format(bits=self.bits)]])
|
[0, ["CHKSUM ERROR bits={bits}".format(bits=self.bits)]])
|
||||||
if self.bit_samples is not None:
|
if self.bit_samples is not None and self.bits:
|
||||||
self._confirm_etu(parity_ok)
|
self._confirm_etu(parity_ok)
|
||||||
byte = self.get_bytes(self.bits[1:9])
|
byte = self.get_bytes(self.bits[1:9])
|
||||||
# Stuck-line watchdog (same as the other readers).
|
# Stuck-line watchdog (same as the other readers).
|
||||||
@@ -1462,9 +1542,11 @@ class Decoder(srd.Decoder):
|
|||||||
if nxt is not None:
|
if nxt is not None:
|
||||||
self._start_fall = nxt
|
self._start_fall = nxt
|
||||||
self.es = self.samplenum
|
self.es = self.samplenum
|
||||||
self.signal_quality(1 if (self.bits.count(1) % 2 != 0) else 0)
|
# A TS is not a parity-checked data character (see _read_byte_edges).
|
||||||
parity_ok = (self.bits.count(1) % 2 == 0)
|
is_ts = ts_from_bits(self.bits) is not None
|
||||||
if (self.bits.count(1) % 2 != 0):
|
parity_ok = is_ts or (self.bits.count(1) % 2 == 0)
|
||||||
|
self.signal_quality(0 if parity_ok else 1)
|
||||||
|
if not parity_ok:
|
||||||
self.log(self.samplenum, "CHKSUM ERROR: ", 0, "bits: ", self.bits)
|
self.log(self.samplenum, "CHKSUM ERROR: ", 0, "bits: ", self.bits)
|
||||||
self.put(self.ss, self.samplenum, self.out_ann,
|
self.put(self.ss, self.samplenum, self.out_ann,
|
||||||
[0, ["CHKSUM ERROR bits={bits}".format(bits=self.bits)]])
|
[0, ["CHKSUM ERROR bits={bits}".format(bits=self.bits)]])
|
||||||
@@ -1511,14 +1593,11 @@ class Decoder(srd.Decoder):
|
|||||||
min_high = max(int(1.5 * self.bit_samples), 4)
|
min_high = max(int(1.5 * self.bit_samples), 4)
|
||||||
while True:
|
while True:
|
||||||
# Wait for DATA idle, servicing RST/VCC.
|
# Wait for DATA idle, servicing RST/VCC.
|
||||||
# In direct convention: idle = HIGH, start bit = falling edge (HIGH→LOW).
|
# ISO 7816-3 7.1: the line idles HIGH and the start bit is
|
||||||
# In inverse convention: idle = LOW, start bit = rising edge (LOW→HIGH).
|
# always LOW (falling edge) in BOTH conventions; only the
|
||||||
if self._inverse_convention:
|
# data-bit polarity and order differ.
|
||||||
idle_cond = {self.DATA_IDX: 'l'} # Wait for LOW (idle in inverse)
|
idle_cond = {self.DATA_IDX: 'h'}
|
||||||
start_edge = 'r' # Rising edge = start bit in inverse
|
start_edge = 'f'
|
||||||
else:
|
|
||||||
idle_cond = {self.DATA_IDX: 'h'} # Wait for HIGH (idle in direct)
|
|
||||||
start_edge = 'f' # Falling edge = start bit in direct
|
|
||||||
cond = [idle_cond]
|
cond = [idle_cond]
|
||||||
if (self.track_rst):
|
if (self.track_rst):
|
||||||
cond.append({self.RST_IDX: 'e'})
|
cond.append({self.RST_IDX: 'e'})
|
||||||
@@ -1555,7 +1634,7 @@ class Decoder(srd.Decoder):
|
|||||||
pins = self.wait({'skip': self._min_pulse_samples})
|
pins = self.wait({'skip': self._min_pulse_samples})
|
||||||
if pins is not None:
|
if pins is not None:
|
||||||
bit = pins[self.DATA_IDX]
|
bit = pins[self.DATA_IDX]
|
||||||
idle_val = 1 if not self._inverse_convention else 0
|
idle_val = 1 # line HIGH is idle in both conventions
|
||||||
if bit == idle_val:
|
if bit == idle_val:
|
||||||
self._glitch_rejects += 1
|
self._glitch_rejects += 1
|
||||||
self.log("start-bit glitch rejected (#%d)"
|
self.log("start-bit glitch rejected (#%d)"
|
||||||
@@ -1639,8 +1718,10 @@ class Decoder(srd.Decoder):
|
|||||||
self.bits.append(bit)
|
self.bits.append(bit)
|
||||||
self.wait({'skip': self.clock_skip - 4})
|
self.wait({'skip': self.clock_skip - 4})
|
||||||
self.es = self.samplenum
|
self.es = self.samplenum
|
||||||
self.signal_quality(1 if (self.bits.count(1) % 2 != 0) else 0)
|
is_ts = ts_from_bits(self.bits) is not None
|
||||||
if (self.bits.count(1) % 2 != 0):
|
bad_parity = (not is_ts) and (self.bits.count(1) % 2 != 0)
|
||||||
|
self.signal_quality(1 if bad_parity else 0)
|
||||||
|
if bad_parity:
|
||||||
self.log(self.samplenum, "CHKSUM ERROR: ", pins[1], "bits: ", self.bits)
|
self.log(self.samplenum, "CHKSUM ERROR: ", pins[1], "bits: ", self.bits)
|
||||||
self.put(self.ss, self.samplenum, self.out_ann, [0, ["CHKSUM ERROR bits={bits}".format(bits=self.bits)]])
|
self.put(self.ss, self.samplenum, self.out_ann, [0, ["CHKSUM ERROR bits={bits}".format(bits=self.bits)]])
|
||||||
byte = self.get_bytes(self.bits[1:9])
|
byte = self.get_bytes(self.bits[1:9])
|
||||||
@@ -1688,29 +1769,37 @@ class Decoder(srd.Decoder):
|
|||||||
|
|
||||||
|
|
||||||
def resync_idle(self):
|
def resync_idle(self):
|
||||||
'''Re-align framing after a desync: wait for DATA to go to idle level
|
'''Re-align framing after a desync: wait for DATA to return to its
|
||||||
so the next peek lands on a genuine start bit of a fresh exchange
|
idle level so the next peek lands on a genuine start bit of a fresh
|
||||||
instead of a mid-burst byte. Gating-immune: we poll DATA in
|
exchange instead of a mid-burst byte. The wait is a level condition
|
||||||
samples (which advance even when CLK is frozen), and bound the hunt
|
(plus optional RST/VCC edges), so it advances the sample counter
|
||||||
by a sample budget so a pathologically-busy (or stuck) line never
|
instead of spinning on the current sample; a bounded loop guards
|
||||||
stalls the decoder.'''
|
against a pathologically busy or stuck line.'''
|
||||||
bs = self.bit_samples or self._compute_bit_samples()
|
bs = self.bit_samples or self._compute_bit_samples()
|
||||||
budget = int(IDLE_RESYNC_ETU * bs) * 8
|
budget = int(IDLE_RESYNC_ETU * bs) * 8
|
||||||
step = max(int(bs), 1)
|
step = max(int(bs), 1)
|
||||||
loops = budget // step + 16
|
loops = budget // step + 16
|
||||||
# In direct convention: idle = HIGH (mark).
|
# ISO 7816-3 7.1: the line idles HIGH in both conventions.
|
||||||
# In inverse convention: idle = LOW (mark is inverted).
|
idle_level = 1
|
||||||
idle_level = 0 if self._inverse_convention else 1
|
cond = [{self.DATA_IDX: 'h' if idle_level else 'l'}]
|
||||||
|
if self.track_rst:
|
||||||
|
cond.append({self.RST_IDX: 'e'})
|
||||||
|
if self.track_vcc:
|
||||||
|
cond.append({self.VCC_IDX: 'e'})
|
||||||
for _ in range(loops):
|
for _ in range(loops):
|
||||||
pins = self.wait({'skip': 0})
|
pins = self.wait(cond)
|
||||||
if pins is None:
|
if pins is None:
|
||||||
# End of capture / session stopped mid-resync: stop cleanly
|
# End of capture / session stopped mid-resync: stop cleanly
|
||||||
# instead of raising TypeError on the None pin state.
|
# instead of raising TypeError on the None pin state.
|
||||||
self._eof = True
|
self._eof = True
|
||||||
return
|
return
|
||||||
if pins[self.DATA_IDX] == idle_level:
|
if self._stall_check(pins, track=False):
|
||||||
|
self._eof = True
|
||||||
return
|
return
|
||||||
# Could not find an idle gap -- resume normal decode anyway.
|
if self.line_events(pins):
|
||||||
|
continue
|
||||||
|
return
|
||||||
|
# Could not find an idle level -- resume normal decode anyway.
|
||||||
|
|
||||||
def handle_atr(self, pins):
|
def handle_atr(self, pins):
|
||||||
|
|
||||||
@@ -1718,6 +1807,7 @@ class Decoder(srd.Decoder):
|
|||||||
|
|
||||||
self.log("START ATR:", self.samplenum);
|
self.log("START ATR:", self.samplenum);
|
||||||
self.state = 'ATR'
|
self.state = 'ATR'
|
||||||
|
bad_parity_before = self._parity_errors
|
||||||
|
|
||||||
tA = []
|
tA = []
|
||||||
tB = []
|
tB = []
|
||||||
@@ -1744,6 +1834,12 @@ class Decoder(srd.Decoder):
|
|||||||
else:
|
else:
|
||||||
byte = self.read_first_byte()
|
byte = self.read_first_byte()
|
||||||
atr_start = self.ss
|
atr_start = self.ss
|
||||||
|
# ISO 7816-3 8.1: an inverse-convention TS (moments 5-7 = LLL) is
|
||||||
|
# decoded as 0x03 by the direct reader; take the convention from
|
||||||
|
# the raw pattern so both TS forms are recognised.
|
||||||
|
raw_ts = ts_from_bits(self.bits)
|
||||||
|
if raw_ts is not None:
|
||||||
|
byte = raw_ts
|
||||||
# TS must be 0x3B (direct) or 0x3F (inverse) per TS 102 221 §5.2.2.
|
# TS must be 0x3B (direct) or 0x3F (inverse) per TS 102 221 §5.2.2.
|
||||||
# Pre-activation traffic (reader card-detect polling, a stray PPS
|
# Pre-activation traffic (reader card-detect polling, a stray PPS
|
||||||
# `ff 10 95 7a`, power-up noise) can appear on DATA before RST even
|
# `ff 10 95 7a`, power-up noise) can appear on DATA before RST even
|
||||||
@@ -1784,6 +1880,9 @@ class Decoder(srd.Decoder):
|
|||||||
# End of capture mid-hunt: stop cleanly instead of raising
|
# End of capture mid-hunt: stop cleanly instead of raising
|
||||||
# TypeError (None & 0x80) in the next loop iteration.
|
# TypeError (None & 0x80) in the next loop iteration.
|
||||||
return
|
return
|
||||||
|
raw_ts = ts_from_bits(self.bits)
|
||||||
|
if raw_ts is not None:
|
||||||
|
byte = raw_ts
|
||||||
atr_start = self.ss
|
atr_start = self.ss
|
||||||
ts_attempts += 1
|
ts_attempts += 1
|
||||||
if (byte not in (0x3b, 0x3f)):
|
if (byte not in (0x3b, 0x3f)):
|
||||||
@@ -1820,6 +1919,7 @@ class Decoder(srd.Decoder):
|
|||||||
count=self._atr_hunt_count,
|
count=self._atr_hunt_count,
|
||||||
fails=self._atr_fail_total))
|
fails=self._atr_fail_total))
|
||||||
self.bit_samples = None
|
self.bit_samples = None
|
||||||
|
self._etu_measured = False
|
||||||
self._etu_confirm_count = 0
|
self._etu_confirm_count = 0
|
||||||
self._etu_fail_count = 0
|
self._etu_fail_count = 0
|
||||||
self.hasT0 = True
|
self.hasT0 = True
|
||||||
@@ -1911,6 +2011,24 @@ class Decoder(srd.Decoder):
|
|||||||
self.put(atr_start, self.samplenum, self.out_ann, [0, ["Invalid TCK in ATR, got={tck:02x} expected={xor:02x}".format(tck=byte,xor=xor)]])
|
self.put(atr_start, self.samplenum, self.out_ann, [0, ["Invalid TCK in ATR, got={tck:02x} expected={xor:02x}".format(tck=byte,xor=xor)]])
|
||||||
self.log("Invalid TCK in ATR", hex(byte), hex(xor))
|
self.log("Invalid TCK in ATR", hex(byte), hex(xor))
|
||||||
|
|
||||||
|
if self._parity_errors != bad_parity_before:
|
||||||
|
# A real ATR is parity-clean; a data run misread at a frame
|
||||||
|
# boundary is not. Ignore the ATR instead of committing 372.
|
||||||
|
self.log("ATR rejected: parity error(s) in",
|
||||||
|
codecs.encode(bytes(self.ATR), 'hex'))
|
||||||
|
self.state = 'DATA'
|
||||||
|
self._resync = True
|
||||||
|
return
|
||||||
|
if len(self.ATR) < 2 or (len(self.ATR) < 3
|
||||||
|
and (self.ATR[1] & 0xF0) == 0):
|
||||||
|
# A bare TS+T0 ATR is legal ISO but not credible for a SIM/UICC:
|
||||||
|
# a 0x3B data byte at a frame boundary would otherwise fabricate
|
||||||
|
# an ATR and force clock_skip=372 for the rest of the capture.
|
||||||
|
self.log("implausible bare ATR ignored:",
|
||||||
|
codecs.encode(bytes(self.ATR), 'hex'))
|
||||||
|
self.state = 'DATA'
|
||||||
|
self._resync = True
|
||||||
|
return
|
||||||
self.put(atr_start, self.samplenum, self.out_ann, [2, ["ATR", "ATR={atr}".format(atr=codecs.encode(bytes(self.ATR), 'hex'))]])
|
self.put(atr_start, self.samplenum, self.out_ann, [2, ["ATR", "ATR={atr}".format(atr=codecs.encode(bytes(self.ATR), 'hex'))]])
|
||||||
self.put(atr_start, self.samplenum, self.out_python, [0, self.ATR])
|
self.put(atr_start, self.samplenum, self.out_python, [0, self.ATR])
|
||||||
self.emit_packet(GSMTAP_SIM_ATR, bytes(self.ATR), atr_start, self.samplenum)
|
self.emit_packet(GSMTAP_SIM_ATR, bytes(self.ATR), atr_start, self.samplenum)
|
||||||
@@ -1931,6 +2049,7 @@ class Decoder(srd.Decoder):
|
|||||||
# cleared bit_samples during ATR hunt (parity failures on noise),
|
# cleared bit_samples during ATR hunt (parity failures on noise),
|
||||||
# which would trigger _measure_etu() on the next decode_step.
|
# which would trigger _measure_etu() on the next decode_step.
|
||||||
self.bit_samples = self.clock_skip * (self._samples_per_clock or 3)
|
self.bit_samples = self.clock_skip * (self._samples_per_clock or 3)
|
||||||
|
self._etu_measured = True
|
||||||
# Lock the ETU so _confirm_etu() cannot clear bit_samples after
|
# Lock the ETU so _confirm_etu() cannot clear bit_samples after
|
||||||
# parity failures on post-ATR noise bytes. Without this, two
|
# parity failures on post-ATR noise bytes. Without this, two
|
||||||
# consecutive parity errors would trigger an ETU re-measure that
|
# consecutive parity errors would trigger an ETU re-measure that
|
||||||
@@ -2143,6 +2262,7 @@ class Decoder(srd.Decoder):
|
|||||||
else:
|
else:
|
||||||
self._pps_speed_changed = True
|
self._pps_speed_changed = True
|
||||||
self.bit_samples = None
|
self.bit_samples = None
|
||||||
|
self._etu_measured = False
|
||||||
self._start_fall = None
|
self._start_fall = None
|
||||||
# clock_skip now reflects the negotiated FI/DI, so it is known to
|
# clock_skip now reflects the negotiated FI/DI, so it is known to
|
||||||
# be correct regardless of which reader is active.
|
# be correct regardless of which reader is active.
|
||||||
@@ -2282,6 +2402,7 @@ class Decoder(srd.Decoder):
|
|||||||
self._atr_hunt_count = 0
|
self._atr_hunt_count = 0
|
||||||
self._pps_attempted = False
|
self._pps_attempted = False
|
||||||
self.bit_samples = None
|
self.bit_samples = None
|
||||||
|
self._etu_measured = False
|
||||||
self._etu_confirm_count = 0
|
self._etu_confirm_count = 0
|
||||||
self._etu_fail_count = 0
|
self._etu_fail_count = 0
|
||||||
self.clock_skip = 372
|
self.clock_skip = 372
|
||||||
@@ -2364,7 +2485,9 @@ class Decoder(srd.Decoder):
|
|||||||
self.read_byte()
|
self.read_byte()
|
||||||
self._resync = True
|
self._resync = True
|
||||||
return True
|
return True
|
||||||
elif (firstByte == 0x3b) and not self._atr_parsed: # Probably ATR (only if no ATR yet)
|
elif ((firstByte in (0x3b, 0x3f)
|
||||||
|
or ts_from_bits(self.bits) is not None)
|
||||||
|
and not self._atr_parsed and not self._apdu_seen): # ATR candidate (direct/inverse), only before traffic
|
||||||
self.handle_atr(self.wait({'skip': 0}))
|
self.handle_atr(self.wait({'skip': 0}))
|
||||||
return True
|
return True
|
||||||
elif is_idle_noise_byte(firstByte):
|
elif is_idle_noise_byte(firstByte):
|
||||||
@@ -2746,6 +2869,9 @@ class Decoder(srd.Decoder):
|
|||||||
|
|
||||||
|
|
||||||
if (len(packet) > 0):
|
if (len(packet) > 0):
|
||||||
|
# APDU traffic has started: a later DATA-state 0x3B is
|
||||||
|
# data, not an ATR.
|
||||||
|
self._apdu_seen = True
|
||||||
# All-zero first: an I/O-held-low stretch is idle noise,
|
# All-zero first: an I/O-held-low stretch is idle noise,
|
||||||
# not a TPDU -- suppress it entirely (and re-sync later).
|
# not a TPDU -- suppress it entirely (and re-sync later).
|
||||||
if (all(b == 0 for b in packet)):
|
if (all(b == 0 for b in packet)):
|
||||||
|
|||||||
+30
-12
@@ -1,7 +1,7 @@
|
|||||||
=== Baseline captured 2026-10-20T16:56:58+03:00 ===
|
=== Baseline captured 2026-10-20T17:56:26+03:00 ===
|
||||||
|
|
||||||
=== test_8 ===
|
=== test_8 ===
|
||||||
('=== iso7816 decoder v1.9.5 ===',)
|
('=== iso7816 decoder v1.9.6 ===',)
|
||||||
('etu recovered (samples):', 1237)
|
('etu recovered (samples):', 1237)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -36,7 +36,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== test_7 ===
|
=== test_7 ===
|
||||||
('=== iso7816 decoder v1.9.5 ===',)
|
('=== iso7816 decoder v1.9.6 ===',)
|
||||||
('etu recovered (samples):', 1236)
|
('etu recovered (samples):', 1236)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -77,7 +77,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== xiaomi_phone_sample ===
|
=== xiaomi_phone_sample ===
|
||||||
('=== iso7816 decoder v1.9.5 ===',)
|
('=== iso7816 decoder v1.9.6 ===',)
|
||||||
('etu recovered (samples):', 53)
|
('etu recovered (samples):', 53)
|
||||||
(11674007, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1])
|
(11674007, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1])
|
||||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 1, 1, 1, 1]
|
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 1, 1, 1, 1]
|
||||||
@@ -102,7 +102,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== xiaomi_mi_a1_coldboot ===
|
=== xiaomi_mi_a1_coldboot ===
|
||||||
('=== iso7816 decoder v1.9.5 ===',)
|
('=== iso7816 decoder v1.9.6 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -129,7 +129,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== 4gmodem_coldboot ===
|
=== 4gmodem_coldboot ===
|
||||||
('=== iso7816 decoder v1.9.5 ===',)
|
('=== iso7816 decoder v1.9.6 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -156,7 +156,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== alcor_reader_sample ===
|
=== alcor_reader_sample ===
|
||||||
('=== iso7816 decoder v1.9.5 ===',)
|
('=== iso7816 decoder v1.9.6 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -189,7 +189,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_A55_coldboot_new_cable ===
|
=== samsung_A55_coldboot_new_cable ===
|
||||||
('=== iso7816 decoder v1.9.5 ===',)
|
('=== iso7816 decoder v1.9.6 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -216,7 +216,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_A55_coldboot_new_cable_sim2 ===
|
=== samsung_A55_coldboot_new_cable_sim2 ===
|
||||||
('=== iso7816 decoder v1.9.5 ===',)
|
('=== iso7816 decoder v1.9.6 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -243,7 +243,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_S21p_coldboot_new_cable ===
|
=== samsung_S21p_coldboot_new_cable ===
|
||||||
('=== iso7816 decoder v1.9.5 ===',)
|
('=== iso7816 decoder v1.9.6 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -270,7 +270,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_S21p_coldboot_new_cable_sim2 ===
|
=== samsung_S21p_coldboot_new_cable_sim2 ===
|
||||||
('=== iso7816 decoder v1.9.5 ===',)
|
('=== iso7816 decoder v1.9.6 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -342,25 +342,34 @@ test_explicit_eof_wins (__main__.TestAtEof.test_explicit_eof_wins) ... ok
|
|||||||
test_pending_replay_is_bounded (__main__.TestAtEof.test_pending_replay_is_bounded) ... ok
|
test_pending_replay_is_bounded (__main__.TestAtEof.test_pending_replay_is_bounded) ... ok
|
||||||
test_pending_replay_is_not_eof (__main__.TestAtEof.test_pending_replay_is_not_eof) ... ok
|
test_pending_replay_is_not_eof (__main__.TestAtEof.test_pending_replay_is_not_eof) ... ok
|
||||||
test_stuck_with_nothing_queued_is_eof (__main__.TestAtEof.test_stuck_with_nothing_queued_is_eof) ... ok
|
test_stuck_with_nothing_queued_is_eof (__main__.TestAtEof.test_stuck_with_nothing_queued_is_eof) ... ok
|
||||||
|
test_atr_with_parity_error_is_ignored (__main__.TestAtrHuntDetect.test_atr_with_parity_error_is_ignored) ... ok
|
||||||
|
test_bare_ts_t0_is_not_committed_as_an_atr (__main__.TestAtrHuntDetect.test_bare_ts_t0_is_not_committed_as_an_atr) ... ok
|
||||||
test_eof_after_plausible_cla_is_clean (__main__.TestAtrHuntDetect.test_eof_after_plausible_cla_is_clean) ... ok
|
test_eof_after_plausible_cla_is_clean (__main__.TestAtrHuntDetect.test_eof_after_plausible_cla_is_clean) ... ok
|
||||||
test_handoff_on_extended_channel_cla (__main__.TestAtrHuntDetect.test_handoff_on_extended_channel_cla) ... ok
|
test_handoff_on_extended_channel_cla (__main__.TestAtrHuntDetect.test_handoff_on_extended_channel_cla) ... ok
|
||||||
test_handoff_on_odd_ins_89 (__main__.TestAtrHuntDetect.test_handoff_on_odd_ins_89) ... ok
|
test_handoff_on_odd_ins_89 (__main__.TestAtrHuntDetect.test_handoff_on_odd_ins_89) ... ok
|
||||||
|
test_inverse_ts_pattern_is_parsed (__main__.TestAtrHuntDetect.test_inverse_ts_pattern_is_parsed) ... ok
|
||||||
test_no_handoff_on_basic_channel_cla (__main__.TestAtrHuntDetect.test_no_handoff_on_basic_channel_cla) ... ok
|
test_no_handoff_on_basic_channel_cla (__main__.TestAtrHuntDetect.test_no_handoff_on_basic_channel_cla) ... ok
|
||||||
test_no_handoff_on_invalid_ins (__main__.TestAtrHuntDetect.test_no_handoff_on_invalid_ins) ... ok
|
test_no_handoff_on_invalid_ins (__main__.TestAtrHuntDetect.test_no_handoff_on_invalid_ins) ... ok
|
||||||
test_all_ff_idle_loop_is_bounded (__main__.TestAtrHuntEscape.test_all_ff_idle_loop_is_bounded) ... ok
|
test_all_ff_idle_loop_is_bounded (__main__.TestAtrHuntEscape.test_all_ff_idle_loop_is_bounded) ... ok
|
||||||
test_idle_ff_retries_before_cap (__main__.TestAtrHuntEscape.test_idle_ff_retries_before_cap) ... ok
|
test_idle_ff_retries_before_cap (__main__.TestAtrHuntEscape.test_idle_ff_retries_before_cap) ... ok
|
||||||
test_idle_resets_capped_then_resume (__main__.TestAtrHuntEscape.test_idle_resets_capped_then_resume) ... ok
|
test_idle_resets_capped_then_resume (__main__.TestAtrHuntEscape.test_idle_resets_capped_then_resume) ... ok
|
||||||
|
test_measured_etu_skips_recovery (__main__.TestAtrHuntEscape.test_measured_etu_skips_recovery) ... ok
|
||||||
test_no_recovery_when_etu_known (__main__.TestAtrHuntEscape.test_no_recovery_when_etu_known) ... ok
|
test_no_recovery_when_etu_known (__main__.TestAtrHuntEscape.test_no_recovery_when_etu_known) ... ok
|
||||||
test_non_ff_resumes_when_count_high (__main__.TestAtrHuntEscape.test_non_ff_resumes_when_count_high) ... ok
|
test_non_ff_resumes_when_count_high (__main__.TestAtrHuntEscape.test_non_ff_resumes_when_count_high) ... ok
|
||||||
test_recover_etu_in_hunt_runs_measurement (__main__.TestAtrHuntEscape.test_recover_etu_in_hunt_runs_measurement) ... ok
|
test_recover_etu_in_hunt_runs_measurement (__main__.TestAtrHuntEscape.test_recover_etu_in_hunt_runs_measurement) ... ok
|
||||||
test_recovery_attempts_capped_then_hard_limit (__main__.TestAtrHuntEscape.test_recovery_attempts_capped_then_hard_limit) ... ok
|
test_recovery_attempts_capped_then_hard_limit (__main__.TestAtrHuntEscape.test_recovery_attempts_capped_then_hard_limit) ... ok
|
||||||
test_retry_then_recover_after_two_failures (__main__.TestAtrHuntEscape.test_retry_then_recover_after_two_failures) ... ok
|
test_retry_then_recover_after_two_failures (__main__.TestAtrHuntEscape.test_retry_then_recover_after_two_failures) ... ok
|
||||||
|
test_seeded_bit_samples_does_not_block_recovery (__main__.TestAtrHuntEscape.test_seeded_bit_samples_does_not_block_recovery) ... ok
|
||||||
test_bit_dense_valid_cla_is_not_idle_noise (__main__.TestDecodeStep.test_bit_dense_valid_cla_is_not_idle_noise) ... ok
|
test_bit_dense_valid_cla_is_not_idle_noise (__main__.TestDecodeStep.test_bit_dense_valid_cla_is_not_idle_noise) ... ok
|
||||||
|
test_data_0x3b_before_traffic_is_atr_candidate (__main__.TestDecodeStep.test_data_0x3b_before_traffic_is_atr_candidate) ... ok
|
||||||
|
test_data_0x3b_is_not_an_atr_after_traffic (__main__.TestDecodeStep.test_data_0x3b_is_not_an_atr_after_traffic) ... ok
|
||||||
|
test_data_0x3f_before_traffic_is_atr_candidate (__main__.TestDecodeStep.test_data_0x3f_before_traffic_is_atr_candidate) ... ok
|
||||||
test_ex_class_exchange_frames (__main__.TestDecodeStep.test_ex_class_exchange_frames) ... ok
|
test_ex_class_exchange_frames (__main__.TestDecodeStep.test_ex_class_exchange_frames) ... ok
|
||||||
test_extended_channel_exchange_frames (__main__.TestDecodeStep.test_extended_channel_exchange_frames) ... ok
|
test_extended_channel_exchange_frames (__main__.TestDecodeStep.test_extended_channel_exchange_frames) ... ok
|
||||||
test_idle_noise_still_discarded (__main__.TestDecodeStep.test_idle_noise_still_discarded) ... ok
|
test_idle_noise_still_discarded (__main__.TestDecodeStep.test_idle_noise_still_discarded) ... ok
|
||||||
test_immediate_sw_rejection_frames (__main__.TestDecodeStep.test_immediate_sw_rejection_frames) ... ok
|
test_immediate_sw_rejection_frames (__main__.TestDecodeStep.test_immediate_sw_rejection_frames) ... ok
|
||||||
test_invalid_ins_re_frame_recovers_command (__main__.TestDecodeStep.test_invalid_ins_re_frame_recovers_command) ... ok
|
test_invalid_ins_re_frame_recovers_command (__main__.TestDecodeStep.test_invalid_ins_re_frame_recovers_command) ... ok
|
||||||
|
test_inverse_ts_raw_pattern_is_an_atr_candidate (__main__.TestDecodeStep.test_inverse_ts_raw_pattern_is_an_atr_candidate) ... ok
|
||||||
test_le_256_response_data_is_kept (__main__.TestDecodeStep.test_le_256_response_data_is_kept) ... ok
|
test_le_256_response_data_is_kept (__main__.TestDecodeStep.test_le_256_response_data_is_kept) ... ok
|
||||||
test_short_response_is_split_and_flagged (__main__.TestDecodeStep.test_short_response_is_split_and_flagged) ... ok
|
test_short_response_is_split_and_flagged (__main__.TestDecodeStep.test_short_response_is_split_and_flagged) ... ok
|
||||||
test_short_response_replays_following_exchange (__main__.TestDecodeStep.test_short_response_replays_following_exchange) ... ok
|
test_short_response_replays_following_exchange (__main__.TestDecodeStep.test_short_response_replays_following_exchange) ... ok
|
||||||
@@ -380,6 +389,7 @@ test_is_desynced_short_fragment (__main__.TestDesyncHelpers.test_is_desynced_sho
|
|||||||
test_max_tpdu_len (__main__.TestDesyncHelpers.test_max_tpdu_len) ... ok
|
test_max_tpdu_len (__main__.TestDesyncHelpers.test_max_tpdu_len) ... ok
|
||||||
test_plausible_sw_accepts_valid_classes (__main__.TestDesyncHelpers.test_plausible_sw_accepts_valid_classes) ... ok
|
test_plausible_sw_accepts_valid_classes (__main__.TestDesyncHelpers.test_plausible_sw_accepts_valid_classes) ... ok
|
||||||
test_plausible_sw_rejects_garbage (__main__.TestDesyncHelpers.test_plausible_sw_rejects_garbage) ... ok
|
test_plausible_sw_rejects_garbage (__main__.TestDesyncHelpers.test_plausible_sw_rejects_garbage) ... ok
|
||||||
|
test_resync_idle_waits_on_a_level_condition (__main__.TestDesyncHelpers.test_resync_idle_waits_on_a_level_condition) ... ok
|
||||||
test_a55_u1_concat (__main__.TestEmbeddedExchanges.test_a55_u1_concat) ... ok
|
test_a55_u1_concat (__main__.TestEmbeddedExchanges.test_a55_u1_concat) ... ok
|
||||||
test_a55_u4_mega_concat (__main__.TestEmbeddedExchanges.test_a55_u4_mega_concat) ... ok
|
test_a55_u4_mega_concat (__main__.TestEmbeddedExchanges.test_a55_u4_mega_concat) ... ok
|
||||||
test_channel1_select_after_manage_channel_swallow (__main__.TestEmbeddedExchanges.test_channel1_select_after_manage_channel_swallow) ... ok
|
test_channel1_select_after_manage_channel_swallow (__main__.TestEmbeddedExchanges.test_channel1_select_after_manage_channel_swallow) ... ok
|
||||||
@@ -396,11 +406,19 @@ test_samsung_record_burst_concat (__main__.TestEmbeddedExchanges.test_samsung_re
|
|||||||
test_xiaomi_select_getresp_swallow (__main__.TestEmbeddedExchanges.test_xiaomi_select_getresp_swallow) ... ok
|
test_xiaomi_select_getresp_swallow (__main__.TestEmbeddedExchanges.test_xiaomi_select_getresp_swallow) ... ok
|
||||||
test_eoferror_propagates (__main__.TestEndOfStream.test_eoferror_propagates) ... ok
|
test_eoferror_propagates (__main__.TestEndOfStream.test_eoferror_propagates) ... ok
|
||||||
test_systemerror_propagates (__main__.TestEndOfStream.test_systemerror_propagates) ... ok
|
test_systemerror_propagates (__main__.TestEndOfStream.test_systemerror_propagates) ... ok
|
||||||
|
test_ff_burst_is_accepted (__main__.TestEtuCandidate.test_ff_burst_is_accepted) ... ok
|
||||||
|
test_normal_burst_is_accepted (__main__.TestEtuCandidate.test_normal_burst_is_accepted) ... ok
|
||||||
|
test_single_spacing_is_rejected (__main__.TestEtuCandidate.test_single_spacing_is_rejected) ... ok
|
||||||
|
test_zero_run_guard_is_rejected (__main__.TestEtuCandidate.test_zero_run_guard_is_rejected) ... ok
|
||||||
test_build_packet_bad_fcs_flag (__main__.TestGsmtapFlags.test_build_packet_bad_fcs_flag) ... ok
|
test_build_packet_bad_fcs_flag (__main__.TestGsmtapFlags.test_build_packet_bad_fcs_flag) ... ok
|
||||||
test_build_packet_payload_passthrough (__main__.TestGsmtapFlags.test_build_packet_payload_passthrough) ... ok
|
test_build_packet_payload_passthrough (__main__.TestGsmtapFlags.test_build_packet_payload_passthrough) ... ok
|
||||||
test_build_packet_plain_res_zero (__main__.TestGsmtapFlags.test_build_packet_plain_res_zero) ... ok
|
test_build_packet_plain_res_zero (__main__.TestGsmtapFlags.test_build_packet_plain_res_zero) ... ok
|
||||||
test_flag_constant_value (__main__.TestGsmtapFlags.test_flag_constant_value) ... ok
|
test_flag_constant_value (__main__.TestGsmtapFlags.test_flag_constant_value) ... ok
|
||||||
test_pps_subtypes_are_standard (__main__.TestGsmtapFlags.test_pps_subtypes_are_standard) ... ok
|
test_pps_subtypes_are_standard (__main__.TestGsmtapFlags.test_pps_subtypes_are_standard) ... ok
|
||||||
|
test_get_bytes_direct_lsb_first (__main__.TestInverseConvention.test_get_bytes_direct_lsb_first) ... ok
|
||||||
|
test_get_bytes_inverse_msb_first (__main__.TestInverseConvention.test_get_bytes_inverse_msb_first) ... ok
|
||||||
|
test_ts_from_bits_direct_inverse_and_garbage (__main__.TestInverseConvention.test_ts_from_bits_direct_inverse_and_garbage) ... ok
|
||||||
|
test_wait_data_falling_uses_high_idle_and_falling_start (__main__.TestInverseConvention.test_wait_data_falling_uses_high_idle_and_falling_start) ... ok
|
||||||
test_base_payload_without_rate (__main__.TestLineEventPayload.test_base_payload_without_rate) ... ok
|
test_base_payload_without_rate (__main__.TestLineEventPayload.test_base_payload_without_rate) ... ok
|
||||||
test_flag_constant (__main__.TestLineEventPayload.test_flag_constant) ... ok
|
test_flag_constant (__main__.TestLineEventPayload.test_flag_constant) ... ok
|
||||||
test_legacy_prefix_preserved (__main__.TestLineEventPayload.test_legacy_prefix_preserved) ... ok
|
test_legacy_prefix_preserved (__main__.TestLineEventPayload.test_legacy_prefix_preserved) ... ok
|
||||||
@@ -439,6 +457,6 @@ test_valid_ins_odd_variants (__main__.TestValidateT0Apdu.test_valid_ins_odd_vari
|
|||||||
test_vs_reader_mirror (__main__.TestValidateT0Apdu.test_vs_reader_mirror) ... ok
|
test_vs_reader_mirror (__main__.TestValidateT0Apdu.test_vs_reader_mirror) ... ok
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
Ran 99 tests in 0.032s
|
Ran 117 tests in 0.026s
|
||||||
|
|
||||||
OK
|
OK
|
||||||
@@ -151,6 +151,139 @@ class TestDesyncHelpers(unittest.TestCase):
|
|||||||
self.assertFalse(self.pd.is_desynced(b'\x00' * 7))
|
self.assertFalse(self.pd.is_desynced(b'\x00' * 7))
|
||||||
|
|
||||||
|
|
||||||
|
def test_resync_idle_waits_on_a_level_condition(self):
|
||||||
|
# The re-align must WAIT for DATA (a level condition that advances
|
||||||
|
# the sample counter). It used to call wait({'skip': 0}) in a loop,
|
||||||
|
# which never advanced -- a no-op repeated on the same sample.
|
||||||
|
inst = object.__new__(self.pd.Decoder)
|
||||||
|
inst.reset()
|
||||||
|
inst.bit_samples = 100
|
||||||
|
inst._inverse_convention = False # normally set by start()
|
||||||
|
inst._start_fall = None
|
||||||
|
inst.samplenum = 0
|
||||||
|
levels = [0, 0, 1]
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def wait(cond):
|
||||||
|
calls.append(cond)
|
||||||
|
want = None
|
||||||
|
if isinstance(cond, list):
|
||||||
|
for c in cond:
|
||||||
|
if self.pd.Decoder.DATA_IDX in c:
|
||||||
|
want = c[self.pd.Decoder.DATA_IDX]
|
||||||
|
while levels:
|
||||||
|
inst.samplenum += 10
|
||||||
|
lvl = levels.pop(0)
|
||||||
|
if (want == 'h' and lvl == 1) or (want == 'l' and lvl == 0):
|
||||||
|
return [lvl] * 8
|
||||||
|
inst.samplenum += 10
|
||||||
|
return [1] * 8
|
||||||
|
|
||||||
|
inst.wait = wait
|
||||||
|
inst.resync_idle()
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
self.assertIsInstance(calls[0], list)
|
||||||
|
self.assertIn(self.pd.Decoder.DATA_IDX, calls[0][0])
|
||||||
|
# it advanced through the two LOW samples to the HIGH one
|
||||||
|
self.assertEqual(inst.samplenum, 30)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEtuCandidate(unittest.TestCase):
|
||||||
|
"""The mid-session ETU estimator's burst validation (_etu_candidate)."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.inst = object.__new__(self.pd.Decoder)
|
||||||
|
|
||||||
|
def test_zero_run_guard_is_rejected(self):
|
||||||
|
# All-zero characters: 10-ETU LOW runs + a short HIGH inter-byte
|
||||||
|
# guard. The smallest recurring spacing is the guard, not the ETU.
|
||||||
|
pairs = [(1000, 0), (200, 1)] * 8
|
||||||
|
etu, ok = self.inst._etu_candidate(pairs)
|
||||||
|
self.assertEqual(etu, 200)
|
||||||
|
self.assertFalse(ok)
|
||||||
|
|
||||||
|
def test_single_spacing_is_rejected(self):
|
||||||
|
# A lone 0x00 character yields one 10-ETU run (10x the true ETU).
|
||||||
|
self.assertEqual(self.inst._etu_candidate([(1000, 0)]),
|
||||||
|
(None, False))
|
||||||
|
self.assertEqual(self.inst._etu_candidate([(8, 0)]),
|
||||||
|
(None, False))
|
||||||
|
|
||||||
|
def test_normal_burst_is_accepted(self):
|
||||||
|
# A 1-ETU LOW spacing (start bit / data run) alongside 2- and
|
||||||
|
# 3-ETU runs: the recurring minimum is the true ETU.
|
||||||
|
pairs = [(100, 0), (100, 1), (200, 0), (300, 1),
|
||||||
|
(100, 0), (100, 1), (200, 0)]
|
||||||
|
etu, ok = self.inst._etu_candidate(pairs)
|
||||||
|
self.assertEqual(etu, 100)
|
||||||
|
self.assertTrue(ok)
|
||||||
|
|
||||||
|
def test_ff_burst_is_accepted(self):
|
||||||
|
# 0xFF: 1-ETU LOW start pulses + long HIGH runs. The minimum is a
|
||||||
|
# LOW run (the start bit), so the guard heuristic must not fire.
|
||||||
|
pairs = [(100, 0), (900, 1), (100, 0), (900, 1), (100, 0)]
|
||||||
|
etu, ok = self.inst._etu_candidate(pairs)
|
||||||
|
self.assertEqual(etu, 100)
|
||||||
|
self.assertTrue(ok)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInverseConvention(unittest.TestCase):
|
||||||
|
"""ISO 7816-3 7.1: inverse = inverted data, msb first; the start bit
|
||||||
|
stays a falling edge from an idle-HIGH line (the old code assumed an
|
||||||
|
idle-LOW line / rising start)."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.inst = object.__new__(self.pd.Decoder)
|
||||||
|
self.inst.reset()
|
||||||
|
|
||||||
|
def test_get_bytes_direct_lsb_first(self):
|
||||||
|
self.inst._inverse_convention = False
|
||||||
|
self.assertEqual(self.inst.get_bytes([1, 0, 0, 0, 0, 0, 0, 0]), 0x01)
|
||||||
|
self.assertEqual(self.inst.get_bytes([1, 1, 0, 0, 0, 0, 0, 0]), 0x03)
|
||||||
|
|
||||||
|
def test_get_bytes_inverse_msb_first(self):
|
||||||
|
self.inst._inverse_convention = True
|
||||||
|
self.assertEqual(self.inst.get_bytes([1, 0, 0, 0, 0, 0, 0, 0]), 0x80)
|
||||||
|
self.assertEqual(self.inst.get_bytes([1, 1, 0, 0, 0, 0, 0, 0]), 0xC0)
|
||||||
|
|
||||||
|
def test_ts_from_bits_direct_inverse_and_garbage(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.pd.ts_from_bits([0, 1, 1, 0, 1, 1, 1, 0, 0, 1]), 0x3b)
|
||||||
|
self.assertEqual(
|
||||||
|
self.pd.ts_from_bits([0, 1, 1, 0, 0, 0, 0, 0, 0, 1]), 0x3f)
|
||||||
|
self.assertIsNone(
|
||||||
|
self.pd.ts_from_bits([0, 1, 1, 0, 1, 0, 1, 0, 0, 1]))
|
||||||
|
self.assertIsNone(self.pd.ts_from_bits([0, 1, 1, 0]))
|
||||||
|
|
||||||
|
def test_wait_data_falling_uses_high_idle_and_falling_start(self):
|
||||||
|
self.inst._inverse_convention = True
|
||||||
|
self.inst.bit_samples = 100
|
||||||
|
self.inst._edge_read = True
|
||||||
|
self.inst._min_pulse_samples = 0
|
||||||
|
self.inst.samplenum = 0
|
||||||
|
conds = []
|
||||||
|
|
||||||
|
def wait(cond):
|
||||||
|
conds.append(cond)
|
||||||
|
self.inst.samplenum += 200
|
||||||
|
return [1] * 8 if len(conds) == 1 else [0] * 8
|
||||||
|
|
||||||
|
self.inst.wait = wait
|
||||||
|
self.inst.wait_data_falling()
|
||||||
|
self.assertFalse(self.inst._eof)
|
||||||
|
self.assertEqual(conds[0][0][self.pd.Decoder.DATA_IDX], 'h')
|
||||||
|
self.assertEqual(conds[1][0][self.pd.Decoder.DATA_IDX], 'f')
|
||||||
|
self.assertEqual(self.inst._start_fall, self.inst.samplenum)
|
||||||
|
|
||||||
|
|
||||||
class TestValidateT0Apdu(unittest.TestCase):
|
class TestValidateT0Apdu(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
@@ -393,6 +526,7 @@ class TestAtrHuntEscape(unittest.TestCase):
|
|||||||
self.pd = pd
|
self.pd = pd
|
||||||
self.inst = object.__new__(pd.Decoder)
|
self.inst = object.__new__(pd.Decoder)
|
||||||
self.inst.bit_samples = None
|
self.inst.bit_samples = None
|
||||||
|
self.inst._etu_measured = False
|
||||||
self.inst._atr_hunt_count = 0
|
self.inst._atr_hunt_count = 0
|
||||||
self.inst._atr_fail_total = 0
|
self.inst._atr_fail_total = 0
|
||||||
self.inst._hunt_etu_attempts = 0
|
self.inst._hunt_etu_attempts = 0
|
||||||
@@ -414,6 +548,22 @@ class TestAtrHuntEscape(unittest.TestCase):
|
|||||||
self.assertEqual(self._hunt_round(0x00), 'recover')
|
self.assertEqual(self._hunt_round(0x00), 'recover')
|
||||||
self.assertEqual(self.inst._hunt_etu_attempts, 1)
|
self.assertEqual(self.inst._hunt_etu_attempts, 1)
|
||||||
|
|
||||||
|
def test_seeded_bit_samples_does_not_block_recovery(self):
|
||||||
|
# handle_atr pre-seeds bit_samples with the default-rate guess; the
|
||||||
|
# recovery must key off a real DATA measurement, not on bit_samples
|
||||||
|
# being None (the pre-seed used to defeat the v1.6.0 recovery).
|
||||||
|
self.inst.bit_samples = 1488
|
||||||
|
self.inst._etu_measured = False
|
||||||
|
self.assertEqual(self._hunt_round(0x00), 'retry')
|
||||||
|
self.assertEqual(self._hunt_round(0x00), 'recover')
|
||||||
|
|
||||||
|
def test_measured_etu_skips_recovery(self):
|
||||||
|
# A real measurement means the rate is known: no re-measurement.
|
||||||
|
self.inst.bit_samples = 1488
|
||||||
|
self.inst._etu_measured = True
|
||||||
|
actions = [self._hunt_round(0x00) for _ in range(6)]
|
||||||
|
self.assertNotIn('recover', actions)
|
||||||
|
|
||||||
def test_recovery_attempts_capped_then_hard_limit(self):
|
def test_recovery_attempts_capped_then_hard_limit(self):
|
||||||
actions = [self._hunt_round(0x00) for _ in range(200)]
|
actions = [self._hunt_round(0x00) for _ in range(200)]
|
||||||
actions = actions[:actions.index('resume_data') + 1]
|
actions = actions[:actions.index('resume_data') + 1]
|
||||||
@@ -452,6 +602,7 @@ class TestAtrHuntEscape(unittest.TestCase):
|
|||||||
|
|
||||||
def test_no_recovery_when_etu_known(self):
|
def test_no_recovery_when_etu_known(self):
|
||||||
self.inst.bit_samples = 100
|
self.inst.bit_samples = 100
|
||||||
|
self.inst._etu_measured = True
|
||||||
for _ in range(6):
|
for _ in range(6):
|
||||||
self.assertEqual(self._hunt_round(0x00), 'retry')
|
self.assertEqual(self._hunt_round(0x00), 'retry')
|
||||||
self.assertEqual(self.inst._hunt_etu_attempts, 0)
|
self.assertEqual(self.inst._hunt_etu_attempts, 0)
|
||||||
@@ -870,6 +1021,53 @@ class TestDecodeStep(unittest.TestCase):
|
|||||||
self.assertEqual(inst.emitted,
|
self.assertEqual(inst.emitted,
|
||||||
[(bytes.fromhex('00a40004009000'), 0)])
|
[(bytes.fromhex('00a40004009000'), 0)])
|
||||||
|
|
||||||
|
def test_data_0x3b_is_not_an_atr_after_traffic(self):
|
||||||
|
# Once an APDU has been emitted, a DATA-state 0x3B is data misread
|
||||||
|
# at a frame boundary, not an ATR: it falls through to the
|
||||||
|
# suspicious-CLA discard instead of calling handle_atr.
|
||||||
|
inst = self._make(bytes.fromhex('3b80900000'))
|
||||||
|
inst._atr_parsed = False
|
||||||
|
inst._apdu_seen = True
|
||||||
|
called = []
|
||||||
|
inst.handle_atr = lambda pins: called.append(pins)
|
||||||
|
inst.decode_step()
|
||||||
|
self.assertEqual(called, [])
|
||||||
|
self.assertEqual(len(inst.discarded), 1)
|
||||||
|
self.assertEqual(inst.discarded[0][0], 0x3B)
|
||||||
|
|
||||||
|
def test_data_0x3b_before_traffic_is_atr_candidate(self):
|
||||||
|
# Before any APDU the byte is still a legitimate ATR candidate
|
||||||
|
# (a capture starting between RST deassert and the ATR).
|
||||||
|
inst = self._make(bytes.fromhex('3b80900000'))
|
||||||
|
inst._atr_parsed = False
|
||||||
|
inst._apdu_seen = False
|
||||||
|
called = []
|
||||||
|
inst.handle_atr = lambda pins: called.append(pins)
|
||||||
|
inst.decode_step()
|
||||||
|
self.assertEqual(len(called), 1)
|
||||||
|
|
||||||
|
def test_data_0x3f_before_traffic_is_atr_candidate(self):
|
||||||
|
# Inverse-convention ATRs (TS=0x3F) are candidates too.
|
||||||
|
inst = self._make(bytes.fromhex('3f100000'))
|
||||||
|
inst._atr_parsed = False
|
||||||
|
inst._apdu_seen = False
|
||||||
|
called = []
|
||||||
|
inst.handle_atr = lambda pins: called.append(pins)
|
||||||
|
inst.decode_step()
|
||||||
|
self.assertEqual(len(called), 1)
|
||||||
|
|
||||||
|
def test_inverse_ts_raw_pattern_is_an_atr_candidate(self):
|
||||||
|
# A real inverse TS decodes as 0x03 through the direct reader; the
|
||||||
|
# raw moments 5-7 (LLL) mark it as the 0x3F candidate.
|
||||||
|
inst = self._make(bytes.fromhex('03100000'))
|
||||||
|
inst._atr_parsed = False
|
||||||
|
inst._apdu_seen = False
|
||||||
|
inst.bits = [0, 1, 1, 0, 0, 0, 0, 0, 0, 1]
|
||||||
|
called = []
|
||||||
|
inst.handle_atr = lambda pins: called.append(pins)
|
||||||
|
inst.decode_step()
|
||||||
|
self.assertEqual(len(called), 1)
|
||||||
|
|
||||||
def test_idle_noise_still_discarded(self):
|
def test_idle_noise_still_discarded(self):
|
||||||
# 0xFE (all-1s minus one bit) is not a valid class: unchanged
|
# 0xFE (all-1s minus one bit) is not a valid class: unchanged
|
||||||
# idle-noise discard.
|
# idle-noise discard.
|
||||||
@@ -944,6 +1142,12 @@ class TestAtrHuntDetect(unittest.TestCase):
|
|||||||
inst._samples_per_clock = 4.0 # skip _measure_clock_period()
|
inst._samples_per_clock = 4.0 # skip _measure_clock_period()
|
||||||
inst.bit_samples = 100 # skip _compute_bit_samples()
|
inst.bit_samples = 100 # skip _compute_bit_samples()
|
||||||
inst.out_ann = 0
|
inst.out_ann = 0
|
||||||
|
inst.out_python = 0
|
||||||
|
inst.options = {'protocol': 'T=0'}
|
||||||
|
inst._atr_parsed = False
|
||||||
|
inst.emitted = []
|
||||||
|
inst.emit_packet = lambda st, pl, ss, es, flags=0: \
|
||||||
|
inst.emitted.append((st, bytes(pl), flags))
|
||||||
inst.hasT0 = False # start() defaults; a handoff flips it
|
inst.hasT0 = False # start() defaults; a handoff flips it
|
||||||
inst.hasT1 = False
|
inst.hasT1 = False
|
||||||
inst._bytes = collections.deque(byte_seq)
|
inst._bytes = collections.deque(byte_seq)
|
||||||
@@ -1008,6 +1212,46 @@ class TestAtrHuntDetect(unittest.TestCase):
|
|||||||
self.assertEqual(inst.state, 'ATR')
|
self.assertEqual(inst.state, 'ATR')
|
||||||
self.assertEqual(list(inst._replay), [])
|
self.assertEqual(list(inst._replay), [])
|
||||||
|
|
||||||
|
def test_bare_ts_t0_is_not_committed_as_an_atr(self):
|
||||||
|
# A data byte 0x3B followed by 0x00 looks like a bare TS+T0 ATR.
|
||||||
|
# It must not be emitted or force clock_skip=372 (a mid-session
|
||||||
|
# data 0x3B used to fabricate an ATR).
|
||||||
|
inst = self._make([0x3B, 0x00])
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertFalse(inst._atr_parsed)
|
||||||
|
self.assertEqual(inst.emitted, [])
|
||||||
|
self.assertEqual(inst.state, 'DATA')
|
||||||
|
|
||||||
|
def test_inverse_ts_pattern_is_parsed(self):
|
||||||
|
# 0x03 + the raw inverse TS pattern must be committed as 0x3F and
|
||||||
|
# switch the readers to the inverse convention.
|
||||||
|
inst = self._make([0x03, 0x10, 0x95])
|
||||||
|
inst.bits = [0, 1, 1, 0, 0, 0, 0, 0, 0, 1]
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertTrue(inst._atr_parsed)
|
||||||
|
self.assertTrue(inst._inverse_convention)
|
||||||
|
self.assertEqual(inst.ATR, [0x3f, 0x10, 0x95])
|
||||||
|
self.assertEqual(len(inst.emitted), 1)
|
||||||
|
|
||||||
|
def test_atr_with_parity_error_is_ignored(self):
|
||||||
|
# A real ATR is parity-clean. A data run that parses as an ATR but
|
||||||
|
# has a parity error must not commit the rate (e.g. test_8's
|
||||||
|
# fabricated 3b80900000).
|
||||||
|
inst = self._make([0x3B, 0x10, 0x95])
|
||||||
|
orig = inst.read_byte
|
||||||
|
|
||||||
|
def read():
|
||||||
|
b = orig()
|
||||||
|
if b == 0x10:
|
||||||
|
inst._parity_errors += 1
|
||||||
|
return b
|
||||||
|
|
||||||
|
inst.read_byte = read
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertFalse(inst._atr_parsed)
|
||||||
|
self.assertEqual(inst.emitted, [])
|
||||||
|
self.assertEqual(inst.state, 'DATA')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user