- Split the PPS exchange into the two standard GSMTAP-SIM subtypes: 0x02 (PPS_REQ, raw request bytes, span ss..es_req) and 0x03 (PPS_RSP, raw response bytes, span es_req..end), replacing the single 0x02 packet with concatenated request+response that deviated from libosmocore's gsmtap.h. Wireshark now dissects both halves; simtrace2-pysniff needs no change (its importer already maps 0x03 to 'pps' and _decode_pps() parses the 0xFF-leading response frame). PPS annotation/log unchanged. - gsmtap_stream.py: GSMTAP_SIM_PPS_REQ/RSP constants; header field labels aligned with libosmocore's struct (signal_dbm/snr_db). - Docs: GSMTAP event tables (EN/RU) updated, PPS deviation note removed; corrected the res-byte flag attribution -- upstream simtrace2 never sets res, so the GSMTAP_FLAG_BAD_FCS carriage is this decoder's own extension (semantics mirror simtrace2's USB data flags). - Tests: assert the PPS subtype values; baseline regenerated (only the version line and test listing differed -- no decode-metric changes). 68/68 unit tests.
2627 lines
123 KiB
Python
2627 lines
123 KiB
Python
##
|
||
## Copyright (C) 2020 Sven Soltermann <sven@handyman.ch>
|
||
##
|
||
## This program is free software; you can redistribute it and/or modify
|
||
## it under the terms of the GNU General Public License as published by
|
||
## the Free Software Foundation; either version 2 of the License, or
|
||
## (at your option) any later version.
|
||
##
|
||
## This program is distributed in the hope that it will be useful,
|
||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
## GNU General Public License for more details.
|
||
##
|
||
## You should have received a copy of the GNU General Public License
|
||
## along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||
##
|
||
|
||
import sigrokdecode as srd
|
||
import struct
|
||
import sys
|
||
import time
|
||
import codecs
|
||
import traceback
|
||
from collections import deque
|
||
|
||
from .gsmtap_stream import (GsmtapStreamSender,
|
||
GSMTAP_SIM_APDU, GSMTAP_SIM_ATR, GSMTAP_SIM_PPS_REQ, GSMTAP_SIM_PPS_RSP,
|
||
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
|
||
GSMTAP_FLAG_BAD_FCS)
|
||
|
||
VERSION = '1.8.0'
|
||
|
||
|
||
|
||
class Ann:
|
||
'''Annotation and binary output classes.'''
|
||
(
|
||
ANN_WARN, ANN_BYTE, ANN_ATR, ANN_PPS, ANN_T0, ANN_T1, ANN_T1IBLOCK, ANN_T1RBLOCK, ANN_T1SBLOCK, ANN_APDU,
|
||
ANN_RST, ANN_VCC,
|
||
) = range(12)
|
||
|
||
|
||
class pcap_udp_pkt():
|
||
# GSMTAP-SIM wrapped in a synthetic Ethernet/IP/UDP header so the binary
|
||
# output dissects as a UDP stream in Wireshark. The IPv4 header checksum
|
||
# is left 0 and the endpoints are fixed loopback + the default GSMTAP
|
||
# ports on purpose: Wireshark tolerates the zero checksum and the values
|
||
# are only cosmetic. Field bytes are patched in set_data() below.
|
||
# GSM TAP
|
||
h = b''
|
||
|
||
# layer_2
|
||
h += b'\x00\x00\x00\x00\x00\x00' # destination_mac
|
||
h += b'\x00\x00\x00\x00\x00\x00' # source_mac
|
||
h += b'\x08\x00' # layer_3_protocol
|
||
|
||
# layer_3
|
||
h += b'\x45' # version
|
||
h += b'\x00' # DiffServField
|
||
h += b'\xFF\xFF' # total_length
|
||
h += b'\x2B\x0D' # Identification
|
||
h += b'\x40\x00' # Flags
|
||
h += b'\x40' # TTL
|
||
h += b'\x11' # layer_4_protocol
|
||
h += b'\x00\x00' # header_checksum
|
||
h += b'\x7F\x00\x00\x01' # source_ip
|
||
h += b'\x7F\x00\x00\x01' # dest_ip
|
||
|
||
# layer_4
|
||
h += b'\xcc\x46' # source_port
|
||
h += b'\x12\x79' # dest_port
|
||
h += b'\x00\x00' # datagram_length
|
||
h += b'\x00\x00' # checksum
|
||
|
||
def __init__(self, ts, data, sub_type=GSMTAP_SIM_APDU, flags=0):
|
||
self.header = bytearray(pcap_udp_pkt.h)
|
||
self.data = b''
|
||
self.set_timestamp(ts)
|
||
self.set_data(data, sub_type, flags)
|
||
|
||
def set_timestamp(self, ts):
|
||
self.timestamp = ts
|
||
|
||
def set_data(self, data, sub_type=GSMTAP_SIM_APDU, flags=0):
|
||
gsmtap = bytearray(b'\x02\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
|
||
gsmtap[12] = sub_type
|
||
gsmtap[15] = flags
|
||
self.data = list(gsmtap + bytes(data));
|
||
self.header[16:18] = struct.pack('>H', len(self.data) + 28)
|
||
self.header[38:40] = struct.pack('>H', len(self.data) + 8)
|
||
|
||
def packet(self):
|
||
return bytes(self.header) + bytes(self.data)
|
||
|
||
def record_header(self):
|
||
# See https://wiki.wireshark.org/Development/LibpcapFileFormat.
|
||
(secs, usecs) = self.timestamp
|
||
h = struct.pack('<I', secs) # TS seconds
|
||
h += struct.pack('<I', usecs) # TS microseconds
|
||
# No truncation, so both lengths are the same.
|
||
h += struct.pack('<I', len(self)) # Captured len
|
||
h += struct.pack('<I', len(self)) # Original len
|
||
return h
|
||
|
||
def __len__(self):
|
||
return len(self.h) + len(self.data)
|
||
|
||
def plausible_sw(sw1):
|
||
'''T=0 status word first byte sanity check. SW1 is 0x9x (normal
|
||
processing) or 0x6x (warning/error classes). Anything else means
|
||
the byte stream was almost certainly mis-framed.'''
|
||
return (0x60 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
||
|
||
|
||
def plausible_cla(cla):
|
||
'''Check if CLA byte is a structurally valid ISO 7816 class.
|
||
|
||
Per ISO 7816-4, the CLA byte is laid out as:
|
||
|
||
b8 b7 b6 : interindustry / proprietary class
|
||
bits 4-3 : secure messaging indicator
|
||
bits 2-1 : logical channel number (0-3 for interindustry)
|
||
|
||
Interindustry CLA high nibble is 0x0/0x1/0x4/0x5 (i.e. CLA & 0xF0 in
|
||
(0x00, 0x10, 0x40, 0x50)); this covers 0x00 (basic), 0x01-0x03
|
||
(logical channels 1-3, e.g. the command that follows a MANAGE CHANNEL
|
||
that opened channel 1), 0x0C (secure messaging), chaining variants,
|
||
and the structural/SM classes 0x4x/0x5x.
|
||
|
||
b8 = 1 marks proprietary structural formats; only the classes actually
|
||
used by SIM/UICC CAT stacks (3GPP TS 102 221 / TS 51.011) are accepted.
|
||
|
||
Anything else means the decoder likely mis-framed the byte stream and
|
||
should attempt to resync.'''
|
||
if not (cla & 0x80):
|
||
return (cla & 0xF0) in (0x00, 0x10, 0x40, 0x50)
|
||
return cla in (0x80, 0x84, 0xA0, 0xB0)
|
||
|
||
|
||
def plausible_ins(ins):
|
||
'''Check if INS byte is a structurally valid T=0 instruction.
|
||
|
||
Per ISO 7816-4 §12.1.1 / TS 102 221:
|
||
- 0x00 is reserved/invalid.
|
||
- MSB nibble 0x6/0x9 collides with status words.
|
||
- LSB must be 0 (standard interindustry commands).
|
||
|
||
Used together with plausible_cla() to distinguish a real command header
|
||
from noise while hunting for an ATR.'''
|
||
if ins == 0x00:
|
||
return False
|
||
if (ins & 0xF0) == 0x60 or (ins & 0xF0) == 0x90:
|
||
return False
|
||
if (ins & 0x01) != 0:
|
||
return False
|
||
return True
|
||
|
||
|
||
def validate_t0_apdu(packet):
|
||
'''Return (valid, reason) for a reassembled T=0 APDU.
|
||
|
||
Validation is purely structural: we parse the 5-byte header and
|
||
verify that the packet length, CLA, INS and status word are all
|
||
plausible. No content scanning of command/response data is performed,
|
||
so the check has very low false-positive risk. It catches truncation,
|
||
missing status words, and obviously bogus header bytes. Concatenated
|
||
APDUs (several real exchanges swallowed into one "valid" packet) pass
|
||
this structural check; they are caught separately by
|
||
embedded_exchanges() and flagged CONCAT/BAD_FCS at emit time.
|
||
'''
|
||
n = len(packet)
|
||
|
||
if n < 7:
|
||
return (False, 'too short (< 7)')
|
||
if n > MAX_TPDU_LEN:
|
||
return (False, 'exceeds T=0 max length')
|
||
|
||
cla = packet[0]
|
||
ins = packet[1]
|
||
sw1 = packet[-2]
|
||
|
||
if not plausible_cla(cla):
|
||
return (False, 'invalid CLA')
|
||
if not plausible_ins(ins):
|
||
return (False, 'invalid INS')
|
||
if not plausible_sw(sw1):
|
||
return (False, 'invalid SW1')
|
||
|
||
return (True, None)
|
||
|
||
|
||
def is_desynced(packet):
|
||
'''Classify an emitted T=0 packet as mis-framed.
|
||
|
||
All-zero packets are idle-low noise (suppressed elsewhere), NOT
|
||
desync. A real TPDU must reach its status word: >= 7 bytes ending
|
||
in a plausible SW1, never exceed the T=0 maximum, and start with a
|
||
valid CLA/INS pair.'''
|
||
if not packet:
|
||
return False
|
||
if all(b == 0 for b in packet):
|
||
return False
|
||
valid, _ = validate_t0_apdu(packet)
|
||
return not valid
|
||
|
||
|
||
# --- embedded-exchange (CONCAT) detector ---
|
||
# A byte-drop mis-frame can swallow several real T=0 exchanges into one
|
||
# structurally valid packet: each swallowed header survives verbatim in the
|
||
# data region, followed by its P3 data bytes and an interior status word.
|
||
# These constants bound detector precision (empirically tuned on all 10
|
||
# example traces: 0 hits on clean traces, every Samsung/xiaomi family hit).
|
||
# A swallowed header's CLA is validated with the same structural rule as the
|
||
# decoder itself (see plausible_cla): interindustry classes including the
|
||
# logical-channel numbers 0x01-0x03 are legitimate first bytes of a command.
|
||
EMBEDDED_CLA = frozenset(cla for cla in range(256) if plausible_cla(cla))
|
||
# Commands that actually occur on these cards' bus (T=0 + STK/CAT).
|
||
EMBEDDED_INS = frozenset((
|
||
0x12, 0x14, 0x20, 0x44, 0x70, 0x88,
|
||
0xA2, 0xA4, 0xB0, 0xB2, 0xB6,
|
||
0xC0, 0xC2, 0xCA, 0xCB, 0xD6, 0xDC, 0xF2,
|
||
))
|
||
# Real SW1 values that are not also frequent data bytes (BER-TLV tags).
|
||
EMBEDDED_SW1 = frozenset((
|
||
0x60, 0x61, 0x62, 0x63, 0x67, 0x68, 0x69,
|
||
0x6A, 0x6B, 0x6D, 0x6E, 0x6F, 0x90, 0x91,
|
||
0x93, 0x98, 0x99,
|
||
))
|
||
EMBEDDED_SW1_SLACK = 3 # SW1 search window after the nominal data end
|
||
|
||
|
||
def embedded_exchanges(buf):
|
||
'''Return [(header_start, sw1_index), ...] for complete T=0 exchanges
|
||
embedded inside a packet's data region (index 5 onward).
|
||
|
||
Pattern: header (CLA INS P1 P2 P3) + P3 data bytes + interior SW1/SW2
|
||
before the packet's own final status word. CLA must be structurally
|
||
valid (same rule as plausible_cla); INS must be a real command byte;
|
||
SW1 is accepted within a small slack window after the nominal data end
|
||
and must be interior (trailing bytes follow it).'''
|
||
n = len(buf)
|
||
out = []
|
||
if n < 12:
|
||
return out
|
||
q = 5
|
||
while q <= n - 9:
|
||
s = q
|
||
cla, ins, _p1, _p2, p3 = buf[s:s + 5]
|
||
if cla not in EMBEDDED_CLA:
|
||
q += 1
|
||
continue
|
||
if ins not in EMBEDDED_INS:
|
||
q += 1
|
||
continue
|
||
if p3 == 0:
|
||
q += 1
|
||
continue
|
||
d1 = s + 5 + p3
|
||
if d1 + 3 > n:
|
||
q += 1
|
||
continue
|
||
sw1 = -1
|
||
for e in range(d1, min(n - 1, d1 + EMBEDDED_SW1_SLACK + 1)):
|
||
if buf[e] in EMBEDDED_SW1:
|
||
sw1 = e
|
||
break
|
||
if sw1 == -1 or sw1 >= n - 2:
|
||
q += 1
|
||
continue
|
||
out.append((s, sw1))
|
||
q += 1
|
||
return out
|
||
|
||
|
||
MAX_TPDU_LEN = 271 # ISO 7816-3 T=0: header(5) + proc + 255 + SW(2) < 271
|
||
IDLE_RESYNC_ETU = 4 # idle bit-periods marking an exchange boundary
|
||
MAX_RESYNC_ATTEMPTS = 1024 # bail out if no idle gap found in that many tries
|
||
|
||
# ATR-hunt failure bounds. When the default-rate (372 CLK/bit) hunt keeps
|
||
# failing, the card is either idle or already running at a non-default F/D
|
||
# (e.g. Samsung F=512/D=32 -> 16 CLK/bit). Without a bound the hunt reads
|
||
# idle 0xFF forever at the wrong rate and can never reach the DATA state,
|
||
# where the live ETU is measured -- a deadlock. These cap the retries.
|
||
ATR_HUNT_ETU_ATTEMPTS = 3 # live-ETU recovery attempts before resuming DATA
|
||
ATR_HUNT_IDLE_RESETS = 3 # all-0xFF hunt resets before resuming DATA
|
||
ATR_HUNT_FAIL_LIMIT = 12 # total failed hunt rounds before resuming DATA
|
||
|
||
|
||
class Decoder(srd.Decoder):
|
||
api_version = 3
|
||
id = 'iso7816'
|
||
name = 'ISO 7816'
|
||
longname = 'Smartcard'
|
||
desc = 'ISO 7816 decoder (smartcard)'
|
||
license = 'gplv2+'
|
||
inputs = ['logic']
|
||
outputs = ['iso7816']
|
||
tags = ['Embedded/industrial']
|
||
channels = (
|
||
{'id': 'clk', 'name': 'CLK', 'desc': 'clock'},
|
||
{'id': 'data', 'name': 'data', 'desc': 'data'},
|
||
)
|
||
optional_channels = (
|
||
{'id': 'rst', 'name': 'RST', 'desc': 'reset (enable via rst_detect=true)'},
|
||
{'id': 'vcc', 'name': 'VCC', 'desc': 'power supply, digital only (enable via vcc_detect=true)'},
|
||
)
|
||
options = (
|
||
{'id': 'clock_option', 'desc': 'Clock option',
|
||
'default': 'native', 'values': ('native', 'detect', 'sample_as_clock')},
|
||
{'id': 'protocol', 'desc': 'Protocol',
|
||
'default': 'auto', 'values': ('auto', 'T=0', 'T=1')},
|
||
{'id': 'gsmtap_enable', 'desc': 'Live GSMTAP UDP stream (simtrace2-sniff compatible)',
|
||
'default': 'true', 'values': ('true', 'false')},
|
||
{'id': 'gsmtap_host', 'desc': 'GSMTAP destination host', 'default': '127.0.0.1'},
|
||
{'id': 'gsmtap_port', 'desc': 'GSMTAP destination port', 'default': 4729},
|
||
{'id': 'pcap_file', 'desc': 'Write PCAP to file in addition to binary output', 'default': ''},
|
||
{'id': 'rst_detect', 'desc': 'Track RST pin events (needs rst channel assigned)',
|
||
'default': 'false', 'values': ('false', 'true')},
|
||
{'id': 'vcc_detect', 'desc': 'Track VCC pin events (needs vcc channel assigned)',
|
||
'default': 'false', 'values': ('false', 'true')},
|
||
)
|
||
annotations = (
|
||
('warning', 'Human-readable warnings'),
|
||
('byte', 'Byte'),
|
||
('atr', 'ATR (Answer to Reset)'),
|
||
('pps', 'PPS (Protocol and parameters selection)'),
|
||
('t0', 'T=0 packet'),
|
||
('t1', 'T=1 packet'),
|
||
('t1-iblock', 'T=1 I-Block'),
|
||
('t1-rblock', 'T=1 R-Block'),
|
||
('t1-sblock', 'T=1 S-Block'),
|
||
('apdu', 'APDU'),
|
||
('rst', 'RST event'),
|
||
('vcc', 'VCC event'),
|
||
)
|
||
annotation_rows = (
|
||
('warnings', 'Warnings', (Ann.ANN_WARN,)),
|
||
('bytes', 'Bytes', (Ann.ANN_BYTE,)),
|
||
('type', 'Type', (Ann.ANN_ATR,Ann.ANN_PPS,Ann.ANN_T0,Ann.ANN_T1)),
|
||
('t1blocks', 'T=1 Decode', (Ann.ANN_T1IBLOCK,Ann.ANN_T1RBLOCK,Ann.ANN_T1SBLOCK)),
|
||
('apdus', 'apdu', (Ann.ANN_APDU,)),
|
||
('lines', 'Lines', (Ann.ANN_RST, Ann.ANN_VCC)),
|
||
)
|
||
binary = (
|
||
('pcap', 'PCAP format'),
|
||
)
|
||
|
||
clock_rate = {
|
||
0: 372,
|
||
1: 372,
|
||
2: 558,
|
||
3: 744,
|
||
4: 1116,
|
||
5: 1488,
|
||
6: 1860,
|
||
9: 512,
|
||
10: 768,
|
||
11: 1024,
|
||
12: 1536,
|
||
13: 2048
|
||
}
|
||
baud_rate = {
|
||
0: 1,
|
||
1: 1,
|
||
2: 2,
|
||
3: 4,
|
||
4: 8,
|
||
5: 16,
|
||
6: 32,
|
||
7: 64,
|
||
8: 12,
|
||
9: 20
|
||
}
|
||
|
||
|
||
# Channel indices within the pins tuple returned by self.wait()
|
||
# (declaration order of self.channels).
|
||
CLK_IDX = 0
|
||
DATA_IDX = 1
|
||
RST_IDX = 2
|
||
VCC_IDX = 3
|
||
|
||
def __init__(self):
|
||
self.reset()
|
||
|
||
def log(self, *args):
|
||
print(args, file=sys.stderr, flush=True)
|
||
|
||
def _note_discard(self, value, reason, idle=False):
|
||
# Report bytes consumed but not emitted as part of an APDU. Idle
|
||
# line noise (0x00/0xFF on an idle period) is counted and reported
|
||
# periodically so a long idle line cannot flood the session log;
|
||
# every other discard (real data) is logged and annotated always.
|
||
if idle:
|
||
self._idle_discard_total += 1
|
||
if self._idle_discard_total == 1 or self._idle_discard_total % 256 == 0:
|
||
self.log("discarded idle byte 0x%02x: %s (idle discards so far: %d)"
|
||
% (value, reason, self._idle_discard_total))
|
||
return
|
||
self._discard_total += 1
|
||
self.log("discarded byte 0x%02x: %s (discards so far: %d)"
|
||
% (value, reason, self._discard_total))
|
||
if self.ss is not None and self.ss >= 0:
|
||
self.put(self.ss, self.ss, self.out_ann,
|
||
[Ann.ANN_WARN, ["dropped 0x%02x: %s" % (value, reason)]])
|
||
return value
|
||
|
||
def _consume_first(self):
|
||
'''Consume the byte the last peek_byte() returned, removing it from
|
||
whichever source it came from (the _replay buffer or the peeked
|
||
byte). The decode-step consume paths use this so re-framed bytes
|
||
queued in _replay cannot loop forever.'''
|
||
if self._replay:
|
||
return self._replay.popleft()
|
||
b = self.peeked_byte
|
||
self.peeked_byte = None
|
||
self.peeked_samplenum = -1
|
||
return b
|
||
|
||
def reset(self):
|
||
self.peeked_byte = None
|
||
self.peeked_samplenum = -1
|
||
# Replay buffer for bytes read ahead but not consumed (used by the
|
||
# command-detect path in the ATR hunt to hand a command header back
|
||
# to the DATA state without dropping it).
|
||
self._replay = deque()
|
||
self.wrote_pcap_header = False
|
||
self.samplerate = None
|
||
self.sample_as_clock = False
|
||
self.detect_clock = False
|
||
self.fi = self.clock_rate[0]
|
||
self.di = self.baud_rate[0]
|
||
self.ss = self.es = -1
|
||
self.state = 'FIND START'
|
||
self.bits = []
|
||
self.clock_skip = 372
|
||
# Measured bit period (samples or CLK edges) from the first frame;
|
||
# 372 = spec default until measured. Must exist before any PPS
|
||
# handling -- native mode never measures it explicitly.
|
||
self.detected_clock_skip = 372
|
||
self.bit_samples = None
|
||
self._etu_confirm_count = 0
|
||
self._etu_fail_count = 0
|
||
self._edge_spacings = deque(maxlen=128)
|
||
self._samples_per_clock = None
|
||
# True when clock_skip is known to be correct: after a parsed ATR
|
||
# (372 default), an accepted PPS (FI/DI), or a successful ETU-based
|
||
# recovery on a mid-session capture. The CLK-sync reader is only
|
||
# trusted when this is set; otherwise the edge-list reader (which
|
||
# uses bit_samples) is the safe fallback.
|
||
self._clock_skip_confident = False
|
||
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
|
||
self._prev_wait_sn = None
|
||
self._stall = 0
|
||
# One-shot: at the first decode_step the initial RST/VCC levels are
|
||
# primed from the real first sample instead of the defaults below.
|
||
# Without this, last_rst=0/last_vcc=0 make "capture started while the
|
||
# card was already in reset" indistinguishable from a live session.
|
||
self._primed = False
|
||
self.lastSamplePositive = True
|
||
# Rolling parity-error monitor: warns when the wire degrades
|
||
# (e.g. GSM TX bursts coupling into the probe wiring).
|
||
self.parity_hist = deque(maxlen=64)
|
||
self.sig_warned = False
|
||
# Stuck-line watchdog: consecutive all-zero frames usually mean
|
||
# the I/O probe lost contact (floating low), not data.
|
||
self.zero_run = 0
|
||
# Idle-low APDU suppression counter (all-zero T=0 reads).
|
||
self._idle_low = 0
|
||
# Set after a desync so the next DATA step re-aligns on an
|
||
# inter-frame idle gap before resuming.
|
||
self._resync = False
|
||
# Discard accounting: every consumed byte that is not emitted as
|
||
# part of an APDU is reported. Non-idle discards (real data) are
|
||
# logged + annotated at every occurrence; idle 0x00/0xFF line noise
|
||
# is counted and reported periodically so a long idle line cannot
|
||
# flood the session log.
|
||
self._discard_total = 0
|
||
self._idle_discard_total = 0
|
||
# Set by handle_pps() when an accepted PPS changes the bit rate.
|
||
# Forces _measure_etu() on the next decode_step() cycle so the
|
||
# decoder re-locks to the new ETU from the live DATA line.
|
||
self._pps_speed_changed = False
|
||
# Count of times the ATR hunt failed to find a valid TS byte --
|
||
# bounds the "keep hunting" loop so a mid-session sniffer (no ATR
|
||
# ever present) eventually falls back to T=0 parsing instead of
|
||
# looping forever.
|
||
self._atr_hunt_count = 0
|
||
# Persistent hunt-failure accounting (reset only when an ATR is
|
||
# parsed, NOT on RST re-arm): a device that power-cycles RST while
|
||
# never sending a valid ATR would otherwise starve the escape below.
|
||
# _atr_fail_total counts every failed hunt round across activations;
|
||
# _hunt_etu_attempts caps live-ETU recovery; _atr_idle_resets caps the
|
||
# all-0xFF reset loop.
|
||
self._atr_fail_total = 0
|
||
self._hunt_etu_attempts = 0
|
||
self._atr_idle_resets = 0
|
||
# PPS is only attempted once after an ATR/warm reset. Prevents
|
||
# idle-line 0xFF bytes from being parsed as an endless stream of
|
||
# PPS requests when the card does not actually perform PPS.
|
||
self._pps_attempted = False
|
||
self.gsmtap = None
|
||
self.pcap_fh = None
|
||
self.track_rst = False
|
||
self.track_vcc = False
|
||
# RST/VCC line tracking uses level-stability hysteresis (see
|
||
# _line_hyst): a level must stay continuously stable for
|
||
# vcc_min_gap samples before it is "confirmed" and reported. This
|
||
# makes power-up and power-down symmetric -- a VCC sense line that
|
||
# rings on the rising edge (noisy/floating tap) no longer loses its
|
||
# "VCC on" event; it simply fires once the line settles HIGH.
|
||
# last_rst/last_vcc hold the *confirmed* level; *_cand / *_cand_sn
|
||
# track a pending level and the sample it first appeared.
|
||
self.last_rst = 0
|
||
self.last_vcc = 0
|
||
self.rst_cand = None
|
||
self.rst_cand_sn = 0
|
||
self.vcc_cand = None
|
||
self.vcc_cand_sn = 0
|
||
# Minimum quiet window (samples) a NEW level must hold before it is
|
||
# honored. 10 ms @ 16 MHz; set from samplerate in metadata().
|
||
self.vcc_min_gap = 0 # 0 = hysteresis disabled (no samplerate)
|
||
|
||
def metadata(self, key, value):
|
||
if key == srd.SRD_CONF_SAMPLERATE:
|
||
self.samplerate = value
|
||
if self.samplerate:
|
||
self.secs_per_sample = float(1) / float(self.samplerate)
|
||
# 10 ms debounce window, scaled to samples.
|
||
self.vcc_min_gap = int(self.samplerate * 0.010)
|
||
|
||
def start(self):
|
||
self.log("=== iso7816 decoder v{} ===".format(VERSION))
|
||
self._pcap_base_time = time.time()
|
||
self.out_python = self.register(srd.OUTPUT_PYTHON)
|
||
self.out_ann = self.register(srd.OUTPUT_ANN)
|
||
self.out_binary = self.register(srd.OUTPUT_BINARY)
|
||
self.sample_as_clock = self.options['clock_option'] == "sample_as_clock"
|
||
self.detect_clock = self.options['clock_option'] == "detect"
|
||
self.state = 'DATA'
|
||
self._atr_parsed = False
|
||
# Inverse convention (TS=0x3F): logic 1 = LOW, logic 0 = HIGH.
|
||
# Set when TS=0x3F is detected; all subsequent byte reads invert bits.
|
||
self._inverse_convention = False
|
||
# clock_skip is not trusted until recovered (the card may have already
|
||
# PPS'd to a faster ETU before the capture began).
|
||
if (self.options['protocol'] == "T=0"):
|
||
self.hasT0 = True
|
||
self.hasT1 = False
|
||
elif (self.options['protocol'] == "T=1"):
|
||
self.hasT0 = False
|
||
self.hasT1 = True
|
||
else:
|
||
self.hasT0 = False
|
||
self.hasT1 = False
|
||
# True only when the user explicitly set protocol=T=0/T=1 (not 'auto').
|
||
# Used to enable mid-session raw-burst emission (below) without
|
||
# affecting the default auto-protocol path.
|
||
self.explicit_protocol = self.options['protocol'] in ("T=0", "T=1")
|
||
|
||
# Live GSMTAP UDP stream (default on, mimics simtrace2-sniff).
|
||
if (self.options['gsmtap_enable'] == "true" and self.options['gsmtap_host']):
|
||
try:
|
||
self.gsmtap = GsmtapStreamSender(self.options['gsmtap_host'],
|
||
int(self.options['gsmtap_port']))
|
||
self.log("GSMTAP stream to",
|
||
"%s:%s" % (self.options['gsmtap_host'], self.options['gsmtap_port']))
|
||
except OSError as e:
|
||
self.gsmtap = None
|
||
self.log("GSMTAP stream disabled:", str(e))
|
||
|
||
# Optional direct PCAP file output.
|
||
if (self.options['pcap_file']):
|
||
try:
|
||
self.pcap_fh = open(self.options['pcap_file'], 'wb')
|
||
self.pcap_fh.write(self.pcap_global_header())
|
||
except OSError as e:
|
||
self.pcap_fh = None
|
||
self.log("PCAP file disabled:", str(e))
|
||
|
||
# Optional RST/VCC line tracking.
|
||
self.track_rst = self.options['rst_detect'] == "true"
|
||
self.track_vcc = self.options['vcc_detect'] == "true"
|
||
if (self.track_rst and hasattr(self, 'has_channel')
|
||
and not self.has_channel(self.RST_IDX)):
|
||
self.log("rst_detect=true but 'rst' channel not assigned - disabled")
|
||
self.track_rst = False
|
||
if (self.track_vcc and hasattr(self, 'has_channel')
|
||
and not self.has_channel(self.VCC_IDX)):
|
||
self.log("vcc_detect=true but 'vcc' channel not assigned - disabled")
|
||
self.track_vcc = False
|
||
|
||
def finish(self):
|
||
if self.pcap_fh is not None:
|
||
try:
|
||
self.pcap_fh.close()
|
||
except OSError:
|
||
pass
|
||
self.pcap_fh = None
|
||
if self.gsmtap is not None:
|
||
self.gsmtap.close()
|
||
self.gsmtap = None
|
||
|
||
def emit_packet(self, sub_type, payload, ss, es, flags=0):
|
||
'''Emit one event as PCAP binary stream + PCAP file + GSMTAP UDP.
|
||
|
||
`flags` is written into the GSMTAP header `res` byte (see
|
||
GSMTAP_FLAG_BAD_FCS in gsmtap_stream).'''
|
||
ts = self.ts_from_samplenum(ss)
|
||
pkt = pcap_udp_pkt(ts, payload, sub_type, flags)
|
||
self.put(ss, es, self.out_binary, [0, pkt.record_header()])
|
||
self.put(ss, es, self.out_binary, [0, pkt.packet()])
|
||
if self.pcap_fh is not None:
|
||
self.pcap_fh.write(pkt.record_header())
|
||
self.pcap_fh.write(pkt.packet())
|
||
if self.gsmtap is not None:
|
||
self.gsmtap.send(sub_type, payload, flags)
|
||
|
||
def _line_hyst(self, lvl, confirmed, cand, cand_sn):
|
||
'''Level-stability hysteresis for RST/VCC line tracking.
|
||
|
||
A level different from `confirmed` becomes a *candidate*. It is
|
||
confirmed (and returned for emission) once it has been
|
||
continuously stable for >= vcc_min_gap samples -- either because it
|
||
persists that long, or because it ends (the line moves again) after
|
||
having been stable that long. This matters because the decoder is
|
||
edge-driven: the stable window between a fall and the next rise is
|
||
never re-sampled, so a candidate must be confirmable when it ENDS.
|
||
|
||
Returns (emit_lvl, edge_sn, new_confirmed, new_cand, new_cand_sn):
|
||
emit_lvl is the level just confirmed (or None); edge_sn is the sample
|
||
the (now confirmed) level first appeared.'''
|
||
now = self.samplenum
|
||
if lvl == confirmed:
|
||
# Candidate (if any) ended by returning to the confirmed level.
|
||
# If it had been stable long enough, report it; the current line
|
||
# level (lvl) is now the state to track as a fresh candidate, so
|
||
# a genuine rise (e.g. power-up) is not lost on the same call.
|
||
if cand is not None and ((not self.vcc_min_gap)
|
||
or (now - cand_sn) >= self.vcc_min_gap):
|
||
return (cand, cand_sn, cand, lvl, now)
|
||
return (None, 0, confirmed, None, 0)
|
||
if cand is None:
|
||
# First divergence from confirmed.
|
||
if not self.vcc_min_gap:
|
||
# Hysteresis disabled: report the edge immediately.
|
||
return (lvl, now, lvl, None, 0)
|
||
return (None, 0, confirmed, lvl, now)
|
||
if cand == lvl:
|
||
# Same candidate persists: confirm once stable long enough.
|
||
if (not self.vcc_min_gap) or (now - cand_sn) >= self.vcc_min_gap:
|
||
return (lvl, cand_sn, lvl, None, 0)
|
||
return (None, 0, confirmed, cand, cand_sn)
|
||
# lvl differs from both confirmed and the current candidate: the
|
||
# candidate just ended and a new level appeared. If the candidate
|
||
# was stable, report it; then start tracking the new level.
|
||
if (not self.vcc_min_gap) or (now - cand_sn) >= self.vcc_min_gap:
|
||
return (cand, cand_sn, cand, lvl, now)
|
||
return (None, 0, confirmed, lvl, now)
|
||
|
||
def line_events(self, pins):
|
||
'''Check RST/VCC levels via level-stability hysteresis and emit
|
||
events + annotations for confirmed level changes.
|
||
|
||
An edge is only reported once the new level has been continuously
|
||
stable for >= vcc_min_gap samples, so a noisy/floating sense line
|
||
that rings on a transition no longer drops the event. Returns True
|
||
if any event fired.'''
|
||
fired = False
|
||
if (self.track_rst):
|
||
lvl = pins[self.RST_IDX]
|
||
emit_lvl, edge_sn, self.last_rst, self.rst_cand, self.rst_cand_sn = \
|
||
self._line_hyst(lvl, self.last_rst, self.rst_cand, self.rst_cand_sn)
|
||
if emit_lvl is not None:
|
||
direction = 1 if emit_lvl == 0 else 0 # 1=asserted, 0=deasserted
|
||
self.emit_packet(GSMTAP_SIM_RST_EVENT,
|
||
bytes([direction, emit_lvl, 0]), edge_sn, edge_sn)
|
||
text = ('RST asserted' if emit_lvl == 0 else 'RST deasserted')
|
||
self.put(edge_sn, edge_sn, self.out_ann, [Ann.ANN_RST, [text]])
|
||
self.log(text)
|
||
fired = True
|
||
if (emit_lvl == 0):
|
||
# RST asserted (LOW): card entering reset state. A reset
|
||
# makes the card send its ATR at the DEFAULT ETU (372 CLK
|
||
# cycles/bit), so clock_skip must be restored to 372 --
|
||
# otherwise a post-PPS clock_skip (e.g. 16) would make the
|
||
# decoder hunt for the fresh ATR at the wrong rate and
|
||
# mis-frame every byte. Prepare for the next ATR by
|
||
# re-arming the hunt and clearing _atr_parsed so the next
|
||
# RST deassert will also re-arm (first cycle at startup).
|
||
self._atr_parsed = False
|
||
self._inverse_convention = False
|
||
self.clock_skip = 372
|
||
self.bit_samples = None
|
||
self.state = 'FIND START'
|
||
self._atr_hunt_count = 0
|
||
self._pps_attempted = False
|
||
if (emit_lvl == 1):
|
||
# RST deasserted (HIGH): card released from reset and
|
||
# will send a fresh ATR. However, the 10ms hysteresis
|
||
# means this event fires AFTER the ATR has already been
|
||
# parsed and the decoder entered DATA state. In that
|
||
# case, stay in DATA so the terminal's T=0 commands
|
||
# (which follow the ATR) are decoded. Only re-arm if
|
||
# _atr_parsed is False (startup / first cycle, where no
|
||
# ATR has been parsed yet).
|
||
if self._atr_parsed:
|
||
self.log("warm reset: staying in DATA (ATR already parsed)")
|
||
else:
|
||
# _atr_parsed is False: a fresh ATR is expected. The
|
||
# 10ms hysteresis confirms the deassert ~10ms after
|
||
# the physical edge -- typically in the MIDDLE of that
|
||
# very ATR. By then the hunt is already armed with
|
||
# the correct default rate (clock_skip=372,
|
||
# bit_samples=None): either by the RST assert handler
|
||
# (warm reset, edge observed) or by the "RST low at
|
||
# start" cold-boot path. Re-arming here would clear
|
||
# the in-flight ATR's timing and garble it, so only
|
||
# re-arm if we are still sitting in DATA with nothing
|
||
# armed (the one case the assert handler/cold-boot
|
||
# path never covered: a deassert without a preceding
|
||
# observed assert or hunt).
|
||
if self.state == 'DATA':
|
||
self.state = 'FIND START'
|
||
self._atr_hunt_count = 0
|
||
self._pps_attempted = False
|
||
self.bit_samples = None
|
||
self._etu_confirm_count = 0
|
||
self._etu_fail_count = 0
|
||
self.clock_skip = 372
|
||
self._inverse_convention = False
|
||
self._clock_skip_confident = \
|
||
not (self.sample_as_clock or self.detect_clock)
|
||
self.log("RST deasserted with no hunt armed; re-arming ATR hunt")
|
||
else:
|
||
self.log("RST deasserted (hunt/ATR already in progress)")
|
||
if (self.track_vcc):
|
||
lvl = pins[self.VCC_IDX]
|
||
emit_lvl, edge_sn, self.last_vcc, self.vcc_cand, self.vcc_cand_sn = \
|
||
self._line_hyst(lvl, self.last_vcc, self.vcc_cand, self.vcc_cand_sn)
|
||
if emit_lvl is not None:
|
||
direction = emit_lvl # 1=power applied, 0=power removed
|
||
self.emit_packet(GSMTAP_SIM_VCC_EVENT,
|
||
bytes([direction, emit_lvl, 0]), edge_sn, edge_sn)
|
||
text = ('VCC on' if emit_lvl == 1 else 'VCC off')
|
||
self.put(edge_sn, edge_sn, self.out_ann, [Ann.ANN_VCC, [text]])
|
||
self.log(text)
|
||
fired = True
|
||
return fired
|
||
|
||
def pcap_global_header(self):
|
||
# See https://wiki.wireshark.org/Development/LibpcapFileFormat.
|
||
h = b'\xd4\xc3\xb2\xa1' # Magic, indicate microsecond ts resolution
|
||
h += b'\x02\x00' # Major version 2
|
||
h += b'\x04\x00' # Minor version 4
|
||
h += b'\x00\x00\x00\x00' # Correction vs. UTC, seconds
|
||
h += b'\x00\x00\x00\x00' # Timestamp accuracy
|
||
h += b'\xff\xff\x00\x00' # Max packet len
|
||
h += b'\x01\x00\x00\x00' # Link layer
|
||
return h
|
||
|
||
def get_bytes(self, bits):
|
||
byte = 0
|
||
for bit in list(reversed(bits)):
|
||
byte = (byte << 1) | bit
|
||
return byte
|
||
|
||
def write_pcap_header(self):
|
||
if not self.wrote_pcap_header:
|
||
self.put(0, 0, self.out_binary, [0, self.pcap_global_header()])
|
||
self.wrote_pcap_header = True
|
||
|
||
def ts_from_samplenum(self, sample):
|
||
# Use the full sample number. The previous c_uint32 truncation (plus
|
||
# an overflow counter that was never incremented) made pcap timestamps
|
||
# wrap and go backwards once a capture passed 2^32 samples (~4.5 min
|
||
# at 16 MHz). float64 holds sample numbers well past 2^53, so the
|
||
# whole sample domain is representable.
|
||
ts = float(sample) * self.secs_per_sample + self._pcap_base_time
|
||
return (int(ts), int((ts % 1.0) * 1e6))
|
||
|
||
def signal_quality(self, err):
|
||
'''Feed one frame's parity result (1 = bad) into the rolling
|
||
monitor; warn once per degradation episode when >= 8 of the last
|
||
64 frames failed parity.'''
|
||
self.parity_hist.append(err)
|
||
if len(self.parity_hist) < 32:
|
||
return
|
||
errs = sum(self.parity_hist)
|
||
if errs >= 8 and not self.sig_warned:
|
||
self.sig_warned = True
|
||
self.log("SIGNAL INTEGRITY WARNING: {}/{} recent frames failed "
|
||
"parity (noise? RF burst? contact?)".format(
|
||
errs, len(self.parity_hist)))
|
||
try:
|
||
wss = max(self.ss, 0)
|
||
wes = max(self.es, wss + 1)
|
||
self.put(wss, wes, self.out_ann,
|
||
[Ann.ANN_WARN, ["noisy signal"]])
|
||
except (SystemError, EOFError):
|
||
raise
|
||
except Exception:
|
||
pass
|
||
elif errs <= 2:
|
||
self.sig_warned = False
|
||
|
||
def read_first_byte(self):
|
||
'''Find the first start-bit falling edge and read the first byte with
|
||
the standard re-anchored reader. (Mid-session ETU recovery is done by
|
||
_measure_etu, called once from the mid-session branch of decode_step
|
||
before the normal T=0/T=1 framing state machine takes over.)'''
|
||
self.wait_data_falling()
|
||
return self.read_byte_no_wait()
|
||
|
||
def _measure_clock_period(self):
|
||
'''Measure the period of the native CLK signal in samples.
|
||
|
||
For native CLK mode, the actual bit period is
|
||
clock_skip * samples_per_clock. Measuring it directly removes the
|
||
hard-coded 3× approximation and gives the correct ETU for the ATR and
|
||
for any post-PPS speed. Returns True if a stable period was found.
|
||
|
||
Collects multiple CLK rising-edge spacings and averages them, which
|
||
handles CLK periods that alternate between samples (e.g. a 4,3,3
|
||
pattern for ~5.33 MHz at 16 MHz sample rate).'''
|
||
if self.sample_as_clock or self.detect_clock:
|
||
return False
|
||
spacings = []
|
||
last_sn = None
|
||
try:
|
||
# Collect up to 16 consecutive CLK rising-edge spacings.
|
||
for _ in range(16):
|
||
pins = self.wait({self.CLK_IDX: 'r'})
|
||
if pins is None:
|
||
return False
|
||
sn = self.samplenum
|
||
if last_sn is not None:
|
||
spacings.append(sn - last_sn)
|
||
last_sn = sn
|
||
if len(spacings) < 3:
|
||
return False
|
||
# Use average period instead of robust_min to handle varying CLK periods
|
||
# (e.g., 4,3,3 alternating pattern).
|
||
period = sum(spacings) / len(spacings)
|
||
if 2 <= period <= 100000:
|
||
self._samples_per_clock = period
|
||
self.log("clock period (samples):", period)
|
||
return True
|
||
except (SystemError, EOFError):
|
||
raise
|
||
except Exception:
|
||
pass
|
||
return False
|
||
|
||
def _compute_bit_samples(self):
|
||
'''Return the best available bit-period estimate in samples.
|
||
|
||
Uses the measured clock period if available; otherwise falls back to
|
||
the legacy clock_skip * 3 estimate.'''
|
||
if self._samples_per_clock is not None:
|
||
return int(self.clock_skip * self._samples_per_clock)
|
||
return int(self.clock_skip * 3)
|
||
|
||
def _derive_clock_skip_from_etu(self):
|
||
'''Derive clock_skip (CLK edges per ETU) from the recovered ETU.
|
||
|
||
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
|
||
has recovered bit_samples (samples per ETU) and _measure_clock_period
|
||
has recovered _samples_per_clock (samples per CLK cycle), clock_skip
|
||
= bit_samples / _samples_per_clock. Sets _clock_skip_confident on
|
||
success so the CLK-sync reader is used; leaves it clear (edge-list
|
||
fallback) if the CLK period could not be measured.'''
|
||
if self._samples_per_clock and self.bit_samples:
|
||
cs = int(round(float(self.bit_samples) / float(self._samples_per_clock)))
|
||
if cs >= 1:
|
||
if cs != self.clock_skip:
|
||
self.log("clock_skip recovered from ETU:", cs,
|
||
"(bit_samples", self.bit_samples,
|
||
"/ spc", self._samples_per_clock, ")")
|
||
self.clock_skip = cs
|
||
self._clock_skip_confident = True
|
||
return True
|
||
return False
|
||
|
||
def _measure_etu(self):
|
||
'''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.
|
||
|
||
Jumps to each DATA transition (O(edges), immune to Clock Stop Mode)
|
||
and collects edge spacings until an idle gap ends the burst. The
|
||
shortest *recurring* spacing equals one etu, so this recovers fast /
|
||
non-standard ETUs (e.g. a phone SIM already PPS'd to a short etu) that
|
||
the spec-default 372-clock etu would mis-read by ~20x.
|
||
|
||
The result is **locked after 3 successful decodes**: bit_samples is
|
||
set after measurement, then validated by decoding subsequent bytes.
|
||
If 2 consecutive parity errors occur before validation, the
|
||
measurement is discarded and re-measured from the next burst.
|
||
Consumes one burst for the measurement; the normal framing resumes
|
||
on the next start bit (the first exchange may therefore be
|
||
mis-framed -- expected for a mid-session capture).'''
|
||
if self.bit_samples is not None and self._etu_confirm_count >= 3:
|
||
return
|
||
if self._start_fall is None:
|
||
self.wait_data_falling()
|
||
if self._eof or self._start_fall is None:
|
||
return
|
||
e0 = self._start_fall
|
||
self._start_fall = None # consumed; framing re-waits on its own
|
||
self.ss = e0
|
||
idle_limit = int((self.bit_samples or
|
||
self._compute_bit_samples()) * 20)
|
||
if idle_limit < 3000:
|
||
idle_limit = 3000
|
||
edges = [(e0, 0)]
|
||
last_sn = e0
|
||
while True:
|
||
pins = self.wait({self.DATA_IDX: 'e'})
|
||
if pins is None:
|
||
break
|
||
sn = self.samplenum
|
||
if sn <= last_sn:
|
||
break # no progress (end of capture) -> stop scan
|
||
last_sn = sn
|
||
d = pins[self.DATA_IDX]
|
||
if (sn - edges[-1][0]) > idle_limit:
|
||
# Big gap => burst ended; leave this edge for the framing.
|
||
break
|
||
edges.append((sn, d))
|
||
if len(edges) >= 2:
|
||
spacings = [edges[i + 1][0] - edges[i][0]
|
||
for i in range(len(edges) - 1)]
|
||
# Filter glitches (< MIN_ETU) then check if the first spacing
|
||
# is an outlier (scan started in a noisy pre-burst region).
|
||
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._etu_confirm_count = 0
|
||
self._etu_fail_count = 0
|
||
if self.bit_samples is None:
|
||
self.bit_samples = self._compute_bit_samples()
|
||
self._etu_confirm_count = 0
|
||
self._etu_fail_count = 0
|
||
# 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
|
||
# every byte from here on.
|
||
self._edge_read = True
|
||
self.log("etu recovered (samples):", self.bit_samples)
|
||
|
||
def _recover_etu_in_hunt(self):
|
||
'''Recover the live bit period from the DATA line while still in the
|
||
ATR hunt.
|
||
|
||
The hunt reads at the spec-default 372 CLK/bit. A device already
|
||
running at a non-default F/D (Samsung F=512/D=32 -> 16 CLK/bit) or a
|
||
mid-session capture with no ATR sends no 372-rate bytes, so every
|
||
candidate reads as idle 0xFF and the hunt can never reach the DATA
|
||
state -- where _measure_etu() normally lives. Measure the ETU here
|
||
(after the default-rate hunt has already failed a few rounds) and
|
||
re-derive clock_skip so subsequent hunt reads use the true rate.
|
||
Reuses the DATA-path measurement and its _confirm_etu() validation.'''
|
||
if self._samples_per_clock is None:
|
||
self._measure_clock_period()
|
||
self._measure_etu()
|
||
self._derive_clock_skip_from_etu()
|
||
# Start a fresh default-rate counting window at the recovered rate.
|
||
self._atr_hunt_count = 0
|
||
self.log("hunt ETU recovery attempt", self._hunt_etu_attempts,
|
||
"bit_samples", self.bit_samples,
|
||
"clock_skip", self.clock_skip)
|
||
|
||
def _atr_hunt_failure_action(self, byte):
|
||
'''Decide what to do after a failed ATR hunt round.
|
||
|
||
Returns one of:
|
||
'recover' -- stop and recover the live ETU, then re-hunt
|
||
'retry' -- go back to FIND START (bounded)
|
||
'resume_data' -- give up on the ATR and resume T=0 framing
|
||
|
||
The counters are persistent across RST re-arms (reset only when an
|
||
ATR is parsed) so a phone that cycles RST while never emitting a
|
||
valid ATR cannot starve the escape. Pure function of state + byte
|
||
so it is unit-testable without libsigrokdecode.'''
|
||
self._atr_fail_total += 1
|
||
# Live-ETU recovery every 2 failures, up to ATR_HUNT_ETU_ATTEMPTS.
|
||
if (self.bit_samples is None
|
||
and self._hunt_etu_attempts < ATR_HUNT_ETU_ATTEMPTS
|
||
and self._atr_fail_total >=
|
||
2 * (self._hunt_etu_attempts + 1)):
|
||
self._hunt_etu_attempts += 1
|
||
return 'recover'
|
||
# Hard bound: never hunt forever on an idle/unsyncable line.
|
||
if self._atr_fail_total >= ATR_HUNT_FAIL_LIMIT:
|
||
return 'resume_data'
|
||
if byte == 0xFF and self._atr_idle_resets >= ATR_HUNT_IDLE_RESETS:
|
||
return 'resume_data'
|
||
if self._atr_hunt_count < 8:
|
||
return 'retry'
|
||
return 'resume_data'
|
||
|
||
def _confirm_etu(self, parity_ok):
|
||
'''Validate ETU measurement after _measure_etu. Call after each byte
|
||
decode in _read_byte_edges. After 3 successful decodes the
|
||
measurement is locked. If 2 consecutive parity errors occur before
|
||
validation, the measurement is discarded and re-measured from the
|
||
next burst.'''
|
||
if self._etu_confirm_count >= 3:
|
||
return
|
||
if parity_ok:
|
||
self._etu_confirm_count += 1
|
||
if self._etu_confirm_count >= 3:
|
||
self.log("ETU confirmed:", self.bit_samples)
|
||
else:
|
||
self._etu_fail_count += 1
|
||
if self._etu_fail_count >= 2:
|
||
self.log("ETU re-measure (bad initial measurement):",
|
||
self.bit_samples)
|
||
self.bit_samples = None
|
||
self._etu_confirm_count = 0
|
||
self._etu_fail_count = 0
|
||
|
||
def _robust_min(self, spacings):
|
||
'''Smallest spacing that recurs (>=3x): the true etu. Two
|
||
consecutive differing bits produce a 1-etu gap, so a real bit period
|
||
recurs often; a glitch is unique. Falls back to the raw minimum.
|
||
|
||
Spacings below MIN_ETU (12 samples at 16 MHz) are rejected as
|
||
metastability glitches — they are too short for any valid ISO 7816
|
||
bit period and would otherwise be picked as the "minimum recurring"
|
||
spacing by the counter.'''
|
||
MIN_ETU = 12
|
||
from collections import Counter
|
||
c = Counter(s for s in spacings if s >= MIN_ETU)
|
||
for v in sorted(c):
|
||
if c[v] >= 3:
|
||
return v
|
||
# Fallback: accept any spacing >= MIN_ETU, or raw min if nothing qualifies.
|
||
valid = [s for s in spacings if s >= MIN_ETU]
|
||
return min(valid) if valid else min(spacings)
|
||
|
||
def _deglitch_edges(self, edges, bs):
|
||
'''Remove pulses shorter than a fraction of an ETU from an edge list.
|
||
|
||
Phone traces (especially Samsung at 16 CLK/ETU) contain 1-sample
|
||
(62.5 ns) LOW glitches in inter-byte guard times. These are far
|
||
shorter than the shortest real ISO 7816 pulse (about 1 ETU) and
|
||
would otherwise be accepted as false start-bit falls.
|
||
|
||
The filter uses an iterative narrowest-first strategy: at each step
|
||
it finds the shortest pulse, and if it is shorter than 0.2 * bs it
|
||
removes the edge that *ends* the pulse. Removing the end edge lets
|
||
the previous (real) edge's value persist, re-merging the surrounding
|
||
fragments back into the original ETU-multiple pulse. The first edge
|
||
(the start-bit fall passed to _read_byte_edges) is never removed.
|
||
|
||
Returns a new edge list; the input list is not modified.'''
|
||
if not edges:
|
||
return edges
|
||
threshold = max(1, int(0.2 * bs))
|
||
# Work with a mutable copy. Each item is [sample, value].
|
||
out = [[s, v] for (s, v) in edges]
|
||
changed = True
|
||
while changed:
|
||
changed = False
|
||
# Find the shortest pulse (distance between consecutive edges).
|
||
min_width = None
|
||
min_idx = None
|
||
n = len(out)
|
||
for i in range(n - 1):
|
||
width = out[i + 1][0] - out[i][0]
|
||
if width < threshold and (min_width is None
|
||
or width < min_width):
|
||
min_width = width
|
||
min_idx = i
|
||
if min_idx is not None:
|
||
# Remove the edge that ends the short pulse. This keeps the
|
||
# preceding edge (the real transition) and lets its value
|
||
# persist until the next edge. If the short pulse starts at
|
||
# the first edge, keep the first edge and remove the second.
|
||
del out[min_idx + 1]
|
||
changed = True
|
||
return [(s, v) for [s, v] in out]
|
||
|
||
def _bits_at(self, e, edges, bs):
|
||
'''Reconstruct the 10 bits (start + 8 data + parity) of the byte whose
|
||
start-bit fall is at sample e, from the DATA edge list. Returns None
|
||
only if `e` is not a genuine start (no following edge).'''
|
||
bits = []
|
||
for k in range(10):
|
||
m = e + round((k + 0.5) * bs)
|
||
val = 1
|
||
for (s, v) in edges:
|
||
if s <= m:
|
||
val = v
|
||
else:
|
||
break
|
||
# Inverse convention (TS=0x3F): logic 1 = LOW, logic 0 = HIGH.
|
||
if self._inverse_convention:
|
||
val = 1 - val
|
||
bits.append(val)
|
||
return bits
|
||
|
||
def _next_start_fall(self, edges, after, bs):
|
||
'''First GENUINE byte-start FALL (1->0) at or after sample `after`,
|
||
= the next byte start.
|
||
|
||
A real start bit is LOW for a full ETU. A sub-ETU fall inside the
|
||
stop-bit / inter-byte guard time is a phantom (line bounce) and must
|
||
not be treated as the next byte's start -- latching onto it shifts
|
||
the whole frame. A fall is accepted only if the line stays LOW for
|
||
at least half an ETU after it (a genuine start bit's low hold is
|
||
~1 ETU; the first data-bit transition arrives no sooner).'''
|
||
thresh = int(0.5 * bs)
|
||
for i in range(len(edges)):
|
||
s, v = edges[i]
|
||
if s < after or v != 0:
|
||
continue
|
||
nxt = edges[i + 1][0] if i + 1 < len(edges) else s + thresh + 1
|
||
if (nxt - s) < thresh:
|
||
continue
|
||
return s
|
||
return None
|
||
|
||
def _wait_clk_rising(self, n):
|
||
'''Wait for n CLK rising edges. Returns False on end-of-capture.
|
||
|
||
CLK runs continuously for the whole of a character (Clock Stop only
|
||
happens between characters), so within a byte it is safe to skip most
|
||
of the way by samples and then land on the exact rising edge. This
|
||
keeps the edge-exactness of CLK-edge timing without paying one wait()
|
||
per CLK cycle. Lands within +/-1 CLK cycle of the target edge, which
|
||
is far inside the stable region of a bit (a bit is held for the whole
|
||
ETU).'''
|
||
if n <= 0:
|
||
return True
|
||
if self._samples_per_clock and n > 2:
|
||
# Skip (n - 2) cycles by samples, then take the last 2 edges
|
||
# exactly. Undershooting by a cycle is harmless (the bit is
|
||
# stable for the full ETU), and the final two edge waits re-align
|
||
# us to a genuine CLK edge.
|
||
skip = int((n - 2) * self._samples_per_clock)
|
||
if skip > 0:
|
||
self.wait({'skip': skip})
|
||
n = 2
|
||
for _ in range(n):
|
||
pins = self.wait({self.CLK_IDX: 'r'})
|
||
if pins is None:
|
||
self._eof = True
|
||
return False
|
||
return True
|
||
|
||
def _read_byte_clk(self):
|
||
'''Decode a single byte using CLK-edge counting for bit timing.
|
||
|
||
Counts clock_skip CLK rising edges per ETU and samples DATA at the bit
|
||
centers (1.5, 2.5, ..., 9.5 ETU from the start-bit fall). Because the
|
||
bit value is held stable for the whole ETU, sampling near the centre
|
||
is exact for ANY byte -- including 0x00 / 0xff, which have too few DATA
|
||
transitions for the edge-list reader to anchor reliably.
|
||
|
||
Safe across Clock Stop Mode: CLK is never stopped mid-character, and
|
||
it always resumes a few cycles before the next start bit (the guard
|
||
time), so once wait_data_falling() has found a start bit, CLK is
|
||
running for the whole frame. clock_skip is protocol-defined (372 for
|
||
the ATR, FI/DI after PPS), so no runtime ETU measurement is needed on
|
||
the native path -- this is what makes decoding deterministic.'''
|
||
if self._start_fall is None:
|
||
self.wait_data_falling()
|
||
if self._eof:
|
||
return 0x00
|
||
e0 = self._start_fall
|
||
self._start_fall = None
|
||
self.ss = e0
|
||
bs = self.clock_skip
|
||
self.bits = [0] # start bit (always low)
|
||
prev_edges = 0
|
||
for i in range(9):
|
||
# bit centres at (1.5 + i) ETU from the start fall
|
||
total = int(round((1.5 + i) * bs))
|
||
if not self._wait_clk_rising(total - prev_edges):
|
||
# End of capture mid-byte: pad so the frame stays 10 bits.
|
||
while len(self.bits) < 10:
|
||
self.bits.append(self.bits[-1] if self.bits else 1)
|
||
break
|
||
prev_edges = total
|
||
pins = self.wait({'skip': 0})
|
||
if pins is None:
|
||
self._eof = True
|
||
while len(self.bits) < 10:
|
||
self.bits.append(self.bits[-1] if self.bits else 1)
|
||
break
|
||
bit = pins[self.DATA_IDX]
|
||
# Inverse convention (TS=0x3F): logic 1 = LOW, logic 0 = HIGH.
|
||
if self._inverse_convention:
|
||
bit = 1 - bit
|
||
self.bits.append(bit)
|
||
self.es = self.samplenum
|
||
self.signal_quality(1 if (self.bits.count(1) % 2 != 0) else 0)
|
||
parity_ok = (self.bits.count(1) % 2 == 0)
|
||
if (self.bits.count(1) % 2 != 0):
|
||
self.log(self.samplenum, "CHKSUM ERROR: ", 0, "bits: ", self.bits)
|
||
self.put(self.ss, self.samplenum, self.out_ann,
|
||
[0, ["CHKSUM ERROR bits={bits}".format(bits=self.bits)]])
|
||
if self.bit_samples is not None:
|
||
self._confirm_etu(parity_ok)
|
||
byte = self.get_bytes(self.bits[1:9])
|
||
# Stuck-line watchdog (same as the other readers).
|
||
if (sum(self.bits) == 0):
|
||
self.zero_run += 1
|
||
if (self.zero_run == 32):
|
||
self.log("I/O line reads all-zero for", self.zero_run,
|
||
"frames - stuck low? check pogo contact")
|
||
try:
|
||
self.put(self.ss, self.samplenum, self.out_ann,
|
||
[Ann.ANN_WARN, ["I/O stuck low?"]])
|
||
except (SystemError, EOFError):
|
||
raise
|
||
except Exception:
|
||
pass
|
||
else:
|
||
self.zero_run = 0
|
||
self.log(self.samplenum, self.bits[1:9], " : ", "0x{:02x}".format(byte))
|
||
self.put(self.ss, self.samplenum, self.out_ann, [1, [hex(byte)]])
|
||
return byte
|
||
|
||
def _read_byte_edges(self, bs):
|
||
'''Decode a single byte from the DATA line using edge-list
|
||
reconstruction (robust to Clock Stop Mode / gated CLK, where fixed
|
||
sample-skip timing drifts). Returns the byte, having set self.ss/es
|
||
and queued the next start-bit position in self._start_fall.
|
||
|
||
Jumps straight to DATA edges around the current start fall (O(edges),
|
||
not O(samples)), collects ~11 etu of edges (one byte plus the gap to
|
||
the next start bit), reconstructs the 10 bits from their actual
|
||
positions, and leaves self._start_fall at the next byte so subsequent
|
||
reads stay re-anchored (no cross-byte drift). A large turn-around
|
||
idle (CLK stopped between command and response) simply leaves
|
||
_start_fall unset here, and the next read re-hunts via
|
||
wait_data_falling().'''
|
||
# Safety: if we were called without a queued start bit, hunt for one.
|
||
if self._start_fall is None:
|
||
self.wait_data_falling()
|
||
if self._eof:
|
||
return 0x00
|
||
e0 = self._start_fall
|
||
self._start_fall = None
|
||
self.ss = e0
|
||
# Collect DATA edges for this byte + the inter-byte gap.
|
||
# Use a 15 ETU window so a 2 ETU turnaround idle (observed on
|
||
# Samsung phone at 16 CLK/bit) still leaves the next start-bit fall
|
||
# inside the collected edge list.
|
||
edges = [(e0, 0)]
|
||
limit = e0 + int(15 * bs)
|
||
last_sn = e0
|
||
while True:
|
||
pins = self.wait({self.DATA_IDX: 'e'})
|
||
if pins is None:
|
||
break
|
||
sn = self.samplenum
|
||
if sn <= last_sn:
|
||
break
|
||
last_sn = sn
|
||
edges.append((sn, pins[self.DATA_IDX]))
|
||
if sn > limit:
|
||
break
|
||
# Remove sub-ETU glitch pulses (common on phone SIMs at fast ETUs
|
||
# like 16 CLK/bit). Without this the edge reader may latch onto a
|
||
# 1-sample bounce in an inter-byte guard time and read the wrong
|
||
# byte, desynchronising the whole T=0 frame.
|
||
edges = self._deglitch_edges(edges, bs)
|
||
bits = self._bits_at(e0, edges, bs)
|
||
# Keep the spacings for ETU locking; the first few bytes of an ATR
|
||
# provide the most reliable reference for the actual bit period.
|
||
if len(edges) >= 2:
|
||
for i in range(1, len(edges)):
|
||
self._edge_spacings.append(edges[i][0] - edges[i - 1][0])
|
||
nxt = None
|
||
if bits is not None and bits[0] == 0:
|
||
self.bits = bits
|
||
byte = self.get_bytes(bits[1:9])
|
||
nxt = self._next_start_fall(edges, e0 + int(10 * bs), bs)
|
||
else:
|
||
# bits[0] is the start bit; it must be 0 after _bits_at() because
|
||
# _bits_at() already inverts the value for inverse convention.
|
||
# bits[0] != 0 means we latched onto a spurious edge (phantom).
|
||
# Do NOT drop the byte: dropping replaces a real byte with a fake
|
||
# 0x00 / shifts the exchange boundary, which is exactly how a
|
||
# whole exchange gets swallowed (CONCAT). Instead slide forward
|
||
# to the next genuine start-bit fall inside this same edge window
|
||
# and re-anchor there.
|
||
for (s, v) in edges:
|
||
if s <= e0 or v != 0:
|
||
continue
|
||
cand = self._bits_at(s, edges, bs)
|
||
if cand is not None and cand[0] == 0:
|
||
e0 = s
|
||
bits = cand
|
||
self.bits = bits
|
||
byte = self.get_bytes(bits[1:9])
|
||
nxt = self._next_start_fall(edges, e0 + int(10 * bs), bs)
|
||
break
|
||
else:
|
||
# No genuine start in the window (EOF / pathological line):
|
||
# keep the legacy drop; the next read re-hunts.
|
||
self.bits = []
|
||
byte = 0
|
||
self.ss = e0
|
||
if nxt is not None:
|
||
self._start_fall = nxt
|
||
self.es = self.samplenum
|
||
self.signal_quality(1 if (self.bits.count(1) % 2 != 0) else 0)
|
||
parity_ok = (self.bits.count(1) % 2 == 0)
|
||
if (self.bits.count(1) % 2 != 0):
|
||
self.log(self.samplenum, "CHKSUM ERROR: ", 0, "bits: ", self.bits)
|
||
self.put(self.ss, self.samplenum, self.out_ann,
|
||
[0, ["CHKSUM ERROR bits={bits}".format(bits=self.bits)]])
|
||
if self.bit_samples is not None:
|
||
self._confirm_etu(parity_ok)
|
||
if (sum(self.bits) == 0):
|
||
self.zero_run += 1
|
||
if (self.zero_run == 32):
|
||
self.log("I/O line reads all-zero for", self.zero_run,
|
||
"frames - stuck low? check pogo contact")
|
||
try:
|
||
self.put(self.ss, self.samplenum, self.out_ann,
|
||
[Ann.ANN_WARN, ["I/O stuck low?"]])
|
||
except (SystemError, EOFError):
|
||
raise
|
||
except Exception:
|
||
pass
|
||
else:
|
||
self.zero_run = 0
|
||
self.log(self.samplenum, self.bits[1:9], " : ", "0x{:02x}".format(byte))
|
||
self.put(self.ss, self.samplenum, self.out_ann, [1, [hex(byte)]])
|
||
return byte
|
||
|
||
def wait_data_falling(self):
|
||
'''Wait for the I/O start-bit falling edge, servicing optional
|
||
RST/VCC line events while hunting.
|
||
|
||
The bit period is measured directly from the DATA line in SAMPLES
|
||
(not by counting CLK edges), so a frozen CLK during Clock Stop Mode
|
||
cannot desync the decoder. A start bit is accepted only after DATA
|
||
has been HIGH for >= 1.5 bit periods (inter-character guard, rejects
|
||
intra-byte edges / idle glitches).'''
|
||
min_high = 4
|
||
if self.bit_samples:
|
||
if self._edge_read:
|
||
# DATA must be HIGH (idle) for at least half an etu before a
|
||
# fall is accepted as a start bit. This rejects intra-byte
|
||
# glitch falls yet still accepts the legitimate inter-byte gap
|
||
# (the ~1-etu stop bit), which a 1.5-etu guard would wrongly
|
||
# reject on fast ETUs (e.g. a phone SIM at ~53 samples/etu),
|
||
# causing bytes to be skipped / 0x00 inserted.
|
||
min_high = max(int(0.5 * self.bit_samples), 4)
|
||
else:
|
||
min_high = max(int(1.5 * self.bit_samples), 4)
|
||
while True:
|
||
# Wait for DATA idle, servicing RST/VCC.
|
||
# In direct convention: idle = HIGH, start bit = falling edge (HIGH→LOW).
|
||
# In inverse convention: idle = LOW, start bit = rising edge (LOW→HIGH).
|
||
if self._inverse_convention:
|
||
idle_cond = {self.DATA_IDX: 'l'} # Wait for LOW (idle in inverse)
|
||
start_edge = 'r' # Rising edge = start bit in inverse
|
||
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]
|
||
if (self.track_rst):
|
||
cond.append({self.RST_IDX: 'e'})
|
||
if (self.track_vcc):
|
||
cond.append({self.VCC_IDX: 'e'})
|
||
while True:
|
||
pins = self.wait(cond)
|
||
if self._stall_check(pins, track=False):
|
||
self._eof = True
|
||
return
|
||
if (self.line_events(pins)):
|
||
continue
|
||
break
|
||
hi_start = self.samplenum
|
||
# Wait for the start-bit edge; re-anchor the high-start across
|
||
# any line events so the measured guard stays accurate.
|
||
cond2 = [{self.DATA_IDX: start_edge}]
|
||
if (self.track_rst):
|
||
cond2.append({self.RST_IDX: 'e'})
|
||
if (self.track_vcc):
|
||
cond2.append({self.VCC_IDX: 'e'})
|
||
while True:
|
||
pins = self.wait(cond2)
|
||
if self._stall_check(pins):
|
||
self._eof = True
|
||
return
|
||
if (self.line_events(pins)):
|
||
hi_start = self.samplenum
|
||
continue
|
||
break
|
||
if (self.samplenum - hi_start) < min_high:
|
||
continue
|
||
if self._min_pulse_samples > 0:
|
||
pins = self.wait({'skip': self._min_pulse_samples})
|
||
if pins is not None:
|
||
bit = pins[self.DATA_IDX]
|
||
idle_val = 1 if not self._inverse_convention else 0
|
||
if bit == idle_val:
|
||
self._glitch_rejects += 1
|
||
self.log("start-bit glitch rejected (#%d)"
|
||
% self._glitch_rejects)
|
||
continue
|
||
self._start_fall = self.samplenum
|
||
return
|
||
|
||
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.
|
||
|
||
`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:
|
||
self._stall = 0
|
||
self._prev_wait_sn = self.samplenum
|
||
return self._stall > 4
|
||
|
||
def read_byte(self):
|
||
if self._replay:
|
||
return self._replay.popleft()
|
||
if (self.peeked_byte != None):
|
||
byte = self.peeked_byte
|
||
self.peeked_byte = None
|
||
self.peeked_samplenum = -1
|
||
return byte;
|
||
# In edge-read (mid-session, gated-CLK) mode the next start bit is
|
||
# already queued by _read_byte_edges, so re-hunting would wrongly
|
||
# reject the short inter-byte gap. Otherwise re-hunt normally.
|
||
if not (self._edge_read and self._start_fall is not None):
|
||
self.wait_data_falling()
|
||
if self._eof:
|
||
return None
|
||
return self.read_byte_no_wait()
|
||
|
||
def peek_byte(self):
|
||
if self._replay:
|
||
return self._replay[0]
|
||
if (self.peeked_byte != None):
|
||
return self.peeked_byte
|
||
if not (self._edge_read and self._start_fall is not None):
|
||
self.wait_data_falling()
|
||
if self._eof:
|
||
return None
|
||
self.peeked_samplenum = self._start_fall
|
||
self.peeked_byte = self.read_byte_no_wait()
|
||
return self.peeked_byte
|
||
|
||
def read_byte_no_wait(self):
|
||
self.bits = []
|
||
if (self.sample_as_clock):
|
||
# Experimental mode: DATA doubles as the clock. Keep the
|
||
# original sample-as-clock reader (bits sampled on the DATA
|
||
# edge, with no separate timing measurement).
|
||
self.ss = self.samplenum
|
||
self.clock_skip = 0
|
||
pins = self.wait({1: 'r'})
|
||
self.clock_skip = self.samplenum - self.ss + 2
|
||
self.detected_clock_skip = self.clock_skip
|
||
self.wait({'skip': int(self.clock_skip / 3)})
|
||
self.bits.append(0)
|
||
for x in range(9):
|
||
pins = self.wait({'skip': 0})
|
||
bit = pins[1]
|
||
# Inverse convention (TS=0x3F): logic 1 = LOW, logic 0 = HIGH.
|
||
if self._inverse_convention:
|
||
bit = 1 - bit
|
||
self.bits.append(bit)
|
||
self.wait({'skip': self.clock_skip - 4})
|
||
self.es = self.samplenum
|
||
self.signal_quality(1 if (self.bits.count(1) % 2 != 0) else 0)
|
||
if (self.bits.count(1) % 2 != 0):
|
||
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)]])
|
||
byte = self.get_bytes(self.bits[1:9])
|
||
if (sum(self.bits) == 0):
|
||
self.zero_run += 1
|
||
if (self.zero_run == 32):
|
||
self.log("I/O line reads all-zero for", self.zero_run,
|
||
"frames - stuck low? check pogo contact")
|
||
try:
|
||
self.put(self.ss, self.samplenum, self.out_ann,
|
||
[Ann.ANN_WARN, ["I/O stuck low?"]])
|
||
except (SystemError, EOFError):
|
||
raise
|
||
except Exception:
|
||
pass
|
||
else:
|
||
self.zero_run = 0
|
||
self.log(self.samplenum, self.bits[1:9], " : ", "0x{:02x}".format(byte))
|
||
self.put(self.ss, self.samplenum, self.out_ann, [1, [hex(byte)]])
|
||
return byte
|
||
# Normal path. Two bit readers are available:
|
||
# - CLK-synchronous (_read_byte_clk): exact CLK-edge timing, used in
|
||
# native mode where a real CLK channel is present. Handles 0x00 /
|
||
# 0xff perfectly and is deterministic.
|
||
# - Edge-list (_read_byte_edges): DATA-edge reconstruction, used for
|
||
# the non-native clock modes (sample_as_clock / detect) and as a
|
||
# fallback when clock_skip has not been established yet.
|
||
# Callers (read_byte / peek_byte / read_first_byte) ensure
|
||
# _start_fall is set before calling here.
|
||
if self._use_clk_sync():
|
||
return self._read_byte_clk()
|
||
return self._read_byte_edges(self.bit_samples or self._compute_bit_samples())
|
||
|
||
def _use_clk_sync(self):
|
||
'''True when the CLK-synchronous reader should be used.
|
||
|
||
Only in native clock mode (a real CLK channel) AND only when
|
||
clock_skip is known to be correct (_clock_skip_confident). In
|
||
sample_as_clock / detect modes there is no usable CLK signal, and on
|
||
a mid-session capture clock_skip is unknown until recovered -- in
|
||
both cases the edge-list DATA reader (driven by bit_samples) is the
|
||
safe fallback.'''
|
||
return (not (self.sample_as_clock or self.detect_clock)
|
||
and self._clock_skip_confident)
|
||
|
||
|
||
def resync_idle(self):
|
||
'''Re-align framing after a desync: wait for DATA to go to idle level
|
||
so the next peek lands on a genuine start bit of a fresh exchange
|
||
instead of a mid-burst byte. Gating-immune: we poll DATA in
|
||
samples (which advance even when CLK is frozen), and bound the hunt
|
||
by a sample budget so a pathologically-busy (or stuck) line never
|
||
stalls the decoder.'''
|
||
bs = self.bit_samples or self._compute_bit_samples()
|
||
budget = int(IDLE_RESYNC_ETU * bs) * 8
|
||
step = max(int(bs), 1)
|
||
loops = budget // step + 16
|
||
# In direct convention: idle = HIGH (mark).
|
||
# In inverse convention: idle = LOW (mark is inverted).
|
||
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.
|
||
|
||
def handle_atr(self, pins):
|
||
|
||
atr_start = self.samplenum;
|
||
|
||
self.log("START ATR:", self.samplenum);
|
||
self.state = 'ATR'
|
||
|
||
tA = []
|
||
tB = []
|
||
tC = []
|
||
tD = []
|
||
historicalBytes = []
|
||
self.ATR = []
|
||
# Clear any stale edge spacings; the first few ATR bytes give us the
|
||
# most reliable ETU reference for the whole session.
|
||
self._edge_spacings.clear()
|
||
# Try to lock the ETU from the actual CLK frequency before reading the
|
||
# first ATR byte. This removes the hard-coded clock_skip*3 estimate.
|
||
if self._samples_per_clock is None:
|
||
self._measure_clock_period()
|
||
# Ensure the sample-domain ETU is available for framing
|
||
# (wait_data_falling / resync_idle) even though the CLK-sync reader
|
||
# itself only needs clock_skip.
|
||
if self.bit_samples is None:
|
||
self.bit_samples = self._compute_bit_samples()
|
||
|
||
if (self.peeked_byte != None):
|
||
atr_start = self.peeked_samplenum
|
||
byte = self.read_byte()
|
||
else:
|
||
byte = self.read_first_byte()
|
||
atr_start = self.ss
|
||
# 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
|
||
# `ff 10 95 7a`, power-up noise) can appear on DATA before RST even
|
||
# deasserts. Resync past those instead of mis-parsing them as the
|
||
# ATR and permanently desyncing the session.
|
||
ts_attempts = 0
|
||
while (byte not in (0x3b, 0x3f) and ts_attempts < 64):
|
||
# Command-detect: a plausible CLA immediately followed by a
|
||
# plausible INS is the start of a command, not an ATR. Hand the
|
||
# header back to the DATA state for normal T=0 framing instead of
|
||
# silently skipping it as an "invalid TS" byte (this is how a
|
||
# SELECT was being lost while the decoder hunted for an ATR that
|
||
# was not coming). The full 5-byte header is still validated in
|
||
# the DATA state; ambiguous frames are emitted flagged, not dropped.
|
||
#
|
||
# CLA=0x00 is excluded: it's technically valid for interindustry
|
||
# commands, but during ATR hunt it's overwhelmingly noise (the
|
||
# real SELECT that was lost used CLA=0x00 and appeared *after* the
|
||
# ATR, not during the hunt). Including it causes false positives
|
||
# on noise like the 0x00 0x1E seen in the live test.
|
||
if plausible_cla(byte) and byte != 0x00:
|
||
nxt = self.peek_byte()
|
||
if plausible_ins(nxt):
|
||
self._replay.appendleft(byte)
|
||
self.hasT0 = True
|
||
self.hasT1 = False
|
||
self.state = 'DATA'
|
||
self.log("Command header 0x{cla:02x} 0x{ins:02x} during ATR hunt; decoding as command".format(cla=byte, ins=nxt))
|
||
return
|
||
self.log("Invalid TS byte 0x{byte:02x} (pre-activation?), resyncing".format(byte=byte))
|
||
self.put(atr_start, self.samplenum, self.out_ann,
|
||
[Ann.ANN_WARN, ["Invalid TS byte 0x{byte:02x} - skipped".format(byte=byte)]])
|
||
byte = self.read_byte()
|
||
if byte is None:
|
||
# End of capture mid-hunt: stop cleanly instead of raising
|
||
# TypeError (None & 0x80) in the next loop iteration.
|
||
return
|
||
atr_start = self.ss
|
||
ts_attempts += 1
|
||
if (byte not in (0x3b, 0x3f)):
|
||
self._atr_hunt_count += 1
|
||
# No valid ATR found this round. Never fabricate a synthetic
|
||
# ATR: a stray 0x3B/0x3F byte inside command traffic (e.g. the
|
||
# FID '3F00' of a SELECT) would otherwise be mis-committed as a
|
||
# fake 2-byte ATR and destroy the surrounding command.
|
||
action = self._atr_hunt_failure_action(byte)
|
||
if action == 'recover':
|
||
# Default-rate hunt has failed a few rounds: the card may be
|
||
# idle or already at a non-default F/D. Measure the live ETU
|
||
# and re-hunt at that rate instead of looping on idle 0xFF.
|
||
self.log("ATR hunt failing; recovering live ETU")
|
||
self._recover_etu_in_hunt()
|
||
self.state = 'FIND START'
|
||
return
|
||
if action == 'retry':
|
||
if byte == 0xFF:
|
||
self._atr_idle_resets += 1
|
||
self.log("line appears idle (all 0xFF), resetting ATR "
|
||
"hunt (#{n})".format(n=self._atr_idle_resets))
|
||
self._atr_hunt_count = 0
|
||
else:
|
||
self.log("No valid TS byte, back to FIND START "
|
||
"(hunt {count})".format(count=self._atr_hunt_count))
|
||
self.state = 'FIND START'
|
||
return
|
||
# Give up hunting for an ATR here without emitting anything;
|
||
# resume T=0 framing from the next byte instead of inventing one.
|
||
# Clear bit_samples so the DATA path measures the ETU fresh.
|
||
self.log("No valid ATR after {count} hunts (fails={fails}); "
|
||
"resuming DATA without synthetic ATR".format(
|
||
count=self._atr_hunt_count,
|
||
fails=self._atr_fail_total))
|
||
self.bit_samples = None
|
||
self._etu_confirm_count = 0
|
||
self._etu_fail_count = 0
|
||
self.hasT0 = True
|
||
self.hasT1 = False
|
||
self.state = 'DATA'
|
||
self._resync = True
|
||
if (self.options['protocol'] == "T=0"):
|
||
self.hasT0 = True
|
||
self.hasT1 = False
|
||
elif (self.options['protocol'] == "T=1"):
|
||
self.hasT0 = False
|
||
self.hasT1 = True
|
||
return
|
||
else:
|
||
self._atr_hunt_count = 0
|
||
self._atr_fail_total = 0
|
||
self._hunt_etu_attempts = 0
|
||
self._atr_idle_resets = 0
|
||
self.ATR.append(byte)
|
||
# TS=0x3F means inverse convention: logic 1 = LOW, logic 0 = HIGH.
|
||
# All subsequent byte reads must invert bits.
|
||
self._inverse_convention = (byte == 0x3f)
|
||
self.log("TS=0x{ts:02x} ({conv} convention)".format(
|
||
ts=byte, conv="inverse" if self._inverse_convention else "direct"))
|
||
|
||
t0 = self.read_byte()
|
||
if t0 is None:
|
||
return
|
||
self.ATR.append(t0)
|
||
|
||
firstT0 = t0
|
||
|
||
self.hasT0 = False
|
||
self.hasT1 = False
|
||
self.hasT15 = False
|
||
while (firstT0 & 0b11110000):
|
||
# Defense-in-depth: a valid ISO 7816 ATR is at most 33 bytes.
|
||
# If the parse runs past that (e.g. a noise byte slipped in as
|
||
# TS and the TD chain never terminates), abandon the parse and
|
||
# hunt again rather than commit a giant bogus ATR.
|
||
if (len(self.ATR) > 33):
|
||
self.log("ATR exceeds 33 bytes (noise-induced?), abandoning parse")
|
||
self._atr_hunt_count += 1
|
||
self.state = 'FIND START'
|
||
return
|
||
if (firstT0 & 0b00010000):
|
||
byte = self.read_byte()
|
||
tA.append(byte)
|
||
self.ATR.append(byte)
|
||
if (firstT0 & 0b00100000):
|
||
byte = self.read_byte()
|
||
tB.append(byte)
|
||
self.ATR.append(byte)
|
||
if (firstT0 & 0b01000000):
|
||
byte = self.read_byte()
|
||
tC.append(byte)
|
||
self.ATR.append(byte)
|
||
if (firstT0 & 0b10000000):
|
||
byte = self.read_byte()
|
||
if ((byte & 0x0F) == 0):
|
||
self.hasT0 = True
|
||
elif ((byte & 0x0F) == 1):
|
||
self.hasT1 = True
|
||
elif ((byte & 0x0F) == 15):
|
||
self.hasT15 = True
|
||
else:
|
||
self.log("Invalid Protocol in ATR: ", "T=",(byte & 0x0F))
|
||
self.put(atr_start, self.samplenum, self.out_ann, [0, ["Invalid Protocol in ATR T={protocol}".format(protocol=(byte & 0x0F))]])
|
||
tD.append(byte)
|
||
self.ATR.append(byte)
|
||
firstT0 = byte
|
||
self.log("TD("+str(len(tD))+"): ", hex(firstT0), "T=",(byte & 0x0F))
|
||
else:
|
||
firstT0 = 0;
|
||
|
||
for _ in range(0, t0 & 0x0F):
|
||
byte = self.read_byte()
|
||
self.ATR.append(byte)
|
||
|
||
if (self.hasT1 == False and self.hasT0 == False):
|
||
self.hasT0 = True
|
||
|
||
if (self.hasT1 == True or self.hasT15):
|
||
byte = self.read_byte()
|
||
self.ATR.append(byte)
|
||
xor = 0
|
||
for i in range(1, len(self.ATR)): xor = xor ^ self.ATR[i]
|
||
if (xor != 0):
|
||
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.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.emit_packet(GSMTAP_SIM_ATR, bytes(self.ATR), atr_start, self.samplenum)
|
||
|
||
self.log("ENDATR", codecs.encode(bytes(self.ATR), 'hex'))
|
||
self._atr_parsed = True
|
||
# A real ATR was parsed at the default rate, so clock_skip=372 is now
|
||
# confirmed correct -- the CLK-sync reader is trusted from here on.
|
||
self._clock_skip_confident = True
|
||
# The ATR is always sent at the default rate (372 CLK/bit).
|
||
# _measure_etu() may have inflated clock_skip via inter-byte gap
|
||
# averaging; reset to 372 so PPS (also at the default rate) is
|
||
# read at the correct bit boundary.
|
||
self.clock_skip = 372
|
||
# Also lock bit_samples so the mid-session re-measure in decode_step
|
||
# does not re-run and overwrite clock_skip=372 with a wrong value
|
||
# derived from pre-ATR edge spacings. _confirm_etu() may have
|
||
# cleared bit_samples during ATR hunt (parity failures on noise),
|
||
# which would trigger _measure_etu() on the next decode_step.
|
||
self.bit_samples = self.clock_skip * (self._samples_per_clock or 3)
|
||
# Lock the ETU so _confirm_etu() cannot clear bit_samples after
|
||
# parity failures on post-ATR noise bytes. Without this, two
|
||
# consecutive parity errors would trigger an ETU re-measure that
|
||
# overwrites clock_skip=372 with a wrong value from pre-ATR data.
|
||
self._etu_confirm_count = 3
|
||
self._etu_fail_count = 0
|
||
self.state = 'DATA'
|
||
self._resync = True # Wait for inter-frame idle gap before reading commands
|
||
# PPS may be attempted once after this ATR.
|
||
self._pps_attempted = False
|
||
|
||
if (self.options['protocol'] == "T=0"):
|
||
self.hasT0 = True
|
||
self.hasT1 = False
|
||
elif (self.options['protocol'] == "T=1"):
|
||
self.hasT0 = False
|
||
self.hasT1 = True
|
||
|
||
def t1_parse_block(self, es):
|
||
packet = []
|
||
lrc = 0;
|
||
nad = self.read_byte()
|
||
lrc = lrc ^ nad
|
||
self.put(es, self.samplenum, self.out_ann, [1, "NAD:" + hex(nad)])
|
||
sad = nad & 0x70
|
||
dad = nad & 0x07
|
||
self.log("T=1 NAD=", hex(nad), "SAD=", hex(sad), "DAD=", hex(dad))
|
||
|
||
pcb = self.read_byte()
|
||
lrc = lrc ^ pcb
|
||
|
||
isSBlock = False
|
||
isRBlock = False
|
||
isIBlock = False
|
||
if (pcb & 0b11000000 == 0b11000000):
|
||
# S-Block
|
||
isSBlock = True
|
||
self.put(es, self.samplenum, self.out_ann, [3, "PCB S " + hex(pcb)])
|
||
self.log("PCB S-Block", hex(pcb & 0b00111111))
|
||
elif (pcb & 0b10000000 == 0b10000000):
|
||
# R-Block
|
||
isRBlock = True
|
||
self.put(es, self.samplenum, self.out_ann, [3, "PCB R " + hex(pcb)])
|
||
self.log("PCB R-Block", hex(pcb & 0b00111111))
|
||
else:
|
||
# I-Block
|
||
isIBlock = True
|
||
self.put(es, self.samplenum, self.out_ann, [3, "PCB I " + hex(pcb)])
|
||
self.log("PCB I-Block", hex(pcb))
|
||
|
||
bLen = self.read_byte()
|
||
lrc = lrc ^ bLen
|
||
if (bLen > 0):
|
||
for _ in range(bLen):
|
||
byte = self.read_byte()
|
||
lrc = lrc ^ byte
|
||
if (isIBlock): packet.append(byte)
|
||
|
||
# CRC to implement
|
||
bLrc = self.read_byte()
|
||
lrc = lrc ^ bLrc
|
||
|
||
if (lrc != 0):
|
||
self.put(es, self.samplenum, self.out_ann, [0, ["Invalid checksum on T=1 block, , got={got:02x} expected={expected:02x}".format(got=lrc,expected=bLrc)]])
|
||
self.log("Invalid checksum on T=1 block", hex(lrc), hex(bLrc))
|
||
|
||
self.log("block_content", codecs.encode(bytes(packet), 'hex'))
|
||
|
||
if (isIBlock):
|
||
self.put(es, self.samplenum, self.out_ann, [6, ["I-Block", "I-Block len={len} isMultiBlock={multi}".format(len=bLen,multi=(pcb & 0b00100000 > 0))]])
|
||
if (isRBlock):
|
||
self.put(es, self.samplenum, self.out_ann, [7, ["R-Block", "R-Block flag={flag:02x}".format(flag=pcb & 0b00111111)]])
|
||
if (isSBlock):
|
||
self.put(es, self.samplenum, self.out_ann, [8, ["S-Block", "S-Block flag={flag:02x}".format(flag=pcb & 0b00111111)]])
|
||
|
||
if (isIBlock and pcb & 0b00100000): # m-flag
|
||
self.log("T=1 Multiblock flag", hex(pcb))
|
||
for _ in range(32): # bounded: garbage must not loop forever
|
||
isIBlock2,packet2 = self.t1_parse_block(self.samplenum)
|
||
if (isIBlock2):
|
||
packet = packet + packet2
|
||
break
|
||
|
||
|
||
return isIBlock,packet;
|
||
|
||
def handle_pps(self):
|
||
# Mark PPS attempted so idle 0xFF bytes after a failed/no-PPS
|
||
# scenario do not loop back into PPS detection indefinitely.
|
||
self._pps_attempted = True
|
||
lrc = 0
|
||
ss = self.peeked_samplenum
|
||
pps = self.read_byte()
|
||
pps_req = [pps]
|
||
pps0 = self.read_byte()
|
||
pps_req.append(pps0)
|
||
# An idle I/O line idles HIGH (repeated 0xFF). A genuine PPS0 always
|
||
# has its reserved bits clear (bit7 and bits 0..3 == 0), so the value
|
||
# is one of 0x00/0x10/0x20/.../0x70. 0xFF (or any byte with bit7 or
|
||
# low nibble set) is not a PPS0 -- it is idle noise. Without this
|
||
# guard an idle line is mis-read as an endless stream of "PPS accepted"
|
||
# and real APDUs are never reached.
|
||
if (pps0 & 0x8F) != 0:
|
||
self.log("0xFF followed by 0x%02x is not a valid PPS0 - idle line, skipping" % pps0)
|
||
self._resync = True
|
||
return
|
||
pps1 = 0; pps2 = 0; pps3 = 0
|
||
if (pps0 & 0b00010000):
|
||
pps1 = self.read_byte()
|
||
pps_req.append(pps1)
|
||
lrc = lrc ^ pps1
|
||
if (pps0 & 0b00100000):
|
||
pps2 = self.read_byte()
|
||
pps_req.append(pps2)
|
||
lrc = lrc ^ pps2
|
||
if (pps0 & 0b01000000):
|
||
pps3 = self.read_byte()
|
||
pps_req.append(pps3)
|
||
lrc = lrc ^ pps3
|
||
pck = self.read_byte()
|
||
pps_req.append(pck)
|
||
lrc = lrc ^ pps ^ pps0 ^ pck
|
||
if (lrc != 0):
|
||
self.put(ss, self.samplenum, self.out_ann, [0, ["INVALID Checksum on PPS Request, got={got:02x} expected={expected:02x}".format(got=pck,expected=(lrc ^ pps ^ pps0))]])
|
||
self.log("INVALID Checksum on PPS Request", hex(lrc))
|
||
# End of the request frame: the response starts on the next byte.
|
||
# Keep the boundary sample so the two halves can be emitted as the
|
||
# separate standard GSMTAP subtypes (PPS_REQ / PPS_RSP).
|
||
es_req = self.samplenum
|
||
|
||
r_lrc = 0
|
||
r_pps = self.read_byte()
|
||
pps_rsp = [r_pps]
|
||
r_pps1 = 0; r_pps2 = 0; r_pps3 = 0
|
||
if (r_pps != 0xFF):
|
||
self.put(ss, self.samplenum, self.out_ann, [0, ["PPS Request not confirmed"]])
|
||
self.log("PPS Request not confirmed", r_pps)
|
||
r_pps0 = self.read_byte()
|
||
pps_rsp.append(r_pps0)
|
||
if (r_pps0 & 0b00010000):
|
||
r_pps1 = self.read_byte()
|
||
pps_rsp.append(r_pps1)
|
||
r_lrc = r_lrc ^ r_pps1
|
||
if (r_pps0 & 0b00100000):
|
||
r_pps2 = self.read_byte()
|
||
pps_rsp.append(r_pps2)
|
||
r_lrc = r_lrc ^ r_pps2
|
||
if (r_pps0 & 0b01000000):
|
||
r_pps3 = self.read_byte()
|
||
pps_rsp.append(r_pps3)
|
||
r_lrc = r_lrc ^ r_pps3
|
||
r_pck = self.read_byte()
|
||
pps_rsp.append(r_pck)
|
||
r_lrc = r_lrc ^ r_pps ^ r_pps0 ^ r_pck
|
||
if (r_lrc != 0):
|
||
self.put(ss, self.samplenum, self.out_ann, [0, ["INVALID Checksum on PPS Response, got={got:02x} expected={expected:02x}".format(got=r_pck,expected=(r_lrc ^ r_pps ^ r_pps0))]])
|
||
self.log("INVALID Checksum on PPS Response", hex(r_lrc))
|
||
|
||
if (pps0 == r_pps0 and pps1 == r_pps1 and pps2 == r_pps2 and pps3 == r_pps3):
|
||
if (self.detect_clock or self.sample_as_clock):
|
||
# .get() with spec defaults: unknown FI/DI codes from a
|
||
# corrupted PPS1 must not raise KeyError or divide by zero.
|
||
tmp_fi = self.clock_rate.get(int(pps1 >> 4), 372)
|
||
tmp_di = self.baud_rate.get(int(pps1 & 0x0F), 1)
|
||
if tmp_di == 0:
|
||
self.log("Corrupted PPS: DI=0 (invalid)")
|
||
return
|
||
tmp_clock_skip = int(tmp_fi / tmp_di)
|
||
self.log("Received PPS change: FI", tmp_fi, "DI", tmp_di, "clock_skip", tmp_clock_skip)
|
||
self.clock_skip = int(tmp_clock_skip * self.detected_clock_skip / 372)
|
||
self.fi = tmp_fi
|
||
self.di = tmp_di
|
||
self.log("PPS Success new settings (calculated): FI", self.fi, "DI", self.di, "clock_skip", self.clock_skip)
|
||
else:
|
||
# Native CLK mode: clock_skip counts real CLK edges per
|
||
# data bit. After an ACCEPTED PPS both sides switch the
|
||
# UART to F/D cycles per bit (e.g. 512/16 = 32 edges),
|
||
# so the decoder must follow the switch. Live capture
|
||
# proof: ATR+PPS decode cleanly at 372, everything after
|
||
# aliases into garbage if clock_skip stays at 372.
|
||
self.fi = self.clock_rate.get(int(pps1 >> 4), 372)
|
||
self.di = self.baud_rate.get(int(pps1 & 0x0F), 1)
|
||
self.clock_skip = max(int(self.fi // self.di), 1)
|
||
self.log("PPS accepted: FI", self.fi, "DI", self.di, "clock_skip", self.clock_skip)
|
||
# PPS changed the bit rate. How we follow it depends on the reader:
|
||
#
|
||
# - Native CLK-sync reader (_use_clk_sync): clock_skip is the ONLY
|
||
# timing input -- the CLK period is unchanged by PPS, so the new
|
||
# ETU is exactly clock_skip * samples_per_clock. No DATA
|
||
# re-measurement is needed (and none should happen: consuming a
|
||
# burst here would silently drop the first post-PPS command).
|
||
# Just refresh bit_samples for wait_data_falling / edge fallback.
|
||
#
|
||
# - Edge-list reader (non-native modes): there is no usable CLK
|
||
# reference, so re-measure the ETU from the live DATA line on
|
||
# the next decode cycle; that burst is emitted flagged, not
|
||
# silently consumed.
|
||
# PPS changed the bit rate. How we follow it depends on the mode:
|
||
#
|
||
# - Native CLK mode (real CLK channel): after an ACCEPTED PPS the
|
||
# card and terminal switch to FI/DI CLK cycles per bit, and the
|
||
# CLK period itself is unchanged. So clock_skip = FI/DI is
|
||
# exact -- no DATA re-measurement is needed (and none should
|
||
# happen: consuming a burst here would silently drop the first
|
||
# post-PPS command). Just refresh bit_samples for
|
||
# wait_data_falling / the edge-list fallback.
|
||
#
|
||
# - sample_as_clock / detect modes: there is no usable CLK
|
||
# reference, so re-measure the ETU from the live DATA line on
|
||
# the next decode cycle; that burst is emitted flagged, not
|
||
# silently consumed.
|
||
if not (self.sample_as_clock or self.detect_clock):
|
||
self.bit_samples = self._compute_bit_samples()
|
||
self._pps_speed_changed = False
|
||
# Re-hunt for the next start bit after the PPS response.
|
||
self._start_fall = None
|
||
else:
|
||
self._pps_speed_changed = True
|
||
self.bit_samples = None
|
||
self._start_fall = None
|
||
# clock_skip now reflects the negotiated FI/DI, so it is known to
|
||
# be correct regardless of which reader is active.
|
||
self._clock_skip_confident = True
|
||
else:
|
||
self.log("INVALID PPS. Request & Response not matching.", hex(r_lrc))
|
||
self.put(ss, self.samplenum, self.out_ann, [0, ["INVALID PPS. Request & Response not matching"]])
|
||
self.put(ss, self.samplenum, self.out_ann, [3, ["PPS", "PPS DI={di} FI={fi} clock_skip={clock_skip}".format(di=self.di,fi=self.fi,clock_skip=self.clock_skip)]])
|
||
# Standard GSMTAP-SIM framing: request and response are separate
|
||
# packets (sub_type 0x02 / 0x03), each with its own sample span.
|
||
self.emit_packet(GSMTAP_SIM_PPS_REQ, bytes(pps_req), ss, es_req)
|
||
self.emit_packet(GSMTAP_SIM_PPS_RSP, bytes(pps_rsp), es_req, 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.'''
|
||
if self._at_eof():
|
||
return False
|
||
self._last_sn = self.samplenum
|
||
# Prime the initial RST/VCC levels from the actual first sample (no
|
||
# event emission, no advancement). The reset() defaults are both 0,
|
||
# which is indistinguishable from a card that is genuinely in reset;
|
||
# a capture started during power-on (RST already LOW, the assertion
|
||
# edge in the past) would otherwise miss the assertion and try to
|
||
# measure the ETU while the card is inactive. With the true initial
|
||
# levels, RST-low-at-start means "expect a cold-boot ATR on deassert"
|
||
# and the DATA state waits for that deassert before measuring.
|
||
# Re-anchor the hysteresis candidates relative to the primed level so
|
||
# the first confirmed transition is measured from the true state.
|
||
if not self._primed:
|
||
self._primed = True
|
||
pins = self.wait({'skip': 0})
|
||
if pins is not None:
|
||
if self.track_rst:
|
||
self.last_rst = pins[self.RST_IDX]
|
||
if self.track_vcc:
|
||
self.last_vcc = pins[self.VCC_IDX]
|
||
self.rst_cand = None
|
||
self.rst_cand_sn = 0
|
||
self.vcc_cand = None
|
||
self.vcc_cand_sn = 0
|
||
if (self.track_rst and self.last_rst == 0 and self.state == 'DATA'
|
||
and self.bit_samples is None):
|
||
# Card is sensed in reset at capture start (RST primed LOW;
|
||
# the assertion edge is in the past -- e.g. the capture began
|
||
# during power-on). The card is inactive, so measuring the
|
||
# ETU now would lock a bogus period from pre-ATR power-up
|
||
# transitions on DATA. Expect the fresh ATR instead: re-arm
|
||
# the hunt at the spec-default rate and let FIND START wait
|
||
# for the card to power on (read_first_byte/wait_data_falling
|
||
# service RST/VCC events while DATA is idle). This is the
|
||
# "a mid-session capture may still begin with a reset" case.
|
||
self.state = 'FIND START'
|
||
self._atr_hunt_count = 0
|
||
self._pps_attempted = False
|
||
self.bit_samples = None
|
||
self._etu_confirm_count = 0
|
||
self._etu_fail_count = 0
|
||
self.clock_skip = 372
|
||
self._inverse_convention = False
|
||
self._clock_skip_confident = \
|
||
not (self.sample_as_clock or self.detect_clock)
|
||
self.log("RST low at start: expecting cold-boot ATR")
|
||
|
||
# State machine.
|
||
if self.state == 'FIND START':
|
||
# Find the first frame (sample-based, gating-immune) and hand it
|
||
# to the ATR hunter. RST/VCC edges are still serviced inside
|
||
# wait_data_falling via track_rst/track_vcc.
|
||
byte = self.read_first_byte()
|
||
self.peeked_byte = byte
|
||
self.peeked_samplenum = self.ss
|
||
self.handle_atr(self.wait({'skip': 0}))
|
||
elif self.state == 'DATA':
|
||
packet = [];
|
||
|
||
# Mid-session capture with an explicit protocol: there is no ATR to
|
||
# derive the bit period from, so recover it once from the live line
|
||
# and then fall through to the normal T=0/T=1 framing state machine
|
||
# below, which reassembles each full command+response exchange into
|
||
# a single GSMTAP APDU. (Emitting each idle-split burst verbatim
|
||
# produced fragments, because the card stops CLK during the
|
||
# turn-around and every exchange is split across idle gaps.)
|
||
#
|
||
# Also re-measure after an accepted PPS changes the bit rate:
|
||
# the old ETU no longer applies and the new one must be measured
|
||
# from the live DATA line.
|
||
if self.bit_samples is None or self._pps_speed_changed:
|
||
self._pps_speed_changed = False
|
||
# For the CLK-sync reader we also need clock_skip, which is
|
||
# unknown on a mid-session capture. Measure the CLK period
|
||
# (samples per CLK cycle) and the ETU (samples per bit), then
|
||
# derive clock_skip = ETU / CLK-period so the CLK-sync reader
|
||
# runs at the true rate. If the CLK period cannot be measured
|
||
# (fully gated CLK), clock_skip stays unconfident and the
|
||
# edge-list reader (bit_samples-driven) is used instead.
|
||
if self._samples_per_clock is None:
|
||
self._measure_clock_period()
|
||
self._measure_etu()
|
||
if self._eof:
|
||
return False
|
||
self._derive_clock_skip_from_etu()
|
||
return True
|
||
# ETU now known: run the standard framing below.
|
||
|
||
# After a desync, re-align on an inter-frame idle gap so the
|
||
# next exchange is read from a real command boundary.
|
||
if (self._resync):
|
||
self._resync = False
|
||
self.resync_idle()
|
||
|
||
# Opportunistic RST/VCC level check at packet boundary.
|
||
# Note: edges occurring entirely inside a packet transfer
|
||
# are only detected here as a net level change.
|
||
if (self.track_rst or self.track_vcc):
|
||
self.line_events(self.wait({'skip': 0}))
|
||
|
||
if not self.hasT0 and not self.hasT1:
|
||
bs = self.bit_samples or self._compute_bit_samples()
|
||
self.wait({'skip': int(bs)})
|
||
return True
|
||
|
||
firstByte = self.peek_byte();
|
||
if firstByte is None:
|
||
return True
|
||
if (firstByte == 0xFF): # PPS Request
|
||
# Only one PPS attempt is made after an ATR/warm reset.
|
||
# Idle 0xFF bytes after a failed/no-PPS scenario would
|
||
# otherwise trigger PPS detection in an infinite loop.
|
||
if not self._pps_attempted:
|
||
self.handle_pps();
|
||
else:
|
||
# Idle 0xFF noise after the PPS window has closed.
|
||
# Consume it and re-sync on the next real start bit.
|
||
self.read_byte()
|
||
self._resync = True
|
||
return True
|
||
elif (firstByte == 0x3b) and not self._atr_parsed: # Probably ATR (only if no ATR yet)
|
||
self.handle_atr(self.wait({'skip': 0}))
|
||
return True
|
||
elif bin(firstByte).count('1') >= 6:
|
||
# Idle-line noise: byte has 6+ bits set (all-1s with
|
||
# 0-2 bit errors). Re-sync instead of mis-parsing as
|
||
# a T=0 header (which would pack idle noise into
|
||
# garbage APDUs).
|
||
self._note_discard(firstByte, 'idle-line noise (6+ bits set)', idle=True)
|
||
self._consume_first()
|
||
self._resync = True
|
||
return True
|
||
elif not plausible_cla(firstByte):
|
||
# Non-standard CLA: byte is not a recognized ISO 7816
|
||
# class. Likely mis-framed data — attempt to resync.
|
||
self._note_discard(firstByte, 'suspicious CLA (not ISO 7816-4)')
|
||
self._consume_first()
|
||
self.log("Suspicious CLA 0x{:02x}, attempting resync".format(firstByte))
|
||
self._resync = True
|
||
return True
|
||
elif firstByte == 0x00 and self.zero_run >= 32:
|
||
# Stuck-low noise: I/O held low for many consecutive frames.
|
||
# Don't start a T=0 frame with leading zeros.
|
||
self._note_discard(firstByte, 'stuck-low noise (zero_run >= 32)', idle=True)
|
||
self._consume_first()
|
||
self._resync = True
|
||
return True
|
||
|
||
es = self.peeked_samplenum;
|
||
if (self.hasT0):
|
||
# Idle-line check before reading the command header.
|
||
# DATA idles HIGH in direct convention (0xFF) or LOW in
|
||
# inverse convention (0x00). If the first byte is idle,
|
||
# the card is not transmitting — skip it and let the RST
|
||
# handler re-arm ATR hunt.
|
||
idle_byte = 0xFF if not self._inverse_convention else 0x00
|
||
first = self.peek_byte()
|
||
if first is None:
|
||
return True
|
||
if first == idle_byte:
|
||
self._note_discard(first, 'idle byte during header wait', idle=True)
|
||
self._consume_first()
|
||
self._resync = True
|
||
return True
|
||
bClass = self.read_byte()
|
||
if bClass is None:
|
||
return True
|
||
bIns = self.read_byte()
|
||
if bIns is None:
|
||
return True
|
||
p1 = self.read_byte()
|
||
if p1 is None:
|
||
return True
|
||
p2 = self.read_byte()
|
||
if p2 is None:
|
||
return True
|
||
p3 = self.read_byte()
|
||
if p3 is None:
|
||
return True
|
||
packet.extend((bClass, bIns, p1, p2, p3))
|
||
# All-zero header is never a valid T=0 command.
|
||
if all(b == 0 for b in packet[:5]):
|
||
self.put(es, self.samplenum, self.out_ann,
|
||
[Ann.ANN_WARN,
|
||
["all-zero T=0 header (stuck low?)"]])
|
||
self._resync = True
|
||
return True
|
||
# T=0 header validation per ISO 7816-4 §12.1.1.
|
||
# Rejects noise headers that slip past the CLA filter.
|
||
# - INS=0x00 is reserved/invalid.
|
||
# - MSB nibble 0x6/0x9 conflicts with status words.
|
||
# - LSB must be 0 (all standard interindustry commands).
|
||
if (bIns == 0x00
|
||
or (bIns & 0xF0) == 0x60
|
||
or (bIns & 0xF0) == 0x90
|
||
or (bIns & 0x01) != 0):
|
||
# Byte-slip re-frame: the whole 5-byte window is a valid
|
||
# size but the INS is unusable, so the window is
|
||
# probably shifted by one (a stray leading byte from the
|
||
# previous exchange, e.g. a leftover SW2). Drop ONLY the
|
||
# leading byte and re-frame the remaining four so a real
|
||
# command that would otherwise be swept into a garbage
|
||
# header (or swallowed by a bogus P3) is recovered.
|
||
self._note_discard(bClass, 'invalid INS 0x%02x in header; re-framing'
|
||
% bIns, idle=False)
|
||
self.peeked_samplenum = es
|
||
self.peeked_byte = None
|
||
self._replay.appendleft(p3)
|
||
self._replay.appendleft(p2)
|
||
self._replay.appendleft(p1)
|
||
self._replay.appendleft(bIns)
|
||
return True
|
||
# T=0 procedure-byte transport (ISO 7816-3 §10.3.3/10.3.4).
|
||
# Reassemble the exchange to ISO 7816-4 form (C-APDU = header
|
||
# + command data, R-APDU = response data + SW1 SW2) into one
|
||
# event. Procedure bytes (ACK == INS, NULL 0x60, 9E/9F) are
|
||
# framing and omitted.
|
||
got_sw = False
|
||
if self._edge_read:
|
||
# Mid-session (gated-CLK) path.
|
||
#
|
||
# T=0 case 3/4 commands carry Lc (= P3) command-data bytes
|
||
# on the wire IMMEDIATELY after the 5-byte header, BEFORE
|
||
# the card's first procedure byte. The original loop (which
|
||
# treats the first post-header byte as a procedure byte)
|
||
# therefore mis-frames them. We distinguish the cases by
|
||
# the card's first procedure byte:
|
||
# * ACK (== INS or ~INS) right after the header -> case 2/4, the
|
||
# terminal sent no command data and the card is
|
||
# acknowledging; read P3 response bytes (+SW), or, for
|
||
# Le == 0 (GET RESPONSE), read until the SW.
|
||
# * any other byte -> case 3/4, it
|
||
# is terminal command data; read P3 command-data bytes,
|
||
# then the card's procedure byte (ACK/SW/9x).
|
||
pb0 = self.read_byte()
|
||
if pb0 is None:
|
||
return True
|
||
# Skip NULL (0x60) procedure bytes and turnaround idle
|
||
# bytes (0xFF): the card may send NULLs while processing
|
||
# before the ACK, or the bus may idle at xFF during
|
||
# turnaround on the bidirectional DATA line. Without
|
||
# this the NULLs / idle bytes are mis-identified as
|
||
# case-3/4 command data.
|
||
for _ in range(MAX_TPDU_LEN):
|
||
if pb0 != 0x60 and pb0 != 0xFF:
|
||
break
|
||
self._note_discard(pb0,
|
||
'idle/NULL byte before procedure byte',
|
||
idle=True)
|
||
pb0 = self.read_byte()
|
||
if pb0 is None:
|
||
return True
|
||
if (pb0 == bIns or pb0 == (bIns ^ 0xFF)):
|
||
# ACK (full match or ~INS): case 2/4 (response data
|
||
# follows). Read exactly P3 response bytes, then
|
||
# scan for SW1 SW2, skipping any turnaround idle
|
||
# bytes (e.g. 0xFF) between data and SW.
|
||
for _ in range(p3):
|
||
b = self.read_byte()
|
||
if b is None:
|
||
return True
|
||
packet.append(b)
|
||
sw1 = None
|
||
for _ in range(MAX_TPDU_LEN):
|
||
sw1 = self.read_byte()
|
||
if sw1 is None:
|
||
return True
|
||
if ((sw1 & 0xF0) == 0x60
|
||
or (sw1 & 0xF0) == 0x90):
|
||
break
|
||
self._note_discard(sw1,
|
||
'non-SW byte before status word '
|
||
'(P3-bounded response)',
|
||
idle=(sw1 in (0x00, 0xFF)))
|
||
if sw1 is None:
|
||
return True
|
||
sw2 = self.read_byte()
|
||
if sw2 is None:
|
||
return True
|
||
packet.append(sw1) # SW1
|
||
packet.append(sw2) # SW2
|
||
got_sw = True
|
||
else:
|
||
# Case 2/3/4: determine data direction from P3 and
|
||
# the first post-header byte.
|
||
packet.append(pb0)
|
||
if (p3 == 0):
|
||
# P3=0 (Le=0): no command data. Card sends
|
||
# response data + SW, or SW directly (no ACK).
|
||
if ((pb0 & 0xF0) == 0x60
|
||
or (pb0 & 0xF0) == 0x90):
|
||
# pb0 is SW1: card sent SW directly.
|
||
sw2 = self.read_byte()
|
||
if sw2 is None:
|
||
return True
|
||
packet.append(sw2)
|
||
got_sw = True
|
||
else:
|
||
# pb0 is response data: read until SW,
|
||
# skipping turnaround idle bytes.
|
||
for _ in range(MAX_TPDU_LEN):
|
||
b = self.read_byte()
|
||
if b is None:
|
||
return True
|
||
if ((b & 0xF0) == 0x60
|
||
or (b & 0xF0) == 0x90):
|
||
sw2 = self.read_byte()
|
||
if sw2 is None:
|
||
return True
|
||
packet.append(b)
|
||
packet.append(sw2)
|
||
got_sw = True
|
||
break
|
||
self._note_discard(b,
|
||
'non-SW byte in P3=0 '
|
||
'response',
|
||
idle=(b in (0x00, 0xFF)))
|
||
else:
|
||
# Case 3/4: P3 command-data bytes, then
|
||
# procedure byte.
|
||
for _ in range(max(p3 - 1, 0)):
|
||
b = self.read_byte()
|
||
if b is None:
|
||
return True
|
||
packet.append(b)
|
||
for _proc in range(32):
|
||
pb = self.read_byte()
|
||
if pb is None:
|
||
return True
|
||
if (pb == bIns or pb == (bIns ^ 0xFF)):
|
||
# ACK (full match / ~INS): case 4 -> read response until SW,
|
||
# skipping turnaround idle bytes.
|
||
for _ in range(MAX_TPDU_LEN):
|
||
b = self.read_byte()
|
||
if b is None:
|
||
return True
|
||
if ((b & 0xF0) == 0x60
|
||
or (b & 0xF0) == 0x90):
|
||
sw2 = self.read_byte()
|
||
if sw2 is None:
|
||
return True
|
||
packet.append(b)
|
||
packet.append(sw2)
|
||
got_sw = True
|
||
break
|
||
self._note_discard(b,
|
||
'non-SW byte in case-4 '
|
||
'response',
|
||
idle=(b in (0x00, 0xFF)))
|
||
break
|
||
elif (pb == (bIns ^ 0x01)
|
||
or pb == ((bIns ^ 0xFF) ^ 0x01)):
|
||
# Single-byte match (INS^0x01 / ~INS^0x01):
|
||
# read 1 byte, continue handshake.
|
||
b = self.read_byte()
|
||
if b is None:
|
||
return True
|
||
packet.append(b)
|
||
continue
|
||
elif (pb == 0x60 or pb == 0xFF):
|
||
# NULL (0x60): card busy. Turnaround
|
||
# idle (0xFF): bidirectional bus turnaround
|
||
# between terminal command data and the
|
||
# card's procedure byte. Both: consume and
|
||
# wait for next procedure byte.
|
||
continue
|
||
elif (pb == 0x9e or pb == 0x9f):
|
||
# Extended-length procedure byte: L
|
||
# response data bytes follow, then SW.
|
||
L = self.read_byte()
|
||
if L is None:
|
||
return True
|
||
for _ in range(L):
|
||
b = self.read_byte()
|
||
if b is None:
|
||
return True
|
||
packet.append(b)
|
||
continue
|
||
elif ((pb & 0xF0) == 0x60
|
||
or (pb & 0xF0) == 0x90):
|
||
# SW1: case 3, no response data.
|
||
sw2 = self.read_byte()
|
||
if sw2 is None:
|
||
return True
|
||
packet.append(pb)
|
||
packet.append(sw2)
|
||
got_sw = True
|
||
break
|
||
else:
|
||
# Unexpected byte: scan for SW,
|
||
# skip non-SW bytes (e.g. 0xFF
|
||
# turnaround artifacts).
|
||
for _ in range(MAX_TPDU_LEN):
|
||
if ((pb & 0xF0) == 0x60
|
||
or (pb & 0xF0) == 0x90):
|
||
sw2 = self.read_byte()
|
||
if sw2 is None:
|
||
return True
|
||
packet.append(pb)
|
||
packet.append(sw2)
|
||
got_sw = True
|
||
break
|
||
self._note_discard(pb,
|
||
'non-SW byte during '
|
||
'unexpected-byte scan',
|
||
idle=(pb in (0x00, 0xFF)))
|
||
pb = self.read_byte()
|
||
if pb is None:
|
||
return True
|
||
break
|
||
else:
|
||
# ATR-based captures (test_8 / test_7): keep the original
|
||
# procedure loop unchanged.
|
||
data_read = False
|
||
for _proc in range(8):
|
||
pb = self.read_byte()
|
||
if pb is None:
|
||
break
|
||
if (pb == bIns or pb == (bIns ^ 0xFF)) and not data_read:
|
||
# ACK (full match / ~INS): the P3 bytes follow on
|
||
# the wire (Lc command data for case 3, Le response
|
||
# data for case 2).
|
||
data_read = True
|
||
for _ in range(p3):
|
||
b = self.read_byte()
|
||
if b is None:
|
||
break
|
||
packet.append(b)
|
||
elif (pb == (bIns ^ 0x01)
|
||
or pb == ((bIns ^ 0xFF) ^ 0x01)):
|
||
# Single-byte match (INS^0x01 / ~INS^0x01):
|
||
# read 1 byte, continue handshake.
|
||
b = self.read_byte()
|
||
if b is None:
|
||
break
|
||
packet.append(b)
|
||
elif (pb == 0x60):
|
||
# NULL: card busy; wait for the next procedure byte.
|
||
pass
|
||
elif (pb == 0x9e or pb == 0x9f):
|
||
# T=0 extended-length procedure byte: the next byte
|
||
# L is the number of response data bytes the card
|
||
# now sends, followed by the status word.
|
||
L = self.read_byte()
|
||
if L is None:
|
||
break
|
||
for _ in range(L):
|
||
b = self.read_byte()
|
||
if b is None:
|
||
break
|
||
packet.append(b)
|
||
elif ((pb & 0xF0) == 0x60 or (pb & 0xF0) == 0x90):
|
||
# SW1: append SW2; the exchange is complete.
|
||
sw2 = self.read_byte()
|
||
if sw2 is None:
|
||
break
|
||
packet.append(pb)
|
||
packet.append(sw2)
|
||
got_sw = True
|
||
break
|
||
else:
|
||
# Mis-framed procedure byte: stop reading. The
|
||
# partial packet is flagged desynced downstream and
|
||
# the decoder re-aligns on the next idle gap.
|
||
self.put(es, self.samplenum, self.out_ann,
|
||
[0, ["INVALID Procedure Byte"]])
|
||
self.log("INVALID Procedure Byte", hex(pb))
|
||
self._resync = True
|
||
break
|
||
self.put(es, self.samplenum, self.out_ann, [4, ["T=0"]])
|
||
if (got_sw and len(packet) >= 7):
|
||
self.put(es, self.samplenum, self.out_ann, [9, ["APDU", "APDU cls={cls:02x} ins={ins:02x}".format(cls=bClass,ins=bIns), "APDU cls={cls:02x} ins={ins:02x} p1={p1:02x} p2={p2:02x} p3={p3:02x} len={len} status={sw1:02x}{sw2:02x}".format(cls=bClass,ins=bIns,p1=p1,p2=p2,p3=p3,len=p3,sw1=packet[-2],sw2=packet[-1])]])
|
||
elif (self.hasT1):
|
||
isIBlock,packet = self.t1_parse_block(es)
|
||
if (isIBlock):
|
||
for _ in range(32): # bounded chain follow-up
|
||
isIBlock,packet2 = self.t1_parse_block(es)
|
||
if (isIBlock):
|
||
packet = packet + packet2
|
||
break
|
||
self.put(es, self.samplenum, self.out_ann, [4, ["T=1", "T=1 (reassembled)"]])
|
||
if (len(packet) >= 8):
|
||
self.put(es, self.samplenum, self.out_ann, [9, ["APDU", "APDU cls={cls:02x} ins={ins:02x}".format(cls=packet[0],ins=packet[1]), "APDU cls={cls:02x} ins={ins:02x} p1={p1:02x} p2={p2:02x} p3={p3:02x} len={len} status={sw1:02x}{sw2:02x}".format(cls=packet[0],ins=packet[1],p1=packet[2],p2=packet[3],p3=packet[4],len=len(packet) - 7,sw1=packet[-2],sw2=packet[-1])]])
|
||
|
||
|
||
if (len(packet) > 0):
|
||
# All-zero first: an I/O-held-low stretch is idle noise,
|
||
# not a TPDU -- suppress it entirely (and re-sync later).
|
||
if (all(b == 0 for b in packet)):
|
||
self._idle_low += 1
|
||
if self._idle_low == 1 or self._idle_low % 256 == 0:
|
||
self.put(es, self.samplenum, self.out_ann,
|
||
[Ann.ANN_WARN,
|
||
["line idle-low (all-zero reads suppressed)"]])
|
||
self._resync = True
|
||
elif is_desynced(packet):
|
||
# Keep the bytes but flag them via GSMTAP_FLAG_BAD_FCS
|
||
# (res byte) so downstream tools can tell desynced
|
||
# TPDUs apart; sub_type stays 0x00. Then re-align.
|
||
self.put(es, self.samplenum, self.out_ann,
|
||
[Ann.ANN_WARN,
|
||
["desynced APDU (flagged BAD_FCS)"]])
|
||
self.emit_packet(GSMTAP_SIM_APDU, bytes(packet),
|
||
es, self.samplenum,
|
||
flags=GSMTAP_FLAG_BAD_FCS)
|
||
self._resync = True
|
||
else:
|
||
emb = embedded_exchanges(packet)
|
||
if emb:
|
||
# CONCAT: a byte-drop mis-frame swallowed several
|
||
# real exchanges into this structurally valid
|
||
# packet (their headers survive in the data region).
|
||
# Flag BAD_FCS + log so downstream tools can
|
||
# split/reject the wrong-APDU group.
|
||
self.put(es, self.samplenum, self.out_ann,
|
||
[Ann.ANN_WARN,
|
||
["CONCAT APDU (embedded exchange(s))"]])
|
||
self.log("CONCAT APDU: %d embedded exchange(s) at %r; packet %s"
|
||
% (len(emb),
|
||
[(s, e) for s, e in emb],
|
||
codecs.encode(bytes(packet), 'hex')))
|
||
self.emit_packet(GSMTAP_SIM_APDU, bytes(packet),
|
||
es, self.samplenum,
|
||
flags=GSMTAP_FLAG_BAD_FCS)
|
||
else:
|
||
self._idle_low = 0
|
||
self.emit_packet(GSMTAP_SIM_APDU, bytes(packet),
|
||
es, self.samplenum)
|
||
|
||
self.log("PACKETEND", codecs.encode(bytes(packet), 'hex'))
|
||
else:
|
||
return False
|
||
return True
|
||
|
||
def decode(self):
|
||
try:
|
||
self.write_pcap_header();
|
||
failures = 0
|
||
while True:
|
||
# Crash protection: garbage from a noisy live signal must
|
||
# never kill the whole sigrok-cli process. Log, warn and
|
||
# resync. End-of-sample-stream must propagate for a clean
|
||
# exit: older libsigrokdecode raises SystemError from
|
||
# put()/wait(), newer versions raise EOFError('samples
|
||
# exhausted') from wait() -- libsigrokdecode accepts either
|
||
# as normal decode() termination. KeyboardInterrupt
|
||
# (Ctrl+C) likewise passes through.
|
||
try:
|
||
if (not self.decode_step()):
|
||
break
|
||
failures = 0
|
||
except (SystemError, EOFError):
|
||
raise
|
||
except Exception as e:
|
||
traceback.print_exc(file=sys.stderr)
|
||
self.log("DECODE ERROR:", repr(e), "- resyncing")
|
||
try:
|
||
wss = max(self.ss, 0)
|
||
wes = max(self.es, wss + 1)
|
||
self.put(wss, wes, self.out_ann,
|
||
[Ann.ANN_WARN, ["decode error, resynced"]])
|
||
except (SystemError, EOFError):
|
||
raise
|
||
except Exception:
|
||
pass
|
||
self.state = 'DATA'
|
||
failures += 1
|
||
if (failures >= 50):
|
||
self.log("decode failed 50 times in a row, stopping")
|
||
break
|
||
finally:
|
||
self.finish();
|