diff --git a/README.md b/README.md index 4ba187e..4b95d8b 100644 --- a/README.md +++ b/README.md @@ -130,14 +130,20 @@ tunable via `--log-interval SECONDS`. The GSMTAP listener also accepts the **sigrok-iso7816-stream** decoder (https://github.com/anttro/sigrok_iso7816_stream) — an FX2-logic-analyzer passive SIM sniffer that emits the same GSMTAP-SIM format on UDP 4729. Its -custom sub-types for RST/VCC line events (`0x10`/`0x11`, added for +standard PPS request/response sub-types (`0x02`/`0x03`) are decoded as PPS, +and its custom sub-types for RST/VCC line events (`0x10`/`0x11`, added for compatibility with that project) are decoded as `RESET ASSERTED/DE-ASSERTED` and `VCC ON/OFF` timeline markers, and a reset-assert or power-removal -correctly invalidates the file-selection tracking. GSMTAP packets carrying -the `GSMTAP_FLAG_BAD_FCS` desync flag (set by that decoder on mis-framed -exchanges) are surfaced as a flagged/desynced entry in the capture timeline, -so such artefacts are easy to spot when reviewing a capture rather than -silently altering the decode. +correctly invalidates the file-selection tracking. When an RST event carries +the measured CLK frequency (decoder v1.9.0+, optional payload extension), the +following ATR is annotated with the CLK frequency, data rate and ETU derived +from its TA1 Fi/Di (`data_rate = clk_hz × D / F`); the measurement is +consumed by the ATR so a later ATR without a reset gets no stale rate (a +late-arriving measurement is backfilled onto the ATR it belongs to). GSMTAP +packets carrying the `GSMTAP_FLAG_BAD_FCS` desync flag (set by that decoder +on mis-framed exchanges) are surfaced as a flagged/desynced entry in the +capture timeline, so such artefacts are easy to spot when reviewing a capture +rather than silently altering the decode. Server options: diff --git a/README_RUS.md b/README_RUS.md index 60836e6..4db8dd3 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -137,15 +137,21 @@ heartbeat `Capture alive: session=N messages=M bytes=B dropped=D` GSMTAP-листенер также принимает декодер **sigrok-iso7816-stream** (https://github.com/anttro/sigrok_iso7816_stream) — пассивный SIM-сниффер на базе FX2-логического анализатора, выдающий тот же формат GSMTAP-SIM -на UDP 4729. Его пользовательские подтипы для событий линий RST/VCC -(`0x10`/`0x11`, добавлены для совместимости с тем проектом) декодируются -как временные маркеры `RESET ASSERTED/DE-ASSERTED` и `VCC ON/OFF`, -а сброс или пропадание питания корректно сбрасывает отслеживание -выбранного файла. GSMTAP-пакеты с флагом десинхронизации -`GSMTAP_FLAG_BAD_FCS` (устанавливается тем декодером при нарушении -кадрирования) отображаются в таймлайне как помеченная/десинхронизированная -запись, чтобы такие артефакты было легко заметить при разборе, а не молча -искажали декодирование. +на UDP 4729. Его стандартные подтипы PPS-запроса/ответа (`0x02`/`0x03`) +декодируются как PPS, а пользовательские подтипы для событий линий RST/VCC +(`0x10`/`0x11`, добавлены для совместимости с тем проектом) — как временные +маркеры `RESET ASSERTED/DE-ASSERTED` и `VCC ON/OFF`; сброс или пропадание +питания корректно сбрасывает отслеживание выбранного файла. Когда событие +RST несёт измеренную частоту CLK (декодер v1.9.0+, опциональное расширение +полезной нагрузки), следующий ATR получает частоту CLK, скорость передачи +данных и ETU, вычисленные из его TA1 Fi/Di +(`data_rate = clk_hz × D / F`); измерение потребляется ATR, поэтому +следующий ATR без сброса не получит устаревшую скорость (поздно пришедшее +измерение восстанавливается для «своего» ATR). GSMTAP-пакеты с флагом +десинхронизации `GSMTAP_FLAG_BAD_FCS` (устанавливается тем декодером при +нарушении кадрирования) отображаются в таймлайне как +помеченная/десинхронизированная запись, чтобы такие артефакты было легко +заметить при разборе, а не молча искажали декодирование. Параметры сервера: diff --git a/frontend/index.html b/frontend/index.html index 9ff7174..b20cb7a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1512,6 +1512,9 @@ function atrPpsRows(decoded) { rows.push([ifc.name, ifaceValue(ifc)]); } } + if (decoded.clk_hz) rows.push(['CLK', (decoded.clk_hz / 1e6).toFixed(3) + ' MHz']); + if (decoded.data_rate) rows.push(['Data rate', decoded.data_rate + ' bit/s']); + if (decoded.etu_us) rows.push(['ETU', decoded.etu_us + ' µs']); if (decoded.historical) { const h = decoded.historical; let v = h.category || ''; @@ -1580,6 +1583,7 @@ function showDetail(msg, entry) { + ${decoded.clk_hz ? `` : ''} ${decoded.raw ? `` : ''}
Event${evName}
Level${decoded.level || '?'}
CLK${(decoded.clk_hz / 1e6).toFixed(3)} MHz
Raw${decoded.raw}
`; @@ -2625,7 +2629,7 @@ function onFilterChange() { } // ──────────────────── Versioning ──────────────────── -const PWA_VERSION = '1.22.17'; +const PWA_VERSION = '1.22.18'; const MIN_SERVER_VERSION = '1.0.0'; function parseVersion(v) { diff --git a/frontend/sw.js b/frontend/sw.js index 4158c6a..f9ff5d5 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simtrace-analyser-v49'; +const CACHE = 'simtrace-analyser-v50'; const PRECACHE = [ 'index.html', diff --git a/pyproject.toml b/pyproject.toml index e6a4059..6e109fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend" [project] name = "simtrace2-pysniff" -version = "1.22.17" +version = "1.22.18" description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "GPL-2.0-or-later"} diff --git a/simtrace2_pysniff/__init__.py b/simtrace2_pysniff/__init__.py index 567bd28..b497707 100644 --- a/simtrace2_pysniff/__init__.py +++ b/simtrace2_pysniff/__init__.py @@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver from .pcap import PcapWriter from .dump import FileDumper, format_message -__version__ = '1.22.17' +__version__ = '1.22.18' __all__ = [ 'SniffSession', 'DeviceDisconnected', diff --git a/simtrace2_pysniff/server/capture.py b/simtrace2_pysniff/server/capture.py index 6d0b5ae..6c38a5f 100644 --- a/simtrace2_pysniff/server/capture.py +++ b/simtrace2_pysniff/server/capture.py @@ -7,6 +7,7 @@ import time import traceback from ..gsmtap import (GsmtapReceiver, GSMTAP_SIM_ATR, + GSMTAP_SIM_PPS_REQ, GSMTAP_SIM_PPS_RSP, GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT) @@ -22,11 +23,14 @@ def _log(msg): def gsmtap_msg_type(sub_type): """Map a GSMTAP-SIM sub_type to a stored message type. - Custom sigrok-iso7816-stream line events (0x10/0x11) become 'rst'/'vcc'; - everything except ATR defaults to 'tpdu'. + PPS request/response (0x02/0x03, standard GSMTAP) become 'pps'; custom + sigrok-iso7816-stream line events (0x10/0x11) become 'rst'/'vcc'; + everything else defaults to 'tpdu'. """ return { GSMTAP_SIM_ATR: 'atr', + GSMTAP_SIM_PPS_REQ: 'pps', + GSMTAP_SIM_PPS_RSP: 'pps', GSMTAP_SIM_RST_EVENT: 'rst', GSMTAP_SIM_VCC_EVENT: 'vcc', }.get(sub_type, 'tpdu') diff --git a/simtrace2_pysniff/server/database.py b/simtrace2_pysniff/server/database.py index 38a3059..df271f6 100644 --- a/simtrace2_pysniff/server/database.py +++ b/simtrace2_pysniff/server/database.py @@ -179,17 +179,18 @@ class Database: 'WHERE session_id=? ORDER BY elapsed, id LIMIT ? OFFSET ?', (session_id, limit, offset)).fetchall() initial_states = None + clk_hz = None if offset > 0 and rows: - initial_states = self._replay_context(session_id, rows[0][0]) - return self._decode_rows(rows, initial_states=initial_states) + initial_states, clk_hz = self._replay_context(session_id, rows[0][0]) + return self._decode_rows(rows, initial_states=initial_states, clk_hz=clk_hz) def get_messages_after(self, session_id, after_id): rows = self._conn.execute( 'SELECT id, session_id, elapsed, type, data, flags FROM messages ' 'WHERE session_id=? AND id > ? ORDER BY elapsed, id', (session_id, after_id)).fetchall() - initial_states = self._replay_context(session_id, after_id + 1) - return self._decode_rows(rows, initial_states=initial_states) + initial_states, clk_hz = self._replay_context(session_id, after_id + 1) + return self._decode_rows(rows, initial_states=initial_states, clk_hz=clk_hz) def get_messages_raw(self, session_id): rows = self._conn.execute( @@ -226,7 +227,7 @@ class Database: (session_id,)).fetchall() return [{'type': r[0], 'count': r[1]} for r in rows] - def _row_to_message(self, row, prev=None): + def _row_to_message(self, row, prev=None, clk_hz=None): data_blob = row[4] flags = row[5] msg = { @@ -240,7 +241,8 @@ class Database: if row[3] in ('tpdu', 'change', 'fidi', 'atr', 'pps', 'rst', 'vcc'): try: from .decode import decode_sniff_msg - msg['decoded'] = decode_sniff_msg(data_blob, row[3], flags, prev=prev) + msg['decoded'] = decode_sniff_msg(data_blob, row[3], flags, + prev=prev, clk_hz=clk_hz) except Exception as e: import sys print(f'Decode error for msg {row[0]} ({row[3]}, {len(data_blob)} bytes): {e}', @@ -260,16 +262,27 @@ class Database: def _replay_context(self, session_id, before_id): """Replay (bounded) messages before *before_id* to recover per-channel - selection, DF, and SFI map.""" + selection, DF, SFI map, and the last RST-reported CLK frequency. + + Returns ``(states, clk_hz)``.""" from .decode import decode_sniff_msg rows = self._conn.execute( "SELECT data, type FROM messages WHERE session_id=? AND id < ? " - "AND type IN ('tpdu','atr','change','gap') ORDER BY id DESC LIMIT 500", + "AND type IN ('tpdu','atr','change','gap','rst','vcc') " + "ORDER BY id DESC LIMIT 500", (session_id, before_id)).fetchall() states = {} + clk_hz = None for data, typ in reversed(rows): if typ in ('atr', 'change', 'gap'): states = {} + # An ATR consumes the CLK rate; a replay starts fresh after it. + clk_hz = None + continue + if typ in ('rst', 'vcc'): + d = decode_sniff_msg(data, typ, 0) + if d and d.get('clk_hz'): + clk_hz = d['clk_hz'] continue channel = data[0] & 0x03 if data and len(data) >= 1 else 0 st = self._channel_state(states, channel) @@ -282,7 +295,7 @@ class Database: continue st['sel'], st['df'], st['sfi_map'] = self._advance_state(d, st['sel'], st['df'], st['sfi_map']) st['prev'] = self._context_from_decoded(d) - return states + return states, clk_hz def _advance_state(self, d, sel, df, sfi_map): """Update (sel, df, sfi_map) for one decoded TPDU.""" @@ -350,9 +363,12 @@ class Database: return new return sel - def _decode_rows(self, rows, initial_states=None): + def _decode_rows(self, rows, initial_states=None, clk_hz=None): + from .decode import atr_rate_fields msgs = [] states = dict(initial_states or {}) + last_atr_i = None # index of the most recent ATR in *msgs* + last_atr_rated = False # whether that ATR already carries a data rate for row in rows: channel = 0 if row[3] == 'tpdu' and row[4] and len(row[4]) >= 5: @@ -362,13 +378,18 @@ class Database: ctx['sel'] = st['sel'] ctx['df'] = st['df'] ctx['sfi_map'] = st['sfi_map'] - msg = self._row_to_message(row, prev=ctx) + msg = self._row_to_message(row, prev=ctx, clk_hz=clk_hz) d = msg.get('decoded') if msg['type'] == 'tpdu' and d and d.get('ins_hex'): st['sel'], st['df'], st['sfi_map'] = self._advance_state(d, st['sel'], st['df'], st['sfi_map']) st['prev'] = self._context_from_decoded(d) elif msg['type'] == 'atr': states = {} + # The CLK rate is consumed by the ATR it precedes: an ATR + # without a preceding RST event must not inherit a stale rate. + last_atr_i = len(msgs) + last_atr_rated = bool(d and d.get('data_rate')) + clk_hz = None elif msg['type'] == 'gap': # Device disconnected and reconnected — messages may have been # missed, so the selection is uncertain. @@ -389,6 +410,17 @@ class Database: # removal destroys the card's selection state. De-assert / # power-on are markers only (the following ATR resets state). data = row[4] if len(row) > 4 and row[4] else b'' + if d and d.get('clk_hz'): + if (last_atr_i is not None and not last_atr_rated + and msgs[last_atr_i].get('decoded')): + # Ordering caveat: a short ATR can be emitted before + # the RST event that measured its CLK rate — backfill + # the rate onto that ATR. + msgs[last_atr_i]['decoded'].update( + atr_rate_fields(d['clk_hz'], msgs[last_atr_i]['decoded'])) + last_atr_rated = True + else: + clk_hz = d['clk_hz'] if msg['type'] == 'rst': if data and data[0] == 1: # asserted states = {} diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py index b0f6f74..213c1a5 100644 --- a/simtrace2_pysniff/server/decode.py +++ b/simtrace2_pysniff/server/decode.py @@ -3992,6 +3992,11 @@ DATA_FLAGS = { 1 << 10: 'parity error', } +# sigrok-iso7816-stream line-event payload flags (payload[2] of RST/VCC, +# decoder v1.9.0+): bit 0 set → a big-endian uint32 CLK frequency in Hz +# follows at bytes 3..6. +LINE_EVENT_FLAG_CLK_HZ = 0x01 + def _flag_names(flags, mapping): names = [] @@ -4013,11 +4018,13 @@ def decode_change(flags): def decode_line_event(raw_data, kind): """Decode a sigrok-iso7816-stream RST/VCC line event (GSMTAP sub_type - 0x10/0x11): payload [direction, level, reserved]. + 0x10/0x11): payload [direction, level, flags, clk_hz_be32?]. direction — RST: 1 = asserted (high→low), 0 = de-asserted (low→high); VCC: 1 = power applied (low→high), 0 = removed (high→low). level — resulting line level: 0 = low, 1 = high. + flags — bit 0 (LINE_EVENT_FLAG_CLK_HZ) set when a CLK frequency follows + (decoder v1.9.0+); bytes 3..6 are then a big-endian uint32 in Hz. ``event`` is the canonical value; ``label`` is the human-facing summary (e.g. RST de-asserted → "ATR STARTS", VCC applied → @@ -4041,6 +4048,8 @@ def decode_line_event(raw_data, kind): result['event'] = 'power removed' result['label'] = 'VCC OFF (power-down)' result['level'] = 'high' if level else 'low' + if (len(raw_data) >= 7 and raw_data[2] & LINE_EVENT_FLAG_CLK_HZ): + result['clk_hz'] = int.from_bytes(raw_data[3:7], 'big') result['raw'] = raw_data.hex().upper() return result @@ -4126,6 +4135,25 @@ def _fidi_dict(fi, di): return entry +def atr_rate_fields(clk_hz, decoded): + """Derive the CLK frequency, data rate and ETU for a decoded ATR. + + The Fi/Di communicated by TA1 (ISO 7816-3 §8.3) give the bit duration: + ``data_rate = clk_hz × D / F`` and ``ETU = F / (clk_hz × D)``. When TA1 + is absent the interface defaults (F=372, D=1) apply. + """ + f, d = 372, 1 + for ifc in (decoded.get('interface') or []): + if ifc.get('name') == 'TA1' and 'f' in ifc and 'd' in ifc: + f, d = ifc['f'], ifc['d'] + break + return { + 'clk_hz': clk_hz, + 'data_rate': int(round(clk_hz * d / f)), + 'etu_us': round(f / (clk_hz * d) * 1e6, 1), + } + + def _decode_historical(hist): """Decode ATR historical bytes (ISO 7816-4 §8.1.1).""" result = {'raw': hist.hex().upper()} @@ -4149,8 +4177,13 @@ def _decode_historical(hist): return result -def _decode_atr(data): - """Decode an Answer-To-Reset (ISO 7816-3 §8) into a structured dict.""" +def _decode_atr(data, clk_hz=None): + """Decode an Answer-To-Reset (ISO 7816-3 §8) into a structured dict. + + *clk_hz* — when the sigrok-iso7816-stream decoder reported a CLK + frequency on a preceding RST event, the ATR gains ``clk_hz``, + ``data_rate`` and ``etu_us`` fields derived from TA1 Fi/Di. + """ if not data: return {'type': 'atr', 'raw': ''} ts = data[0] @@ -4169,6 +4202,8 @@ def _decode_atr(data): 'raw': raw, } if not body: + if clk_hz: + result.update(atr_rate_fields(clk_hz, result)) return result t0 = body[0] @@ -4261,6 +4296,9 @@ def _decode_atr(data): result['tck'] = f'{tck:02X}' result['tck_valid'] = check == tck + if clk_hz: + result.update(atr_rate_fields(clk_hz, result)) + return result @@ -4304,14 +4342,16 @@ def _decode_pps(data): return result -def decode_sniff_msg(raw_data, msg_type, flags=0, prev=None): +def decode_sniff_msg(raw_data, msg_type, flags=0, prev=None, clk_hz=None): """Decode a raw sniff message of the given type. Returns a structured dict with at least a 'type' key, or None if the message type has no structured decode. *prev* optionally carries the previous TPDU's decoded context for - resolving GET RESPONSE. + resolving GET RESPONSE. *clk_hz* is the CLK frequency reported by a + preceding RST event (sigrok-iso7816-stream v1.9.0+), used to derive + the data rate of an ATR. """ if msg_type == 'tpdu' and raw_data: result = decode_message(raw_data, prev=prev) @@ -4331,7 +4371,7 @@ def decode_sniff_msg(raw_data, msg_type, flags=0, prev=None): if msg_type == 'fidi' and raw_data: return decode_fidi(raw_data) if msg_type == 'atr': - result = _decode_atr(raw_data) + result = _decode_atr(raw_data, clk_hz=clk_hz) if result is not None: errs = (decode_data_flags(flags) or []) + (gsmtap_flag_names(flags) or []) if errs: diff --git a/tests/test_capture.py b/tests/test_capture.py index b5b7bdc..74868eb 100644 --- a/tests/test_capture.py +++ b/tests/test_capture.py @@ -19,7 +19,8 @@ class TestGsmtapSubtypeMapping(unittest.TestCase): def test_standard_subtypes(self): self.assertEqual(gsmtap_msg_type(0x00), 'tpdu') self.assertEqual(gsmtap_msg_type(0x01), 'atr') - self.assertEqual(gsmtap_msg_type(0x02), 'tpdu') # PPS combined + self.assertEqual(gsmtap_msg_type(0x02), 'pps') # PPS request + self.assertEqual(gsmtap_msg_type(0x03), 'pps') # PPS response class _FakeBackend: diff --git a/tests/test_decode.py b/tests/test_decode.py index f19a9f4..33606aa 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -871,6 +871,35 @@ class TestAtrPps(unittest.TestCase): self.assertEqual(r['historical_len'], 2) self.assertNotIn('tck', r) + def test_atr_data_rate_from_clk(self): + from simtrace2_pysniff.server.decode import _decode_atr + # TA1=11 → F=372, D=1; CLK 3.579545 MHz → 9622 bit/s, ETU 103.9 µs + r = _decode_atr(bytes.fromhex('3B12110000'), clk_hz=3579545) + self.assertEqual(r['clk_hz'], 3579545) + self.assertEqual(r['data_rate'], 9622) + self.assertEqual(r['etu_us'], 103.9) + + def test_atr_data_rate_uses_ta1_fidi(self): + from simtrace2_pysniff.server.decode import _decode_atr + # TA1=95 → F=512, D=16; CLK 4 MHz → 125000 bit/s, ETU 8.0 µs + r = _decode_atr(bytes.fromhex('3B1095'), clk_hz=4000000) + self.assertEqual(r['data_rate'], 125000) + self.assertEqual(r['etu_us'], 8.0) + + def test_atr_data_rate_defaults_without_ta1(self): + from simtrace2_pysniff.server.decode import _decode_atr + # No TA1 → default F=372, D=1 + r = _decode_atr(bytes.fromhex('3B00'), clk_hz=4000000) + self.assertEqual(r['data_rate'], 10753) + self.assertEqual(r['etu_us'], 93.0) + + def test_atr_no_clk_no_rate(self): + from simtrace2_pysniff.server.decode import _decode_atr + r = _decode_atr(bytes.fromhex('3B12110000')) + self.assertNotIn('clk_hz', r) + self.assertNotIn('data_rate', r) + self.assertNotIn('etu_us', r) + def test_atr_td1_t1_has_tck(self): from simtrace2_pysniff.server.decode import _decode_atr # 3B 80 01 80 31 80 66 ... : construct T=1 with TCK @@ -2431,6 +2460,55 @@ class TestLineEvents(unittest.TestCase): self.assertEqual(r['type'], 'rst') self.assertNotIn('event', r) + def test_rst_clk_extension(self): + # v1.9.0 payload: direction, level, flags(bit0=CLK), clk_hz_be32. + r = self._dec('rst', '000101003d0900') # 4 MHz + self.assertEqual(r['event'], 'reset de-asserted') + self.assertEqual(r['clk_hz'], 4000000) + + def test_rst_clk_extension_asserted(self): + r = self._dec('rst', '010001003d0900') # 4 MHz + self.assertEqual(r['event'], 'reset asserted') + self.assertEqual(r['clk_hz'], 4000000) + + def test_rst_clk_flag_absent(self): + r = self._dec('rst', '000100003d0900') + self.assertNotIn('clk_hz', r) + + def test_rst_clk_flag_truncated(self): + # Flag says CLK follows but the payload is too short — ignore. + r = self._dec('rst', '000101003d09') + self.assertNotIn('clk_hz', r) + + def test_db_atr_data_rate(self): + # CLK from RST → data rate on the following ATR; rate consumed once, + # so a second ATR without a preceding RST has no rate. + import tempfile + from simtrace2_pysniff.server.database import Database + with tempfile.NamedTemporaryFile(suffix='.db') as tmp: + db = Database(tmp.name) + sid = db.create_session('capture') + db.insert_message(sid, 0.1, 'rst', bytes.fromhex('000101003d0900'), 0) + db.insert_message(sid, 0.2, 'atr', bytes.fromhex('3B12110000'), 0) + db.insert_message(sid, 0.3, 'atr', bytes.fromhex('3B12110000'), 0) + msgs = db.get_messages(sid) + self.assertEqual(msgs[1]['decoded']['data_rate'], 10753) # 4 MHz / 372 + self.assertEqual(msgs[1]['decoded']['etu_us'], 93.0) + self.assertNotIn('data_rate', msgs[2]['decoded']) + + def test_db_atr_rate_backfill(self): + # Ordering caveat: a short ATR can be emitted before the RST event + # that measured its CLK rate — the rate is backfilled onto it. + import tempfile + from simtrace2_pysniff.server.database import Database + with tempfile.NamedTemporaryFile(suffix='.db') as tmp: + db = Database(tmp.name) + sid = db.create_session('capture') + db.insert_message(sid, 0.1, 'atr', bytes.fromhex('3B12110000'), 0) + db.insert_message(sid, 0.2, 'rst', bytes.fromhex('000101003d0900'), 0) + msgs = db.get_messages(sid) + self.assertEqual(msgs[0]['decoded']['data_rate'], 10753) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py index b430985..bfb6bff 100644 --- a/tests/test_pcap_import.py +++ b/tests/test_pcap_import.py @@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase): class TestVersion(unittest.TestCase): def test_package_version(self): from simtrace2_pysniff import __version__ - self.assertEqual(__version__, '1.22.17') + self.assertEqual(__version__, '1.22.18') def test_status_includes_version(self): import os