Compare commits

...

11 Commits

Author SHA1 Message Date
catarrh f1ed6a2a48 release: v2.0.0 2026-09-12 15:08:04 +03:00
catarrh 4b19b58bb0 ui: label comparison mismatches with snapshot names
Snapshot comparison results now use the master/checked snapshot names
where profile checks keep expected/actual: the raw-data field rows, the
generic mismatch line and the decoded FCI comparison table headers
(profilerRenderReport/fcpDiffHtml take an optional {expected, actual}
labels object; the name column widens to w-40 and wraps). Labels are
stored with the results so the language switch and the Only mismatches
filter re-render correctly; Check card snapshot and live checks are
unchanged. Tests for both labeled and default rendering; help/README
updated; SW cache v101 -> v102.
2026-09-12 15:06:51 +03:00
catarrh 6f63b5172c ui: show total scan time in the card snapshots list
Each snapshot row now renders '(N files, scanned in X.XX sec)' when the
snapshot has timing data (profilerSnapshotCountLabel); old/imported
snapshots without timing keep the plain '(N files)'. RU wording 'за X.XX
сек'. SW cache v100 -> v101.
2026-09-12 15:00:58 +03:00
catarrh 6d30e7095e snapshots: measure SELECT / READ commands and show timing stats
Server measures every classified APDU (A4 select, B0 read binary, B2 read
record) from command to response: _collect_apdu_times() enables the tracer
(reattaching it if pySim nulled it) and /api/select + /api/read return
'apdu_times': [{type, ms}]. Collection is safe: handlers hold _CARD_LOCK.

Snapshots store per-file {select_ms, read_ms}, a ms value per record and
snapshot-level stats {select, read_binary, read_record}: {min, max, avg,
count} plus total_ms (wall time of the scan). The snapshot view gets a
summary under the title (files/records counts, scan time, min/avg/max per
command type) and shows select/read per file and read time per record.
Timings are display-only: checks, snapshots comparison and imports ignore
them (old snapshots simply show 'No timing data').

Tests: Python classifier/collection (tests/test_apdu_timing.py) and
frontend stats/accumulator/format/build-file/summary. SW cache v99 ->
v100; help, README and docs/api.md updated.
2026-09-12 14:56:59 +03:00
catarrh a261675aad ui: focus scan name input and start scanning on Enter
'Profile from card' and 'New snapshot' now focus the name field when the
dialog opens, and Enter in that field starts the scan
(profilerScanNameKeydown, ignored while the Scan button is disabled).
Tests for the key handler and the input wiring; SW cache v98 -> v99.
2026-09-12 14:41:35 +03:00
catarrh 450f1de68a sw: never resolve respondWith to undefined on offline navigation
When a navigation fetch failed and the cache had no entry for the exact
URL (e.g. '/' while only 'index.html' is precached, common while the
local server restarts), the fetch handler resolved respondWith() to
undefined, producing 'TypeError: Failed to convert value to Response'.

The navigate fallback now chains caches.match(request) -> cached
index.html -> an explicit 503 offline Response. sw.js requests are
network-only with the same offline Response (previously they fell into
the navigate branch), and a new unit test loads sw.js in a VM sandbox
covering navigate/cache/asset/api paths. SW cache v97 -> v98.
2026-09-12 14:33:31 +03:00
catarrh 74efc20bf5 theme: brighten normal dark-mode text above the muted gray level
After the gray text was raised to #cbd5e1 in dark mode, elements using
dark:text-slate-300 (inactive tabs, labels, list text) matched the muted
level. Map dark:text-slate-300 to #e2e8f0 (slate-200, the body's dark
text color) so normal text stays one step brighter than muted gray.
SW cache v96 -> v97.
2026-09-12 14:31:27 +03:00
catarrh d2bd2ed798 theme: brighten gray text and borders in dark mode
Elements using bare text-gray-300/400/500/600 (proactive log ids and
timestamps, expand markers, event codes, ...) kept the light-theme gray
values in dark mode, which made them hard to read on slate-800/900.

- dark .text-gray-*:not([class*='dark:text-']) rules map them to the same
  level as the other dark muted text (slate-300 #cbd5e1, slate-200/100 for
  gray-700/800)
- dark:text-gray-400/600 and dark:text-slate-500 normalized to #cbd5e1
- dark:border-slate-600/700/800 (+ /50) brightened one step
- light theme unchanged
- the whole contrast block moved to src/contrast.css, appended by the
  npm build scripts after Tailwind so it stays reproducible; SW cache
  v95 -> v96
2026-09-12 14:28:41 +03:00
catarrh ccd0b6018c ui: separate STK menu, STATUS and events blocks in Phone view
Wrap each block of the phone row in a bordered card (border, rounded,
p-3) with items-start and gap-4 so they read as distinct panels and wrap
cleanly. SW cache v94 -> v95.
2026-09-12 14:20:35 +03:00
catarrh 9363f209ee ui: split Phone simulator into Phone and TR Config pills
- Phone pill: STK menu, STATUS and polling and the subscribed-events list
  in one row, proactive command log full-width below (room for more
  elements)
- TR Config pill: response data injected into TERMINAL RESPONSEs;
  currently the PROVIDE LOCAL INFORMATION dictionary, structured as
  heading + body blocks for future proactive-command responses
- phoneSwitchSubtab() mirrors the other sub-tab switchers and sets help
  anchors stk-menu / pli-dict; switchTab('phone') always opens Phone
- refreshDynamicI18n renders only the visible phone panel
- help EN/RU section 6 regrouped (6.4 STATUS polling under Phone, 6.5
  TR Config response data), READMEs and AGENTS.md updated
- structural and behavioral tests (html.test.js, phone_tabs.test.js);
  SW cache v93 -> v94
2026-09-12 14:17:48 +03:00
catarrh 33443d4f28 ui: regroup tabs — Remote APDU, Card reader, Profiler, Phone simulator
- C-APDU tab renamed Remote APDU (label untranslated EN/RU); Response
  parser moved from a top-level tab to a pill under it
- Profiler moved from the Card reader sub-tabs to a top-level tab
- Proactive UICC moved to a top-level tab and renamed Phone simulator
- Card reader keeps File manager, Custom files, pySim command line,
  Raw APDU
- modals (event send, profiler scan/snapshot, STK menu) moved outside
  the tab containers so they can open from any tab
- STK overlay disables the top-level tabs while waiting for user input
- OTA PoR jump now goes to Remote APDU > Response parser
- help EN/RU restructured (2.8 Response parser, 5 Profiler, 6 Phone
  simulator, subsequent sections renumbered, anchors kept); READMEs and
  AGENTS.md updated
- html.test.js asserts the new tab/pill structure; SW cache v92 -> v93
2026-09-12 14:01:28 +03:00
17 changed files with 1397 additions and 484 deletions
+57 -51
View File
@@ -31,13 +31,13 @@ npm run build
## Interface
Four top-level tabs: **C-APDU**, **SCP80**, **Response parser**, **Card reader**. The **C-APDU** and **SCP80** tabs use pill sub-tabs, and the Card reader tab has six sub-tabs: **File manager**, **Custom files**, **Profiler**, **pySim command line**, **Raw APDU**, and **Proactive UICC**.
Five top-level tabs: **Remote APDU**, **SCP80**, **Card reader**, **Profiler**, and **Phone simulator**. **Remote APDU** and **SCP80** use pill sub-tabs; the Card reader tab has four sub-tabs: **File manager**, **Custom files**, **pySim command line**, and **Raw APDU**.
---
## C-APDU tab
## Remote APDU tab
Builds command APDUs (C-APDUs). Six sub-tabs cover different card generations and command sets.
Builds command APDUs (C-APDUs). Seven sub-tabs cover different card generations, command sets and decoding tools: **SIM RFM**, **USIM RFM**, **Expanded Script**, **RAM/GP**, **HTTP OTA**, **C-APDU Parser**, and **Response parser**.
### SIM RFM
@@ -334,6 +334,19 @@ The **Command Scripting template** checkbox wraps the whole `81` triggering comm
---
### Response parser
Decodes a raw command response: pick the command that was sent, enter the SW (e.g. `9000`) and the response data hex, then press **Decode**.
- **Command** — SIM/USIM group (SELECT, STATUS, READ/UPDATE, PIN ops, CAT commands like TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, ...) or RAM/GP group (INSTALL, LOAD, DELETE, GET/STORE DATA, auth, SCP commands).
- **SW decode** — status words resolved against generic, UICC (TS 102 221), and GlobalPlatform maps, with context auto-detected.
- **Privilege decode** — GET DATA / INSTALL response payloads decode the privilege bytes into human-readable flags.
- **Response data** — raw hex rendered and interpreted per command (e.g. SELECT FCP templates).
---
## SCP80 tab
The **SCP80** top-level tab groups SCP80-related views, switched by three pills: **Secured Packet**, **Cards**, and **RAM**. Assembles secured packets per ETSI TS 102 225.
@@ -463,17 +476,6 @@ Delete confirms via a browser prompt before sending the GP `DELETE` command via
---
## Response parser tab
Decodes a raw command response: pick the command that was sent, enter the SW (e.g. `9000`) and the response data hex, then press **Decode**.
- **Command** — SIM/USIM group (SELECT, STATUS, READ/UPDATE, PIN ops, CAT commands like TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, ...) or RAM/GP group (INSTALL, LOAD, DELETE, GET/STORE DATA, auth, SCP commands).
- **SW decode** — status words resolved against generic, UICC (TS 102 221), and GlobalPlatform maps, with context auto-detected.
- **Privilege decode** — GET DATA / INSTALL response payloads decode the privilege bytes into human-readable flags.
- **Response data** — raw hex rendered and interpreted per command (e.g. SELECT FCP templates).
---
## Card Reader (pySim integration)
Connects to the bundled [`pysim-otaman-server`](pysim_otaman_server/) for live card operations.
@@ -499,45 +501,13 @@ Files not in pysim's model can be added manually:
Custom files persist in `localStorage` across sessions. Export/import as JSON for sharing.
### Proactive UICC Pill
The **Proactive UICC** sub-tab in the Card Reader provides real-time CAT session interaction:
**Subscribed Events** — the card's SET UP EVENT LIST is displayed with per-event **Send** buttons. Clicking opens a form specific to the event type:
- **No-data events** (User Activity, Idle Screen, etc.) — single-click confirmation
- **Location Status** — dropdown for Normal / Limited / No service
- **Access Technology Change** — dropdown for all 13 RAT types
- **Card Reader Status, Language, UICC Access** — appropriate inputs
- **Network Rejection** — full adaptive form with registration type dropdown
(LU / GPRS / EPS / 5GS), location fields (MCC, MNC, LAC, RAC, TAC), access
technology selection, and 53-cause unified rejection cause code dropdown
covering EMM, GMM, 5GMM, and LU causes
**Proactive Command Log** — chronological list of proactive commands encountered (seconds elapsed, type code, name, byte count). Covers SET UP MENU, SET UP EVENT LIST, POLL INTERVAL, DISPLAY TEXT, SELECT ITEM, and PROVIDE LOCAL INFORMATION.
**PLI Data Dictionary** — editable per-qualifier hex values for all 22 PROVIDE LOCAL INFORMATION qualifiers (TS 102 223 + TS 131 111). 10 qualifiers have inline decode/encode forms (toggle):
| Code | Decoded fields |
|------|--------------|
| 00 | MCC, MNC, LAC/TAC |
| 01 | IMEI (15 digits) |
| 03 | Date, Time, TZ offset |
| 04 | Language (2-char code) |
| 05 | ME Status, Timing Advance |
| 06 | Access Technology (dropdown) |
| 08 | IMEISV (16 digits) |
| 09 | Search Mode (Auto/Manual) |
| 0A | Battery charge (%) |
| 0E | Multiple Access Technologies (comma-list) |
Values persist on the server until restart. Apply → hex updates; Save → POSTs to server. The server will use these values to populate TERMINAL RESPONSE data for future PLI proactive commands.
### Command Hints
Type a command name in the **pySim command line** input. Usage hints appear as a tooltip after 300ms. Command autocomplete suggestions appear above the input.
### Profiler
---
## Profiler
Verifies that a card matches a named **profile** — an ordered set of rules describing the expected file system and, optionally, file contents. Profiles are stored in `localStorage`.
@@ -559,15 +529,51 @@ The scan dialog asks for a profile name and offers the FCP/FCI mode described ab
#### Card snapshots
The list view has two tabs — **Profiles** and **Card snapshots**. A snapshot is an immutable capture of the card filesystem: for every existing file it stores the path, symbolic name, file type, size (or record length/count), the raw FCI from the SELECT response, and the contents whenever the file is readable (no ignore list, no masking). The ICCID is decoded from EF.ICCID and shown next to the snapshot name.
The list view has two tabs — **Profiles** and **Card snapshots**. A snapshot is an immutable capture of the card filesystem: for every existing file it stores the path, symbolic name, file type, size (or record length/count), the raw FCI from the SELECT response, and the contents whenever the file is readable (no ignore list, no masking). The ICCID is decoded from EF.ICCID and shown next to the snapshot name. The scan also measures every card command (SELECT / READ BINARY / READ RECORD) from command to response and stores min/avg/max per type plus the total scan time; the snapshot view shows these in the summary and the select/read time per file (read time per record). Timings are display-only and ignored by checks/comparisons.
- **New snapshot** scans the card; **Import snapshot** loads JSON.
- Each snapshot row has **Open** (all captured data read-only, raw FCI with decoded FCI and contents; only the name is editable), **Export**, and **Delete**.
- **Check card snapshot** on a profile row runs the profile rules against a snapshot picked from the list, without a card reader. Files whose contents were not captured are reported as unverifiable errors.
- **Compare snapshots** compares two snapshots offline exactly like a profile check: pick the *master* snapshot and the *snapshot to check*, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same report. Every file must match exactly (exact FCI, contents); files present only in the checked snapshot are reported as extra files.
- **Compare snapshots** compares two snapshots offline exactly like a profile check: pick the *master* snapshot and the *snapshot to check*, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same report. Every file must match exactly (exact FCI, contents); files present only in the checked snapshot are reported as extra files. In the comparison report the mismatch fields and FCI comparison columns are labeled with the master/checked snapshot names instead of expected/actual.
---
---
## Phone simulator
The **Phone simulator** tab provides real-time CAT session interaction. It has two pills: **Phone** (STK menu, STATUS and polling, subscribed events, proactive command log) and **TR Config** (response data injected into TERMINAL RESPONSEs for proactive commands).
**Subscribed Events** — the card's SET UP EVENT LIST is displayed with per-event **Send** buttons. Clicking opens a form specific to the event type:
- **No-data events** (User Activity, Idle Screen, etc.) — single-click confirmation
- **Location Status** — dropdown for Normal / Limited / No service
- **Access Technology Change** — dropdown for all 13 RAT types
- **Card Reader Status, Language, UICC Access** — appropriate inputs
- **Network Rejection** — full adaptive form with registration type dropdown
(LU / GPRS / EPS / 5GS), location fields (MCC, MNC, LAC, RAC, TAC), access
technology selection, and 53-cause unified rejection cause code dropdown
covering EMM, GMM, 5GMM, and LU causes
**Proactive Command Log** — chronological list of proactive commands encountered (seconds elapsed, type code, name, byte count). Covers SET UP MENU, SET UP EVENT LIST, POLL INTERVAL, DISPLAY TEXT, SELECT ITEM, and PROVIDE LOCAL INFORMATION.
**TR Config: PLI data dictionary** — editable per-qualifier hex values for all 22 PROVIDE LOCAL INFORMATION qualifiers (TS 102 223 + TS 131 111). 10 qualifiers have inline decode/encode forms (toggle):
| Code | Decoded fields |
|------|--------------|
| 00 | MCC, MNC, LAC/TAC |
| 01 | IMEI (15 digits) |
| 03 | Date, Time, TZ offset |
| 04 | Language (2-char code) |
| 05 | ME Status, Timing Advance |
| 06 | Access Technology (dropdown) |
| 08 | IMEISV (16 digits) |
| 09 | Search Mode (Auto/Manual) |
| 0A | Battery charge (%) |
| 0E | Multiple Access Technologies (comma-list) |
Values persist on the server until restart. Apply → hex updates; Save → POSTs to server. The server will use these values to populate TERMINAL RESPONSE data for future PLI proactive commands.
## PWA
OTAMan is a Progressive Web App and can be installed for offline use. Use the **INSTALL PWA** button in the header, or use the browser's install prompt.
+53 -47
View File
@@ -31,13 +31,13 @@ npm run build
## Интерфейс
Четыре вкладки: **C-APDU**, **SCP80**, **Response parser**, **Card reader**. Вкладки C-APDU и SCP80 используют пиллы-подвкладки; во вкладке Card reader шесть подвкладок: **File manager**, **Custom files**, **Profiler**, **pySim command line**, **Raw APDU** и **Proactive UICC**.
Пять вкладок: **Remote APDU**, **SCP80**, **Card reader**, **Profiler** и **Phone simulator**. Вкладки Remote APDU и SCP80 используют пиллы-подвкладки; во вкладке Card reader четыре подвкладки: **File manager**, **Custom files**, **pySim command line** и **Raw APDU**.
---
## Вкладка C-APDU
## Вкладка Remote APDU
Построение команд APDU (C-APDU). Шесть подвкладок для разных поколений карт и наборов команд.
Построение команд APDU (C-APDU). Семь подвкладок для разных поколений карт, наборов команд и инструментов разбора: **SIM RFM**, **USIM RFM**, **Expanded Script**, **RAM/GP**, **HTTP OTA**, **Разбор C-APDU** и **«Парсер ответов»**.
### SIM RFM
@@ -308,6 +308,19 @@ CLA = `80` (GlobalPlatform v2.3.1). Удалённое управление со
---
### Парсер ответов
Декодирование ответа команды: выберите отправленную команду, введите SW (например, `9000`) и данные ответа в hex, затем нажмите **Decode**.
- **Команда** — группа SIM/USIM (SELECT, STATUS, READ/UPDATE, операции с PIN, CAT-команды TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, ...) или группа RAM/GP (INSTALL, LOAD, DELETE, GET/STORE DATA, аутентификация, команды SCP).
- **Декодирование SW** — статусные слова по картам generic, UICC (TS 102 221) и GlobalPlatform с автоопределением контекста.
- **Декодирование привилегий** — байты привилегий из ответов GET DATA / INSTALL в читаемые флаги.
- **Данные ответа** — hex с интерпретацией по команде (например, шаблоны FCP из SELECT).
---
## Вкладка SCP80
Вкладка **SCP80** группирует SCP80-виды, переключаемые тремя пиллами: **Secured Packet**, **Cards** и **RAM**. Сборка защищённых пакетов по ETSI TS 102 225.
@@ -437,17 +450,6 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
---
## Вкладка Response parser
Декодирование ответа команды: выберите отправленную команду, введите SW (например, `9000`) и данные ответа в hex, затем нажмите **Decode**.
- **Команда** — группа SIM/USIM (SELECT, STATUS, READ/UPDATE, операции с PIN, CAT-команды TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, ...) или группа RAM/GP (INSTALL, LOAD, DELETE, GET/STORE DATA, аутентификация, команды SCP).
- **Декодирование SW** — статусные слова по картам generic, UICC (TS 102 221) и GlobalPlatform с автоопределением контекста.
- **Декодирование привилегий** — байты привилегий из ответов GET DATA / INSTALL в читаемые флаги.
- **Данные ответа** — hex с интерпретацией по команде (например, шаблоны FCP из SELECT).
---
## Card Reader (интеграция с pySim)
Подключение к встроенному [`pysim-otaman-server`](pysim_otaman_server/) для работы с картой.
@@ -473,41 +475,13 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
Пользовательские файлы сохраняются в `localStorage`. Экспорт/импорт в JSON для обмена.
### Proactive UICC
Подраздел **Proactive UICC** во вкладке Card Reader обеспечивает взаимодействие с CAT-сессией в реальном времени:
**Subscribed Events** — список событий SET UP EVENT LIST с кнопками **Send**. Клик открывает форму для конкретного типа события:
- **События без данных** (User Activity, Idle Screen и др.) — однократное уведомление
- **Location Status** — выпадающий список: Normal / Limited / No service
- **Access Technology Change** — 13 типов RAT
- **Network Rejection** — полная адаптивная форма: тип регистрации (LU / GPRS / EPS / 5GS), поля локации (MCC, MNC, LAC, RAC, TAC), доступные технологии, 53-позиционный выпадающий список причин отказа (EMM, GMM, 5GMM, LU)
**Proactive Command Log** — хронологический список проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор.
**PLI Data Dictionary** — редактируемые hex-значения для всех 22 квалификаторов PROVIDE LOCAL INFORMATION (TS 102 223 + TS 131 111). 10 квалификаторов имеют встроенные формы декодирования/кодирования:
| Код | Декодированные поля |
|------|--------------|
| 00 | MCC, MNC, LAC/TAC |
| 01 | IMEI (15 цифр) |
| 03 | Дата, время, TZ |
| 04 | Язык (2-символьный код) |
| 05 | ME Status, Timing Advance |
| 06 | Access Technology (выпадающий список) |
| 08 | IMEISV (16 цифр) |
| 09 | Search Mode (Auto/Manual) |
| 0A | Battery charge (%) |
| 0E | Multiple Access Technologies (список через запятую) |
Значения сохраняются на сервере до перезапуска. Apply → hex обновляется; Save → POST на сервер.
### Подсказки команд
Введите имя команды в **pySim command line**. Подсказки по использованию появляются через 300 мс. Автодополнение команд — над полем ввода.
### Профайлер
---
## Профайлер
Проверка соответствия карты именованному **профилю** — упорядоченному набору правил, описывающих ожидаемую файловую систему и (опционально) содержимое файлов. Профили хранятся в `localStorage`.
@@ -529,15 +503,47 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
#### Снимки карт
Представление списка имеет две вкладки — **«Профили»** и **«Снимки карт»**. Снимок — неизменяемая фиксация файловой системы: путь, символьное имя, тип, размер (или длина/число записей), сырой FCI и содержимое (если читается) каждого существующего файла. ICCID декодируется из EF.ICCID и показывается рядом с именем.
Представление списка имеет две вкладки — **«Профили»** и **«Снимки карт»**. Снимок — неизменяемая фиксация файловой системы: путь, символьное имя, тип, размер (или длина/число записей), сырой FCI и содержимое (если читается) каждого существующего файла. ICCID декодируется из EF.ICCID и показывается рядом с именем. При сканировании также измеряется время каждой команды карты (SELECT / READ BINARY / READ RECORD) от отправки до ответа; сохраняются min/сред/max по типам и общее время сканирования — они показываются в сводке снимка и по файлам/записям. Время носит информационный характер и не используется при проверках и сравнении.
- **Новый снимок** сканирует карту; **Импорт снимка** загружает JSON.
- В строке снимка: **Открыть** (все данные только для чтения, сырой FCI с декодированным и содержимое; редактируется только имя), **Экспорт**, **Удалить**.
- **Проверить снимок карты** в строке профиля выполняет правила профиля на выбранном снимке без картридера. Файлы без захваченного содержимого помечаются как непроверяемые ошибки.
- **Сравнить снимки** сравнивает два снимка offline так же, как проверка профиля: выберите *эталонный* снимок и *снимок для проверки*, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию). Всё должно совпадать точно (FCI, содержимое); файлы только в проверяемом снимке помечаются как лишние.
- **Сравнить снимки** сравнивает два снимка offline так же, как проверка профиля: выберите *эталонный* снимок и *снимок для проверки*, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию). Всё должно совпадать точно (FCI, содержимое); файлы только в проверяемом снимке помечаются как лишние. В отчёте поля расхождений и колонки сравнения FCI подписаны именами эталонного и проверяемого снимков вместо expected/actual.
---
---
## Симулятор телефона
Вкладка **«Симулятор телефона»** обеспечивает взаимодействие с CAT-сессией в реальном времени. Две подвкладки: **«Телефон»** (меню STK, STATUS и опрос, подписанные события, журнал проактивных команд) и **«Конфигурация TR»** (данные ответов, подставляемые в TERMINAL RESPONSE для проактивных команд).
**Subscribed Events** — список событий SET UP EVENT LIST с кнопками **Send**. Клик открывает форму для конкретного типа события:
- **События без данных** (User Activity, Idle Screen и др.) — однократное уведомление
- **Location Status** — выпадающий список: Normal / Limited / No service
- **Access Technology Change** — 13 типов RAT
- **Network Rejection** — полная адаптивная форма: тип регистрации (LU / GPRS / EPS / 5GS), поля локации (MCC, MNC, LAC, RAC, TAC), доступные технологии, 53-позиционный выпадающий список причин отказа (EMM, GMM, 5GMM, LU)
**Proactive Command Log** — хронологический список проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор.
**Конфигурация TR: словарь PLI** — редактируемые hex-значения для всех 22 квалификаторов PROVIDE LOCAL INFORMATION (TS 102 223 + TS 131 111). 10 квалификаторов имеют встроенные формы декодирования/кодирования:
| Код | Декодированные поля |
|------|--------------|
| 00 | MCC, MNC, LAC/TAC |
| 01 | IMEI (15 цифр) |
| 03 | Дата, время, TZ |
| 04 | Язык (2-символьный код) |
| 05 | ME Status, Timing Advance |
| 06 | Access Technology (выпадающий список) |
| 08 | IMEISV (16 цифр) |
| 09 | Search Mode (Auto/Manual) |
| 0A | Battery charge (%) |
| 0E | Multiple Access Technologies (список через запятую) |
Значения сохраняются на сервере до перезапуска. Apply → hex обновляется; Save → POST на сервер.
## PWA
OTAMan — Progressive Web App. Можно установить для offline-использования через кнопку **INSTALL PWA** или через браузер.
+12 -4
View File
@@ -55,7 +55,7 @@ Returns server version for compatibility checking.
**Example response:**
```json
{"version": "1.9.28"}
{"version": "2.0.0"}
```
### `GET /api/status`
@@ -277,15 +277,22 @@ Read file content. Auto-detects transparent vs record files.
Returns transparent data:
```json
{"success": true, "sw": "9000", "file_type": "transparent", "data": "..."}
{"success": true, "sw": "9000", "file_type": "transparent", "data": "...",
"apdu_times": [{"type": "select", "ms": 12}, {"type": "read_binary", "ms": 9}]}
```
Returns records:
```json
{"success": true, "sw": "9000", "file_type": "linear_fixed",
"records": [{"num": 1, "data": "..."}, {"num": 2, "data": "..."}]}
"records": [{"num": 1, "data": "..."}, {"num": 2, "data": "..."}],
"apdu_times": [{"type": "select", "ms": 12},
{"type": "read_record", "ms": 11}, {"type": "read_record", "ms": 13}]}
```
`apdu_times` reports each command's duration (command sent to response
received) classified as `select`, `read_binary` or `read_record`; the PWA uses
it for snapshot timing statistics. Other commands are not reported.
### `POST /api/write`
Write raw hex data to a file.
@@ -316,7 +323,8 @@ Returns:
```json
{"name": "EF.ICCID", "fid": "2FE2", "file_type": "transparent",
"file_size": 10, "record_len": null, "num_of_rec": null,
"fci_hex": "621082024021...", "exists": true}
"fci_hex": "621082024021...",
"apdu_times": [{"type": "select", "ms": 12}], "exists": true}
```
`fci_hex` is the raw FCP template (`'62'`) from the SELECT response, used by
+103 -94
View File
@@ -44,13 +44,14 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Шапка</strong> — версия приложения, кнопка <strong>INSTALL PWA</strong> (появляется, когда браузер предлагает установку, для офлайн-работы), ссылки на проект на GitHub и на эту справку, переключатель языка <strong>EN/RU</strong> и переключатель тёмной/светлой <strong>темы</strong>.</li>
<li>Выбор языка и темы хранится в <code class="font-mono text-sm">localStorage</code> и сохраняется между перезагрузками.</li>
<li>Ссылка <strong>справка</strong> открывает эту документацию на разделе, соответствующем текущему представлению (например, подвкладка &laquo;Профайлер&raquo; открывает &sect;5.6).</li>
<li>Вкладки верхнего уровня: <strong>Remote APDU</strong> (<strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, <strong>Разбор C-APDU</strong>, <strong>&laquo;Парсер ответов&raquo;</strong>), <strong>SCP80</strong> (<strong>Secured Packet</strong>, <strong>Карты</strong>, <strong>RAM</strong>), <strong>&laquo;Картридер&raquo;</strong> (<strong>Файловый менеджер</strong>, <strong>Пользовательские файлы</strong>, <strong>Командная строка pySim</strong>, <strong>Отправка APDU</strong>), <strong>&laquo;Профайлер&raquo;</strong> и <strong>&laquo;Симулятор телефона&raquo;</strong>.</li>
<li>Ссылка <strong>справка</strong> открывает эту документацию на разделе, соответствующем текущему представлению (например, вкладка &laquo;Профайлер&raquo; открывает &sect;5).</li>
</ul>
<section class="mb-10">
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. Вкладка C-APDU</h2>
<p class="mb-3">Построение командных APDU (C-APDU). Шесть подвкладок охватывают разные поколения карт и наборы команд: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong> и <strong>Разбор C-APDU</strong>.</p>
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. Вкладка Remote APDU</h2>
<p class="mb-3">Построение командных APDU (C-APDU). Семь подвкладок охватывают разные поколения карт, наборы команд и инструменты разбора: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, <strong>Разбор C-APDU</strong> и <strong>&laquo;Парсер ответов&raquo;</strong>.</p>
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Удалённое управление файлами классических SIM-карт.</p>
@@ -138,7 +139,7 @@
</ul>
<h4 id="expanded-response" class="font-medium mb-2 text-base">Декодирование ответов (TS 102 226 §5.2.2)</h4>
<p class="text-sm mb-2">Входящие ответы Proof of Receipt декодируются сервером — формат expanded Remote Application response data (TS 102 226 §5.2.2) или компактный формат. Представление Secured Packet показывает результат после <strong>Отправить на карту</strong> (см. <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>): статус PoR (TAR, счётчик, сырой PoR), а статусное слово и данные ответа последней команды подставляются на вкладку <strong>&laquo;Парсер ответов&raquo;</strong>.</p>
<p class="text-sm mb-2">Входящие ответы Proof of Receipt декодируются сервером — формат expanded Remote Application response data (TS 102 226 §5.2.2) или компактный формат. Представление Secured Packet показывает результат после <strong>Отправить на карту</strong> (см. <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>): статус PoR (TAR, счётчик, сырой PoR), а статусное слово и данные ответа последней команды подставляются в подвкладку <strong>&laquo;Парсер ответов&raquo;</strong> (Remote APDU).</p>
<h3 id="ram-gp" class="text-lg font-medium mb-2">2.4 RAM/GP</h3>
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Команды удалённого управления приложениями. Строятся тем же сборщиком цепочки, что и SIM/USIM.</p>
@@ -227,6 +228,17 @@
<p class="text-sm mb-2"><strong>Упаковать в Secured packet</strong> отправляет готовый payload на вкладку SCP80 для заполнения SPI/счётчика — там укажите TAR, который слушает SD (обычно TAR OTASD).</p>
<h3 id="response-parser" class="text-lg font-medium mb-2">2.8 &laquo;Парсер ответов&raquo;</h3>
<p class="mb-3">Декодирует raw-ответ команды: выберите отправленную команду, введите SW (например, <code class="font-mono text-sm">9000</code>) и hex данных ответа, затем нажмите <strong>Декодировать</strong>. Поля также автоматически заполняются статусным словом и данными ответа последней команды после успешного нажатия <strong>Отправить на карту</strong> (см. <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>).</p>
<ul class="list-disc list-inside text-sm space-y-1">
<li><strong>Команда</strong> — группа SIM/USIM (SELECT, STATUS, READ/UPDATE, PIN-операции, CAT-команды типа TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, &hellip;) или группа RAM/GP (INSTALL, LOAD, DELETE, GET/STORE DATA, auth, SCP-команды).</li>
<li><strong>Декодирование SW</strong> — статусные слова разрешаются по generic-, UICC- (TS 102 221) и GlobalPlatform-таблицам, контекст определяется автоматически.</li>
<li><strong>Декодирование привилегий</strong> — ответы GET DATA / INSTALL декодируют байты привилегий в читаемые флаги.</li>
<li><strong>Данные ответа</strong> — raw hex отображается и интерпретируется согласно команде (например, FCP-шаблоны SELECT).</li>
</ul>
<section class="mb-10">
<h2 id="scp80" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">3. Вкладка SCP80</h2>
<p class="mb-3">Верхнеуровневая вкладка <strong>SCP80</strong> объединяет разделы, связанные с SCP80. Переключение — тремя переключателями: <strong>Secured Packet</strong>, <strong>Карты</strong> и <strong>RAM</strong>. Собирает защищённые пакеты SCP80 по ETSI TS 102 225.</p>
@@ -260,7 +272,7 @@
<li>AES требует счётчик с защитой от повтора: биты SPI1 b5&nbsp;b4 должны быть <code class="font-mono text-sm">10</code> (счётчик больше) или <code class="font-mono text-sm">11</code> (счётчик +1) согласно TS 102 225 &sect;5.1.2/&sect;5.1.3.1</li>
<li>Байт паддинга настраивается (<code class="font-mono text-sm">00</code> по умолчанию или <code class="font-mono text-sm">FF</code>)</li>
</ul>
<p class="text-sm mb-3">Кнопка <strong>Проверить в pySim</strong> сверяет собранный пакет с эталонной реализацией <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. Кнопка <strong>Отправить на карту</strong> доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются на вкладку <strong>&laquo;Парсер ответов&raquo;</strong>, а успешный PoR увеличивает счётчик повторов и очищает пакет.</p>
<p class="text-sm mb-3">Кнопка <strong>Проверить в pySim</strong> сверяет собранный пакет с эталонной реализацией <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. Кнопка <strong>Отправить на карту</strong> доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку <strong>&laquo;Парсер ответов&raquo;</strong> (Remote APDU), а успешный PoR увеличивает счётчик повторов и очищает пакет.</p>
<h3 id="cards" class="text-lg font-medium mb-2">3.2 Карты</h3>
<p class="mb-2">Хранит предустановки карт локально в браузере (<code class="font-mono text-sm">localStorage</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры.</p>
@@ -301,21 +313,10 @@
<section class="mb-10">
<h2 id="response-parser" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Вкладка &laquo;Парсер ответов&raquo;</h2>
<p class="mb-3">Декодирует raw-ответ команды: выберите отправленную команду, введите SW (например, <code class="font-mono text-sm">9000</code>) и hex данных ответа, затем нажмите <strong>Декодировать</strong>. Поля также автоматически заполняются статусным словом и данными ответа последней команды после успешного нажатия <strong>Отправить на карту</strong> (см. <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>).</p>
<ul class="list-disc list-inside text-sm space-y-1">
<li><strong>Команда</strong> — группа SIM/USIM (SELECT, STATUS, READ/UPDATE, PIN-операции, CAT-команды типа TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, &hellip;) или группа RAM/GP (INSTALL, LOAD, DELETE, GET/STORE DATA, auth, SCP-команды).</li>
<li><strong>Декодирование SW</strong> — статусные слова разрешаются по generic-, UICC- (TS 102 221) и GlobalPlatform-таблицам, контекст определяется автоматически.</li>
<li><strong>Декодирование привилегий</strong> — ответы GET DATA / INSTALL декодируют байты привилегий в читаемые флаги.</li>
<li><strong>Данные ответа</strong> — raw hex отображается и интерпретируется согласно команде (например, FCP-шаблоны SELECT).</li>
</ul>
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Вкладка &laquo;Картридер&raquo; (pySim)</h2>
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> для работы с картой: введите URL сервера (по умолчанию <code class="font-mono text-sm">http://127.0.0.1:8080</code>) и нажмите <strong>Подключиться</strong>. Область статуса показывает состояние ридера/карты, а <strong>Подключить карту</strong> (пере)инициализирует карту после вставки. Подвкладки: <strong>Файловый менеджер</strong>, <strong>Пользовательские файлы</strong>, <strong>Командная строка pySim</strong> и <strong>Отправка APDU</strong>. <strong>&laquo;Профайлер&raquo;</strong> и <strong>&laquo;Симулятор телефона&raquo;</strong> — отдельные вкладки верхнего уровня.</p>
<section class="mb-10">
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">5. Вкладка &laquo;Картридер&raquo; (pySim)</h2>
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> для работы с картой: введите URL сервера (по умолчанию <code class="font-mono text-sm">http://127.0.0.1:8080</code>) и нажмите <strong>Подключиться</strong>. Область статуса показывает состояние ридера/карты, а <strong>Подключить карту</strong> (пере)инициализирует карту после вставки. Подвкладки: <strong>Файловый менеджер</strong>, <strong>Пользовательские файлы</strong>, <strong>Профайлер</strong>, <strong>Командная строка pySim</strong>, <strong>Отправка APDU</strong> и <strong>Проактивный UICC</strong>.</p>
<h3 id="file-manager" class="text-lg font-medium mb-2">5.1 Файловый менеджер</h3>
<h3 id="file-manager" class="text-lg font-medium mb-2">4.1 Файловый менеджер</h3>
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Прочитать</strong> — чтение файла (автоопределение transparent/record)</li>
@@ -323,48 +324,55 @@
<li><strong>Данные как на карте / Декодированные данные</strong> — переключение между hex-дампом и декодированным JSON</li>
</ul>
<h3 id="custom-files" class="text-lg font-medium mb-2">5.2 Пользовательские файлы</h3>
<h3 id="custom-files" class="text-lg font-medium mb-2">4.2 Пользовательские файлы</h3>
<p class="text-sm mb-3">Добавление файлов, не покрытых моделью pySim: введите полный путь (например, <code class="font-mono text-sm">3F00/7F20/6F46</code>) и псевдоним (например, <code class="font-mono text-sm">EF.SPN</code>), затем нажмите <strong>Добавить</strong>; добавленные файлы появляются в дереве &laquo;Файловый менеджер&raquo;. Список сохраняется в <code class="font-mono text-sm">localStorage</code>; обмен — <strong>Экспорт в JSON</strong> / <strong>Экспорт в файл</strong> и <strong>Импорт из файла</strong> / <strong>Вставить и импортировать</strong> / <strong>Импорт JSON из буфера</strong>.</p>
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">5.3 Командная строка pySim</h3>
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">4.3 Командная строка pySim</h3>
<p class="text-sm mb-3">Выполнение любых команд pySim-shell с подсказками (300&nbsp;мс) и автодополнением.</p>
<h3 id="raw-apdu" class="text-lg font-medium mb-2">5.4 Отправка APDU</h3>
<h3 id="raw-apdu" class="text-lg font-medium mb-2">4.4 Отправка APDU</h3>
<p class="text-sm mb-3">Отправка произвольного APDU и просмотр ответа.</p>
<h3 id="proactive-uicc" class="text-lg font-medium mb-2">5.5 Проактивный UICC</h3>
<p class="text-sm mb-3">Работа с сессией Card Application Toolkit: меню STK, подписанные события, журнал проактивных команд, словарь данных PROVIDE LOCAL INFORMATION и опрос STATUS.</p>
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">4.5 Сценарии использования</h3>
<h4 id="stk-menu" class="font-medium mb-1">5.5.1 Меню STK</h4>
<p class="text-sm mb-3">Если карта выдала команду SET UP MENU, вверху этого представления появляется блок &laquo;Меню STK&raquo; с изумрудной кнопкой <strong>STK: &lt;название&gt;</strong>, открывающей оверлей меню (браузер STK-меню карты). Если карта не задала меню, вместо кнопки показывается &laquo;Меню не задано картой&raquo;. Состояние меню обновляется при каждом открытии представления. Интерактивные проактивные команды всегда получают TERMINAL RESPONSE: оверлей ждёт вашего выбора, и если вы не ответили и не нажали <strong>Timeout</strong>, сервер сам отвечает результатом timeout через <code class="font-mono text-sm">--menu-timeout</code> секунд (по умолчанию 60, <code class="font-mono text-sm">0</code> отключает).</p>
<h4 id="scenario-a" class="font-medium mb-1">Сценарий A &mdash; Работа с файлами, не входящими в модель pySim (&laquo;Пользовательские файлы&raquo;)</h4>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Получите FID целевого файла (документация вендора или анализ ATR/файловой системы; такие файлы часто отсутствуют в открытых спецификациях).</li>
<li>Откройте вкладку <strong>&laquo;Картридер&raquo;</strong> &rarr; подвкладку <strong>&laquo;Пользовательские файлы&raquo;</strong>.</li>
<li>Введите полный путь (например, <code class="font-mono text-sm">3F00/7F20/6F46</code>) и псевдоним (например, <code class="font-mono text-sm">EF.SPN</code>).</li>
<li>Нажмите <strong>Добавить</strong> — файл появится в дереве курсивом (непроверенный).</li>
<li>Кликните по файлу для проверки существования; при успехе (<code class="font-mono text-sm">9000</code>) он работает как обычный файл.</li>
<li>Читайте, редактируйте и сохраняйте hex-данные; переключайте <strong>Данные как на карте</strong> / <strong>Декодированные данные</strong>.</li>
<li>Экспортируйте список пользовательских файлов в JSON для переноса на другие машины.</li>
</ol>
<h4 id="subscribed-events" class="font-medium mb-1">5.5.2 Подписанные события (SET UP EVENT LIST)</h4>
<p class="text-sm mb-2">События, которые отслеживает карта. У каждого события есть кнопка <strong>Отправить</strong>, открывающая форму, специфичную для типа события:</p>
<h4 id="scenario-b" class="font-medium mb-1">Сценарий B &mdash; Симуляция реальной сетевой среды для тестирования SIM</h4>
<p class="text-sm mb-1"><strong>B.1 Ответы на PROVIDE LOCAL INFORMATION (PLI)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Откройте <strong>Симулятор телефона</strong> &rarr; <strong>Данные для PROVIDE LOCAL INFORMATION</strong>.</li>
<li>Используйте формы декодирования/кодирования для IMEI (<code class="font-mono text-sm">01</code>), Location Info (<code class="font-mono text-sm">00</code>), Access Technology (<code class="font-mono text-sm">06</code>) и т.д.</li>
<li>Нажмите <strong>Сохранить</strong> — значения сохранятся на сервере.</li>
<li>Включите <strong>Опрос</strong> (интервал 30&nbsp;с), чтобы карта периодически выдавала PLI.</li>
<li>Сервер вставляет значения словаря в каждый TERMINAL RESPONSE.</li>
<li>Проверьте в журнале проактивных команд: запись PLI покажет декодированный ответ.</li>
</ol>
<p class="text-sm mb-1"><strong>B.2 Симуляция сетевых действий через ENVELOPE (event download)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Проверьте список <strong>подписанных событий</strong> (из SET UP EVENT LIST).</li>
<li>Нажмите <strong>Отправить</strong> на событии (например, Location Status) и заполните форму; будет отправлен <code class="font-mono text-sm">ENVELOPE(Event Download)</code>.</li>
<li>Для <strong>Network Rejection</strong> выберите тип регистрации &rarr; поля местоположения &rarr; технологию доступа &rarr; причину отклонения.</li>
<li>Карта может ответить проактивной командой, которую обработчик цепочки зарегистрирует и обработает автоматически.</li>
</ol>
<p class="text-sm mb-1"><strong>B.3 Проверка симулированной среды</strong></p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>События без данных</strong> (User Activity, Idle Screen, Data Available, &hellip;) — уведомление в один клик</li>
<li><strong>Location Status</strong> — выпадающий список: Normal / Limited / No service (тег <code class="font-mono text-sm">9B</code>)</li>
<li><strong>Access Technology Change</strong> — 13 типов RAT (тег <code class="font-mono text-sm">BF</code>)</li>
<li><strong>Network Rejection</strong> — полная адаптивная форма: тип регистрации (LU / GPRS / EPS / 5GS), поля местоположения (MCC, MNC, LAC, RAC, TAC), технология доступа и единый выпадающий список из 53 кодов причин (EMM, GMM, 5GMM и LU)</li>
<li>Журнал проактивных команд показывает полный цикл (команда + байты TERMINAL RESPONSE).</li>
<li>Кнопка <strong>Отправить STATUS</strong> / автопросмотр поддерживают сессию CAT (цикл дренажа).</li>
</ul>
<p class="text-sm mb-3">Отправка события использует <code class="font-mono text-sm">ENVELOPE(Event Download)</code> по TS 102 223 / TS 131 111.</p>
<h4 id="proactive-log" class="font-medium mb-1">5.5.3 Журнал проактивных команд</h4>
<p class="text-sm mb-2">Хронологический список извлечённых проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор (для команд, у которых он есть). Для команд с данными ответа показывается строка <code class="font-mono text-sm">Ответ:</code> с байтами TERMINAL RESPONSE (без служебных TLV); ответы PROVIDE LOCAL INFORMATION декодируются через словарь данных PLI.</p>
<h4 id="pli-dict" class="font-medium mb-1">5.5.4 Словарь данных PROVIDE LOCAL INFORMATION</h4>
<p class="text-sm mb-2">Редактируемые hex-значения для всех 22 квалификаторов PLI (TS 102 223 &sect;8.6 + TS 131 111). У десяти квалификаторов есть встроенные формы декодирования/кодирования:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>00</strong> Location Info (MCC, MNC, LAC/TAC, Cell ID)</li>
<li><strong>01</strong> IMEI &middot; <strong>03</strong> Дата/время/TZ &middot; <strong>04</strong> Язык &middot; <strong>05</strong> Timing Advance</li>
<li><strong>06</strong> Access Technology &middot; <strong>08</strong> IMEISV &middot; <strong>09</strong> Search Mode</li>
<li><strong>0A</strong> Battery &middot; <strong>0E</strong> Multiple Access Technologies</li>
</ul>
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Когда карта выдаёт PLI, сервер вставляет значения словаря в TERMINAL RESPONSE.</p>
<h4 id="status-polling" class="font-medium mb-1">5.5.5 Опрос STATUS</h4>
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Переключатель <strong>Опрос</strong> включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;с, по умолчанию 30&nbsp;с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
<h3 id="profiler" class="text-lg font-medium mb-2">5.6 Профайлер</h3>
<section class="mb-10">
<h2 id="profiler" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">5. Профайлер</h2>
<p class="text-sm mb-2">Проверяет соответствие карты именованному <strong>профилю</strong> — упорядоченному набору правил, описывающих ожидаемую файловую систему и (опционально) содержимое файлов. Профили хранятся в <code class="font-mono text-sm">localStorage</code>.</p>
<h4 class="font-medium mb-1">Список профилей</h4>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
@@ -386,57 +394,58 @@
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>&laquo;Проверка FCP/FCI&raquo;</strong> (те же три режима, по умолчанию <strong>Тип файла + размер (FCP)</strong>), применяемый ко всем создаваемым правилам, а также список <strong>&laquo;Игнорировать содержимое файлов&raquo;</strong> (все отмечены по умолчанию, кроме <code class="font-mono text-sm">EF.ARR</code>; флажок в заголовке отмечает или снимает весь список) часто перезаписываемых файлов, содержимое которых пропускается: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>, <code class="font-mono text-sm">EF.ACC</code>, <code class="font-mono text-sm">EF.EPSNSC</code>, <code class="font-mono text-sm">EF.START-HFN</code>, <code class="font-mono text-sm">EF.ARR</code>. Ещё две отмеченные по умолчанию опции <strong>&laquo;Сравнивать первые 4 байта для&raquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> и <code class="font-mono text-sm">EF.ICCID</code> захватывают содержимое этих файлов как маску только первых 4 байт (снимите для точного сравнения). Строка прогресса показывает <em>N / всего файлов</em> с текущим путём файла во время сканирования; при сканировании опции скрываются, а кнопки блокируются. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки <strong>&laquo;Пользовательские файлы&raquo;</strong> включаются с той же проверкой существования.</p>
<h4 id="card-snapshots" class="font-medium mb-1">Снимки карт</h4>
<p class="text-sm mb-2">Представление списка имеет две вкладки &mdash; <strong>&laquo;Профили&raquo;</strong> и <strong>&laquo;Снимки карт&raquo;</strong>. Снимок карты — неизменяемая фиксация файловой системы карты: для каждого существующего файла сохраняются путь, символьное имя, тип, размер (или длина/число записей), сырой FCI из ответа SELECT и содержимое, если файл читается (без списка игнорирования и без масок). ICCID декодируется из EF.ICCID и показывается рядом с именем снимка.</p>
<p class="text-sm mb-2">Представление списка имеет две вкладки &mdash; <strong>&laquo;Профили&raquo;</strong> и <strong>&laquo;Снимки карт&raquo;</strong>. Снимок карты — неизменяемая фиксация файловой системы карты: для каждого существующего файла сохраняются путь, символьное имя, тип, размер (или длина/число записей), сырой FCI из ответа SELECT и содержимое, если файл читается (без списка игнорирования и без масок). ICCID декодируется из EF.ICCID и показывается рядом с именем снимка. При сканировании измеряется время каждой команды карты (SELECT, READ BINARY, READ RECORD) от отправки до ответа; снимок хранит min/сред/max по каждому типу команд и общее время сканирования, а в представлении эти значения показываются в сводке под заголовком, время select/read — для каждого файла и время чтения — для каждой записи. Время носит информационный характер и не используется при проверках и сравнении.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Новый снимок</strong> &mdash; запрашивает имя и сканирует карту, затем возвращает к списку.</li>
<li><strong>Импорт снимка</strong> &mdash; загружает снимок из JSON-файла.</li>
<li>В каждой строке снимка — <strong>Открыть</strong>, <strong>Экспорт</strong> и <strong>Удалить</strong>. <strong>Открыть</strong> показывает все захваченные данные только для чтения (сырой FCI с декодированным FCI, содержимое); редактируется только имя снимка.</li>
<li><strong>Проверить снимок карты</strong> в строке профиля выполняет правила профиля на выбранном из списка снимке, без картридера. Отчёт такой же, как при проверке карты; файлы, содержимое которых не было захвачено при сканировании, помечаются как непроверяемые ошибки.</li>
<li><strong>Сравнить снимки</strong> сравнивает два снимка без картридера так же, как проверка профиля: выберите <em>эталонный</em> снимок и <em>снимок для проверки</em>, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию) и получите такой же отчёт. Файлы, которые есть только в проверяемом снимке, помечаются как лишние. «К списку» возвращает на вкладку «Снимки карт».</li>
<li><strong>Сравнить снимки</strong> сравнивает два снимка без картридера так же, как проверка профиля: выберите <em>эталонный</em> снимок и <em>снимок для проверки</em>, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию) и получите такой же отчёт; в этом отчёте поля расхождений и колонки сравнения FCI подписаны именами эталонного и проверяемого снимков вместо expected/actual. Файлы, которые есть только в проверяемом снимке, помечаются как лишние. «К списку» возвращает на вкладку «Снимки карт».</li>
</ul>
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Сценарии использования</h3>
<h4 id="scenario-a" class="font-medium mb-1">Сценарий A &mdash; Работа с файлами, не входящими в модель pySim (&laquo;Пользовательские файлы&raquo;)</h4>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Получите FID целевого файла (документация вендора или анализ ATR/файловой системы; такие файлы часто отсутствуют в открытых спецификациях).</li>
<li>Откройте вкладку <strong>&laquo;Картридер&raquo;</strong> &rarr; подвкладку <strong>&laquo;Пользовательские файлы&raquo;</strong>.</li>
<li>Введите полный путь (например, <code class="font-mono text-sm">3F00/7F20/6F46</code>) и псевдоним (например, <code class="font-mono text-sm">EF.SPN</code>).</li>
<li>Нажмите <strong>Добавить</strong> — файл появится в дереве курсивом (непроверенный).</li>
<li>Кликните по файлу для проверки существования; при успехе (<code class="font-mono text-sm">9000</code>) он работает как обычный файл.</li>
<li>Читайте, редактируйте и сохраняйте hex-данные; переключайте <strong>Данные как на карте</strong> / <strong>Декодированные данные</strong>.</li>
<li>Экспортируйте список пользовательских файлов в JSON для переноса на другие машины.</li>
</ol>
<h4 id="scenario-b" class="font-medium mb-1">Сценарий B &mdash; Симуляция реальной сетевой среды для тестирования SIM</h4>
<p class="text-sm mb-1"><strong>B.1 Ответы на PROVIDE LOCAL INFORMATION (PLI)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Откройте <strong>Проактивный UICC</strong> &rarr; <strong>Данные для PROVIDE LOCAL INFORMATION</strong>.</li>
<li>Используйте формы декодирования/кодирования для IMEI (<code class="font-mono text-sm">01</code>), Location Info (<code class="font-mono text-sm">00</code>), Access Technology (<code class="font-mono text-sm">06</code>) и т.д.</li>
<li>Нажмите <strong>Сохранить</strong> — значения сохранятся на сервере.</li>
<li>Включите <strong>Опрос</strong> (интервал 30&nbsp;с), чтобы карта периодически выдавала PLI.</li>
<li>Сервер вставляет значения словаря в каждый TERMINAL RESPONSE.</li>
<li>Проверьте в журнале проактивных команд: запись PLI покажет декодированный ответ.</li>
</ol>
<p class="text-sm mb-1"><strong>B.2 Симуляция сетевых действий через ENVELOPE (event download)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Проверьте список <strong>подписанных событий</strong> (из SET UP EVENT LIST).</li>
<li>Нажмите <strong>Отправить</strong> на событии (например, Location Status) и заполните форму; будет отправлен <code class="font-mono text-sm">ENVELOPE(Event Download)</code>.</li>
<li>Для <strong>Network Rejection</strong> выберите тип регистрации &rarr; поля местоположения &rarr; технологию доступа &rarr; причину отклонения.</li>
<li>Карта может ответить проактивной командой, которую обработчик цепочки зарегистрирует и обработает автоматически.</li>
</ol>
<p class="text-sm mb-1"><strong>B.3 Проверка симулированной среды</strong></p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li>Журнал проактивных команд показывает полный цикл (команда + байты TERMINAL RESPONSE).</li>
<li>Кнопка <strong>Отправить STATUS</strong> / автопросмотр поддерживают сессию CAT (цикл дренажа).</li>
</ul>
</section>
<section class="mb-10">
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Установка сервера</h2>
<p class="mb-3">Для работы с картой (вкладка &laquo;Картридер&raquo;, &laquo;Проактивный UICC&raquo;, доставка OTA) нужен локальный <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — встроенный в OTAMan HTTP-сервер, оборачивающий pySim, работающий с ридером через PC/SC или serial и раздающий сам PWA (откройте <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
<h2 id="proactive-uicc" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Симулятор телефона</h2>
<p class="text-sm mb-3">Работа с сессией Card Application Toolkit. Две подвкладки: <strong>&laquo;Телефон&raquo;</strong> (меню STK, STATUS и опрос, подписанные события, журнал проактивных команд) и <strong>&laquo;Конфигурация TR&raquo;</strong> (данные ответов, подставляемые в TERMINAL RESPONSE для проактивных команд).</p>
<h3 id="prerequisites" class="text-lg font-medium mb-2">6.1 Требования</h3>
<h3 id="stk-menu" class="text-lg font-medium mb-2">6.1 Меню STK</h3>
<p class="text-sm mb-3">Если карта выдала команду SET UP MENU, вверху этого представления появляется блок &laquo;Меню STK&raquo; с изумрудной кнопкой <strong>STK: &lt;название&gt;</strong>, открывающей оверлей меню (браузер STK-меню карты). Если карта не задала меню, вместо кнопки показывается &laquo;Меню не задано картой&raquo;. Состояние меню обновляется при каждом открытии представления. Интерактивные проактивные команды всегда получают TERMINAL RESPONSE: оверлей ждёт вашего выбора, и если вы не ответили и не нажали <strong>Timeout</strong>, сервер сам отвечает результатом timeout через <code class="font-mono text-sm">--menu-timeout</code> секунд (по умолчанию 60, <code class="font-mono text-sm">0</code> отключает).</p>
<h3 id="subscribed-events" class="text-lg font-medium mb-2">6.2 Подписанные события (SET UP EVENT LIST)</h3>
<p class="text-sm mb-2">События, которые отслеживает карта. У каждого события есть кнопка <strong>Отправить</strong>, открывающая форму, специфичную для типа события:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>События без данных</strong> (User Activity, Idle Screen, Data Available, &hellip;) — уведомление в один клик</li>
<li><strong>Location Status</strong> — выпадающий список: Normal / Limited / No service (тег <code class="font-mono text-sm">9B</code>)</li>
<li><strong>Access Technology Change</strong> — 13 типов RAT (тег <code class="font-mono text-sm">BF</code>)</li>
<li><strong>Network Rejection</strong> — полная адаптивная форма: тип регистрации (LU / GPRS / EPS / 5GS), поля местоположения (MCC, MNC, LAC, RAC, TAC), технология доступа и единый выпадающий список из 53 кодов причин (EMM, GMM, 5GMM и LU)</li>
</ul>
<p class="text-sm mb-3">Отправка события использует <code class="font-mono text-sm">ENVELOPE(Event Download)</code> по TS 102 223 / TS 131 111.</p>
<h3 id="proactive-log" class="text-lg font-medium mb-2">6.3 Журнал проактивных команд</h3>
<p class="text-sm mb-2">Хронологический список извлечённых проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор (для команд, у которых он есть). Для команд с данными ответа показывается строка <code class="font-mono text-sm">Ответ:</code> с байтами TERMINAL RESPONSE (без служебных TLV); ответы PROVIDE LOCAL INFORMATION декодируются через словарь данных PLI.</p>
<h3 id="status-polling" class="text-lg font-medium mb-2">6.4 Опрос STATUS</h3>
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Переключатель <strong>Опрос</strong> включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;с, по умолчанию 30&nbsp;с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
<h3 id="pli-dict" class="text-lg font-medium mb-2">6.5 &laquo;Конфигурация TR&raquo; &mdash; данные ответа PROVIDE LOCAL INFORMATION</h3>
<p class="text-sm mb-2">Редактируемые hex-значения для всех 22 квалификаторов PLI (TS 102 223 &sect;8.6 + TS 131 111). У десяти квалификаторов есть встроенные формы декодирования/кодирования:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>00</strong> Location Info (MCC, MNC, LAC/TAC, Cell ID)</li>
<li><strong>01</strong> IMEI &middot; <strong>03</strong> Дата/время/TZ &middot; <strong>04</strong> Язык &middot; <strong>05</strong> Timing Advance</li>
<li><strong>06</strong> Access Technology &middot; <strong>08</strong> IMEISV &middot; <strong>09</strong> Search Mode</li>
<li><strong>0A</strong> Battery &middot; <strong>0E</strong> Multiple Access Technologies</li>
</ul>
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Когда карта выдаёт PLI, сервер вставляет значения словаря в TERMINAL RESPONSE.</p>
</section>
<section class="mb-10">
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">7. Установка сервера</h2>
<p class="mb-3">Для работы с картой (вкладка &laquo;Картридер&raquo;, &laquo;Симулятор телефона&raquo;, доставка OTA) нужен локальный <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — встроенный в OTAMan HTTP-сервер, оборачивающий pySim, работающий с ридером через PC/SC или serial и раздающий сам PWA (откройте <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
<h3 id="prerequisites" class="text-lg font-medium mb-2">7.1 Требования</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Python 3.8+</strong> с <code class="font-mono text-sm">pip</code></li>
<li><strong>Git</strong></li>
@@ -444,20 +453,20 @@
<li><strong>Только Windows</strong> — используйте <strong>Python 3.10&ndash;3.13</strong> (рекомендуется 3.13): <code class="font-mono text-sm">pyscard</code> (обёртка драйвера PC/SC) поставляет готовые wheels для этих версий. На Python 3.9 / 3.14 pip собирает <code class="font-mono text-sm">pyscard</code> из исходников, для чего требуются Microsoft C++ Build Tools (&laquo;Desktop development with C++&raquo;). Мост SMPP (<code class="font-mono text-sm">smpp.twisted3</code>) на Windows намеренно не устанавливается, поэтому для Python 3.10&ndash;3.13 C++ Build Tools не нужны.</li>
</ul>
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">6.2 Быстрый старт — Linux / macOS</h3>
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">7.2 Быстрый старт — Linux / macOS</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
cd otaman
chmod +x setup.sh start.sh
./setup.sh # создаёт .venv, устанавливает pysim + сервер (однократно)
./start.sh # запускает сервер (PWA + API, автоопределение ридера)</pre>
<h3 id="quickstart-windows" class="text-lg font-medium mb-2">6.3 Быстрый старт — Windows</h3>
<h3 id="quickstart-windows" class="text-lg font-medium mb-2">7.3 Быстрый старт — Windows</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
cd otaman
setup.bat # создаёт .venv, устанавливает pysim + сервер (однократно)
start.bat # запускает сервер (PWA + API)</pre>
<h3 id="helper-scripts" class="text-lg font-medium mb-2">6.4 Вспомогательные скрипты</h3>
<h3 id="helper-scripts" class="text-lg font-medium mb-2">7.4 Вспомогательные скрипты</h3>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Скрипт</th><th class="text-left py-1 px-2">Назначение</th></tr></thead>
<tbody>
@@ -466,7 +475,7 @@ start.bat # запускает сервер (PWA + API)</pre>
</tbody>
</table>
<h3 id="reader-autodetect" class="text-lg font-medium mb-2">6.5 Автоопределение ридера</h3>
<h3 id="reader-autodetect" class="text-lg font-medium mb-2">7.5 Автоопределение ридера</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>PC/SC (Linux)</strong><code class="font-mono text-sm">start.sh</code> передаёт <code class="font-mono text-sm">-p 0</code>, если запущен демон <code class="font-mono text-sm">pcscd</code></li>
<li><strong>PC/SC (Windows)</strong><code class="font-mono text-sm">start.bat</code> всегда использует <code class="font-mono text-sm">-p 0</code> (PC/SC встроен в Windows)</li>
@@ -475,7 +484,7 @@ start.bat # запускает сервер (PWA + API)</pre>
</ul>
<p class="text-sm mb-3">Если карта отсутствует, вкладка &laquo;Картридер&raquo; показывает &laquo;Карта не обнаружена. Вставьте карту и нажмите Подключить карту&raquo;.</p>
<h3 id="manual-install" class="text-lg font-medium mb-2">6.6 Ручная установка</h3>
<h3 id="manual-install" class="text-lg font-medium mb-2">7.6 Ручная установка</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3"># Создать и активировать venv
python3 -m venv .venv
source .venv/bin/activate # Linux/macOS
@@ -494,7 +503,7 @@ pysim-otaman-server --http-port 8080</pre>
<section class="mb-10">
<h2 id="compatibility" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">7. Совместимость версий</h2>
<h2 id="compatibility" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">8. Совместимость версий</h2>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">PWA (OTAMan)</th><th class="text-left py-1 px-2">Сервер</th><th class="text-left py-1 px-2">Статус</th></tr></thead>
<tbody>
+102 -93
View File
@@ -44,13 +44,14 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Header</strong> — the app version, an <strong>INSTALL PWA</strong> button (shown when the browser offers installation, enabling offline use), links to the project on GitHub and to this help, an <strong>EN/RU</strong> language toggle, and a dark/light <strong>theme</strong> toggle.</li>
<li>Language and theme choices are stored in <code class="font-mono text-sm">localStorage</code> and persist across reloads.</li>
<li>The <strong>help</strong> link opens this documentation at the section matching the current view (e.g. the Profiler sub-tab opens &sect;5.6).</li>
<li>Top-level tabs: <strong>Remote APDU</strong> (<strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, <strong>C-APDU Parser</strong>, <strong>Response parser</strong>), <strong>SCP80</strong> (<strong>Secured Packet</strong>, <strong>Cards</strong>, <strong>RAM</strong>), <strong>Card reader</strong> (<strong>File manager</strong>, <strong>Custom files</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong>), <strong>Profiler</strong>, and <strong>Phone simulator</strong>.</li>
<li>The <strong>help</strong> link opens this documentation at the section matching the current view (e.g. the Profiler tab opens &sect;5).</li>
</ul>
<section class="mb-10">
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. C-APDU tab</h2>
<p class="mb-3">Builds command APDUs (C-APDUs). Six sub-tabs cover different card generations and command sets: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, and <strong>C-APDU Parser</strong>.</p>
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. Remote APDU tab</h2>
<p class="mb-3">Builds command APDUs (C-APDUs). Seven sub-tabs cover different card generations, command sets and decoding tools: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, <strong>C-APDU Parser</strong>, and <strong>Response parser</strong>.</p>
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Remote File Management for classic SIM cards.</p>
@@ -138,7 +139,7 @@
</ul>
<h4 id="expanded-response" class="font-medium mb-2 text-base">Response decoding (TS 102 226 §5.2.2)</h4>
<p class="text-sm mb-2">Incoming Proof-of-Receipt responses are decoded by the server — expanded Remote Application response data (TS 102 226 §5.2.2) or the compact format. The Secured Packet view shows the outcome after <strong>Send to Card</strong> (see <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>): the PoR status (TAR, counter, raw PoR), with the last command&rsquo;s status word and response data filled into the <strong>Response parser</strong> tab.</p>
<p class="text-sm mb-2">Incoming Proof-of-Receipt responses are decoded by the server — expanded Remote Application response data (TS 102 226 §5.2.2) or the compact format. The Secured Packet view shows the outcome after <strong>Send to Card</strong> (see <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>): the PoR status (TAR, counter, raw PoR), with the last command&rsquo;s status word and response data filled into the <strong>Response parser</strong> pill under Remote APDU.</p>
<h3 id="ram-gp" class="text-lg font-medium mb-2">2.4 RAM/GP</h3>
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Remote Application Management commands for card content management. Built with the same chain builder as SIM/USIM: add rows, fill fields, and the chain preview updates automatically.</p>
@@ -227,6 +228,17 @@
<p class="text-sm mb-2"><strong>Pack into Secured packet</strong> sends the built payload to the SCP80 tab for SPI/counter filling &mdash; insert the TAR the SD listens on (typically the OTASD TAR) there.</p>
<h3 id="response-parser" class="text-lg font-medium mb-2">2.8 Response parser</h3>
<p class="mb-3">Decodes a raw command response: pick the command that was sent, enter the SW (e.g. <code class="font-mono text-sm">9000</code>) and the response data hex, then press <strong>Decode</strong>. The fields are also auto-filled with the last command&rsquo;s status word and response data after a successful &ldquo;Send to Card&rdquo; (see <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>).</p>
<ul class="list-disc list-inside text-sm space-y-1">
<li><strong>Command</strong> — SIM/USIM group (SELECT, STATUS, READ/UPDATE, PIN ops, CAT commands like TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, &hellip;) or RAM/GP group (INSTALL, LOAD, DELETE, GET/STORE DATA, auth, SCP commands).</li>
<li><strong>SW decode</strong> — status words resolved against generic, UICC (TS 102 221), and GlobalPlatform maps, with context auto-detected.</li>
<li><strong>Privilege decode</strong> — GET DATA / INSTALL response payloads decode the privilege bytes into human-readable flags.</li>
<li><strong>Response data</strong> — raw hex rendered and interpreted per command (e.g. SELECT FCP templates).</li>
</ul>
<section class="mb-10">
<h2 id="scp80" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">3. SCP80 tab</h2>
<p class="mb-3">The <strong>SCP80</strong> top-level tab groups the SCP80-related views. It is switched by three pills: <strong>Secured Packet</strong>, <strong>Cards</strong>, and <strong>RAM</strong>. Assembles SCP80 secured packets per ETSI TS 102 225.</p>
@@ -301,21 +313,10 @@
<section class="mb-10">
<h2 id="response-parser" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Response parser tab</h2>
<p class="mb-3">Decodes a raw command response: pick the command that was sent, enter the SW (e.g. <code class="font-mono text-sm">9000</code>) and the response data hex, then press <strong>Decode</strong>. The fields are also auto-filled with the last command&rsquo;s status word and response data after a successful &ldquo;Send to Card&rdquo; (see <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;3.1</a>).</p>
<ul class="list-disc list-inside text-sm space-y-1">
<li><strong>Command</strong> — SIM/USIM group (SELECT, STATUS, READ/UPDATE, PIN ops, CAT commands like TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, &hellip;) or RAM/GP group (INSTALL, LOAD, DELETE, GET/STORE DATA, auth, SCP commands).</li>
<li><strong>SW decode</strong> — status words resolved against generic, UICC (TS 102 221), and GlobalPlatform maps, with context auto-detected.</li>
<li><strong>Privilege decode</strong> — GET DATA / INSTALL response payloads decode the privilege bytes into human-readable flags.</li>
<li><strong>Response data</strong> — raw hex rendered and interpreted per command (e.g. SELECT FCP templates).</li>
</ul>
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Card reader (pySim) tab</h2>
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> for live card operations: enter the server URL (default <code class="font-mono text-sm">http://127.0.0.1:8080</code>) and press <strong>Connect</strong>. The status area shows the reader/card state, and <strong>Equip card</strong> (re)initializes the card after insertion. Sub-tabs: <strong>File manager</strong>, <strong>Custom files</strong>, <strong>pySim command line</strong>, and <strong>Raw APDU</strong>. The <strong>Profiler</strong> and <strong>Phone simulator</strong> are separate top-level tabs.</p>
<section class="mb-10">
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">5. Card reader (pySim) tab</h2>
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> for live card operations: enter the server URL (default <code class="font-mono text-sm">http://127.0.0.1:8080</code>) and press <strong>Connect</strong>. The status area shows the reader/card state, and <strong>Equip card</strong> (re)initializes the card after insertion. Sub-tabs: <strong>File manager</strong>, <strong>Custom files</strong>, <strong>Profiler</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong>, and <strong>Proactive UICC</strong>.</p>
<h3 id="file-manager" class="text-lg font-medium mb-2">5.1 File manager</h3>
<h3 id="file-manager" class="text-lg font-medium mb-2">4.1 File manager</h3>
<p class="text-sm mb-2">The file system tree is displayed on the left; selecting a file opens its detail pane on the right.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Read</strong> — reads the selected file (auto-detects transparent vs record files)</li>
@@ -323,48 +324,55 @@
<li><strong>Raw / Decoded</strong> — toggle between hex dump and pySim-decoded JSON</li>
</ul>
<h3 id="custom-files" class="text-lg font-medium mb-2">5.2 Custom files</h3>
<h3 id="custom-files" class="text-lg font-medium mb-2">4.2 Custom files</h3>
<p class="text-sm mb-3">Add files that pySim&rsquo;s model does not cover: enter the full path (e.g. <code class="font-mono text-sm">3F00/7F20/6F46</code>) and an alias (e.g. <code class="font-mono text-sm">EF.SPN</code>), then press <strong>Add</strong>; added files appear in the File manager tree. The list persists in <code class="font-mono text-sm">localStorage</code> and can be shared with <strong>Export as JSON</strong> / <strong>Export to file</strong> and restored with <strong>Import from file</strong> / <strong>Paste &amp; import</strong> / <strong>Import JSON from clipboard</strong>.</p>
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">5.3 pySim command line</h3>
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">4.3 pySim command line</h3>
<p class="text-sm mb-3">Execute any pySim-shell command with usage hints (300&nbsp;ms) and autocomplete.</p>
<h3 id="raw-apdu" class="text-lg font-medium mb-2">5.4 Raw APDU</h3>
<h3 id="raw-apdu" class="text-lg font-medium mb-2">4.4 Raw APDU</h3>
<p class="text-sm mb-3">Send an arbitrary APDU and view the raw response.</p>
<h3 id="proactive-uicc" class="text-lg font-medium mb-2">5.5 Proactive UICC</h3>
<p class="text-sm mb-3">Interacts with the Card Application Toolkit session: the STK menu, subscribed events, the proactive command log, the PROVIDE LOCAL INFORMATION data dictionary, and STATUS polling.</p>
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">4.5 Usage scenarios</h3>
<h4 id="stk-menu" class="font-medium mb-1">5.5.1 STK menu</h4>
<p class="text-sm mb-3">When the card has issued a SET UP MENU command, a &ldquo;STK menu&rdquo; block appears at the top of this view with an emerald <strong>STK: &lt;title&gt;</strong> button that opens the menu overlay (same as the card&rsquo;s STK menu browser). If the card has not set up a menu, the block shows &ldquo;No menu set by the card&rdquo; instead. The menu state is refreshed each time the view is opened. User-interactive proactive commands always get a TERMINAL RESPONSE: the overlay pauses for your choice, and if you neither answer nor press <strong>Timeout</strong>, the server answers with a timeout result after the <code class="font-mono text-sm">--menu-timeout</code> seconds (default 60, <code class="font-mono text-sm">0</code> disables).</p>
<h4 id="scenario-a" class="font-medium mb-1">Scenario A &mdash; Working with files not in pySim&rsquo;s model (Custom files)</h4>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Obtain the FID of the target file (vendor documentation or ATR/file-system analysis; such files are often not in public specs).</li>
<li>Open the <strong>Card reader</strong> tab &rarr; <strong>Custom files</strong> sub-tab.</li>
<li>Enter the full path (e.g. <code class="font-mono text-sm">3F00/7F20/6F46</code>) and an alias (e.g. <code class="font-mono text-sm">EF.SPN</code>).</li>
<li>Click <strong>Add</strong> — the file appears in the tree in italics (unverified).</li>
<li>Click the file to verify existence; on success (<code class="font-mono text-sm">9000</code>) it behaves like a normal file.</li>
<li>Read, edit and save hex data; toggle Raw/Decoded views.</li>
<li>Export the custom-file list as JSON to share with other machines.</li>
</ol>
<h4 id="subscribed-events" class="font-medium mb-1">5.5.2 Subscribed events (SET UP EVENT LIST)</h4>
<p class="text-sm mb-2">The events the card monitors. Each event has a <strong>Send</strong> button that opens a form specific to the event type:</p>
<h4 id="scenario-b" class="font-medium mb-1">Scenario B &mdash; Simulating a real network environment for SIM testing</h4>
<p class="text-sm mb-1"><strong>B.1 Answer PROVIDE LOCAL INFORMATION (PLI)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Open <strong>Phone simulator</strong> &rarr; <strong>PROVIDE LOCAL INFORMATION response data</strong>.</li>
<li>Use the decode/encode forms to set IMEI (<code class="font-mono text-sm">01</code>), Location Info (<code class="font-mono text-sm">00</code>), Access Technology (<code class="font-mono text-sm">06</code>), etc.</li>
<li>Click <strong>Save</strong> — values persist server-side.</li>
<li>Enable <strong>Polling</strong> (interval 30&nbsp;s) so the card issues PLI periodically.</li>
<li>The server injects the dictionary values into each TERMINAL RESPONSE.</li>
<li>Verify in the proactive log: the PLI entry shows the decoded response.</li>
</ol>
<p class="text-sm mb-1"><strong>B.2 Simulate network actions via ENVELOPE (event download)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Check the <strong>subscribed events</strong> list (from SET UP EVENT LIST).</li>
<li>Click <strong>Send</strong> on an event (e.g. Location Status) and fill the form; an <code class="font-mono text-sm">ENVELOPE(Event Download)</code> is sent.</li>
<li>For <strong>Network Rejection</strong>, select registration type &rarr; location fields &rarr; access technology &rarr; rejection cause.</li>
<li>The card may respond with a proactive command, which the chain handler logs and answers automatically.</li>
</ol>
<p class="text-sm mb-1"><strong>B.3 Verify the simulated environment</strong></p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>No-data events</strong> (User Activity, Idle Screen, Data Available, &hellip;) — one-click notification</li>
<li><strong>Location Status</strong> — dropdown: Normal / Limited / No service (tag <code class="font-mono text-sm">9B</code>)</li>
<li><strong>Access Technology Change</strong> — 13 RAT types (tag <code class="font-mono text-sm">BF</code>)</li>
<li><strong>Network Rejection</strong> — full adaptive form: registration type (LU / GPRS / EPS / 5GS), location fields (MCC, MNC, LAC, RAC, TAC), access technology, and a 53-cause unified rejection cause dropdown covering EMM, GMM, 5GMM and LU causes</li>
<li>The proactive command log shows the full round-trip (command + TERMINAL RESPONSE bytes).</li>
<li>The STATUS button / auto-polling keep the CAT session alive (drain loop).</li>
</ul>
<p class="text-sm mb-3">Sending an event uses <code class="font-mono text-sm">ENVELOPE(Event Download)</code> per TS 102 223 / TS 131 111.</p>
<h4 id="proactive-log" class="font-medium mb-1">5.5.3 Proactive command log</h4>
<p class="text-sm mb-2">Chronological list of fetched proactive commands. Each row shows the elapsed time, type code, name, and a decoded qualifier (for commands that have one). Commands with response data show a <code class="font-mono text-sm">Response:</code> line with the TERMINAL RESPONSE bytes (boilerplate TLVs stripped); PROVIDE LOCAL INFORMATION responses are decoded using the PLI data dictionary decoders.</p>
<h4 id="pli-dict" class="font-medium mb-1">5.5.4 PROVIDE LOCAL INFORMATION data dictionary</h4>
<p class="text-sm mb-2">Editable hex values for all 22 PLI qualifiers (TS 102 223 &sect;8.6 + TS 131 111). Ten qualifiers have inline decode/encode forms:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>00</strong> Location Info (MCC, MNC, LAC/TAC, Cell ID)</li>
<li><strong>01</strong> IMEI &middot; <strong>03</strong> Date/Time/TZ &middot; <strong>04</strong> Language &middot; <strong>05</strong> Timing Advance</li>
<li><strong>06</strong> Access Technology &middot; <strong>08</strong> IMEISV &middot; <strong>09</strong> Search Mode</li>
<li><strong>0A</strong> Battery &middot; <strong>0E</strong> Multiple Access Technologies</li>
</ul>
<p class="text-sm mb-3">Values persist server-side until restart. When the card issues PLI, the server injects the dictionary values into the TERMINAL RESPONSE.</p>
<h4 id="status-polling" class="font-medium mb-1">5.5.5 STATUS polling</h4>
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). A <strong>Polling</strong> toggle enables background polling: after a configurable idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;s, default 30&nbsp;s, <code class="font-mono text-sm">0</code> disables polling) the server sends STATUS and handles any pending proactive command. Polling stops and card state resets if the card is removed.</p>
<h3 id="profiler" class="text-lg font-medium mb-2">5.6 Profiler</h3>
<section class="mb-10">
<h2 id="profiler" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">5. Profiler</h2>
<p class="text-sm mb-2">Verifies that a card matches a named <strong>profile</strong> — an ordered set of rules describing the expected file system and (optionally) file contents. Profiles are stored in <code class="font-mono text-sm">localStorage</code>.</p>
<h4 class="font-medium mb-1">Profile list</h4>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
@@ -386,57 +394,58 @@
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>&ldquo;FCP/FCI check&rdquo;</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>&ldquo;Ignore contents of files&rdquo;</strong> checklist (all checked by default except <code class="font-mono text-sm">EF.ARR</code>; the header checkbox checks or unchecks the whole list) of frequently-overwritten files whose contents are skipped: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>, <code class="font-mono text-sm">EF.ACC</code>, <code class="font-mono text-sm">EF.EPSNSC</code>, <code class="font-mono text-sm">EF.START-HFN</code>, <code class="font-mono text-sm">EF.ARR</code>. Two further checked-by-default options <strong>&ldquo;Match first 4 bytes for&rdquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> and <code class="font-mono text-sm">EF.ICCID</code> capture those files&rsquo; contents as a mask of only the first 4 bytes (uncheck for exact matching). A progress line shows <em>N / total files</em> with the current file path while scanning; during the scan the options are hidden and the buttons are locked. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the <strong>Custom files</strong> sub-tab are included under the same existence check.</p>
<h4 id="card-snapshots" class="font-medium mb-1">Card snapshots</h4>
<p class="text-sm mb-2">The list view has two tabs &mdash; <strong>Profiles</strong> and <strong>Card snapshots</strong>. A card snapshot is an immutable capture of the card filesystem: for every existing file it stores the path, symbolic name, file type, size (or record length/count), the raw FCI from the SELECT response, and the contents whenever the file is readable (no ignore list, no masking). The ICCID is decoded from EF.ICCID and shown next to the snapshot name.</p>
<p class="text-sm mb-2">The list view has two tabs &mdash; <strong>Profiles</strong> and <strong>Card snapshots</strong>. A card snapshot is an immutable capture of the card filesystem: for every existing file it stores the path, symbolic name, file type, size (or record length/count), the raw FCI from the SELECT response, and the contents whenever the file is readable (no ignore list, no masking). The ICCID is decoded from EF.ICCID and shown next to the snapshot name. The scan also measures each card command (SELECT, READ BINARY, READ RECORD) from command to response; the snapshot stores min/avg/max per command type and the total scan time, and the view shows these in the summary under the title plus the select/read times per file and the read time per record. Timings are informational only and are not used by checks or comparisons.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>New snapshot</strong> &mdash; asks for a name and scans the card, then returns to the list.</li>
<li><strong>Import snapshot</strong> &mdash; loads a snapshot from a JSON file.</li>
<li>Each snapshot row has <strong>Open</strong>, <strong>Export</strong>, and <strong>Delete</strong>. <strong>Open</strong> shows all captured data read-only (raw FCI with the decoded FCI, contents); only the snapshot name is editable.</li>
<li><strong>Check card snapshot</strong> on a profile row runs the profile rules against a snapshot you pick from the list, without a card reader. The report is the same as a live check; files whose contents were not captured during the scan are reported as unverifiable errors.</li>
<li><strong>Compare snapshots</strong> compares two snapshots offline, exactly like a profile check: pick the <em>master</em> snapshot and the <em>snapshot to check</em>, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same pass/fail report. Files present only in the checked snapshot are reported as extra files. Back to list returns to the Card snapshots tab.</li>
<li><strong>Compare snapshots</strong> compares two snapshots offline, exactly like a profile check: pick the <em>master</em> snapshot and the <em>snapshot to check</em>, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same pass/fail report; in that report the mismatch fields and the FCI comparison columns are labeled with the master and checked snapshot names instead of expected/actual. Files present only in the checked snapshot are reported as extra files. Back to list returns to the Card snapshots tab.</li>
</ul>
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Usage scenarios</h3>
<h4 id="scenario-a" class="font-medium mb-1">Scenario A &mdash; Working with files not in pySim&rsquo;s model (Custom files)</h4>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Obtain the FID of the target file (vendor documentation or ATR/file-system analysis; such files are often not in public specs).</li>
<li>Open the <strong>Card reader</strong> tab &rarr; <strong>Custom files</strong> sub-tab.</li>
<li>Enter the full path (e.g. <code class="font-mono text-sm">3F00/7F20/6F46</code>) and an alias (e.g. <code class="font-mono text-sm">EF.SPN</code>).</li>
<li>Click <strong>Add</strong> — the file appears in the tree in italics (unverified).</li>
<li>Click the file to verify existence; on success (<code class="font-mono text-sm">9000</code>) it behaves like a normal file.</li>
<li>Read, edit and save hex data; toggle Raw/Decoded views.</li>
<li>Export the custom-file list as JSON to share with other machines.</li>
</ol>
<h4 id="scenario-b" class="font-medium mb-1">Scenario B &mdash; Simulating a real network environment for SIM testing</h4>
<p class="text-sm mb-1"><strong>B.1 Answer PROVIDE LOCAL INFORMATION (PLI)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Open <strong>Proactive UICC</strong> &rarr; <strong>PROVIDE LOCAL INFORMATION response data</strong>.</li>
<li>Use the decode/encode forms to set IMEI (<code class="font-mono text-sm">01</code>), Location Info (<code class="font-mono text-sm">00</code>), Access Technology (<code class="font-mono text-sm">06</code>), etc.</li>
<li>Click <strong>Save</strong> — values persist server-side.</li>
<li>Enable <strong>Polling</strong> (interval 30&nbsp;s) so the card issues PLI periodically.</li>
<li>The server injects the dictionary values into each TERMINAL RESPONSE.</li>
<li>Verify in the proactive log: the PLI entry shows the decoded response.</li>
</ol>
<p class="text-sm mb-1"><strong>B.2 Simulate network actions via ENVELOPE (event download)</strong></p>
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
<li>Check the <strong>subscribed events</strong> list (from SET UP EVENT LIST).</li>
<li>Click <strong>Send</strong> on an event (e.g. Location Status) and fill the form; an <code class="font-mono text-sm">ENVELOPE(Event Download)</code> is sent.</li>
<li>For <strong>Network Rejection</strong>, select registration type &rarr; location fields &rarr; access technology &rarr; rejection cause.</li>
<li>The card may respond with a proactive command, which the chain handler logs and answers automatically.</li>
</ol>
<p class="text-sm mb-1"><strong>B.3 Verify the simulated environment</strong></p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li>The proactive command log shows the full round-trip (command + TERMINAL RESPONSE bytes).</li>
<li>The STATUS button / auto-polling keep the CAT session alive (drain loop).</li>
</ul>
</section>
<section class="mb-10">
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Server installation</h2>
<p class="mb-3">Live card operations (Card reader tab, Proactive UICC, OTA delivery) require the local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — a small HTTP server bundled with OTAMan that wraps pySim, talks to the reader over PC/SC or serial, and also serves the PWA itself (open <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
<h2 id="proactive-uicc" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Phone simulator</h2>
<p class="text-sm mb-3">Interacts with the Card Application Toolkit session. The view has two pills: <strong>Phone</strong> (STK menu, STATUS and polling, subscribed events, proactive command log) and <strong>TR Config</strong> (response data injected into TERMINAL RESPONSEs for proactive commands).</p>
<h3 id="prerequisites" class="text-lg font-medium mb-2">6.1 Prerequisites</h3>
<h3 id="stk-menu" class="text-lg font-medium mb-2">6.1 STK menu</h3>
<p class="text-sm mb-3">When the card has issued a SET UP MENU command, a &ldquo;STK menu&rdquo; block appears at the top of this view with an emerald <strong>STK: &lt;title&gt;</strong> button that opens the menu overlay (same as the card&rsquo;s STK menu browser). If the card has not set up a menu, the block shows &ldquo;No menu set by the card&rdquo; instead. The menu state is refreshed each time the view is opened. User-interactive proactive commands always get a TERMINAL RESPONSE: the overlay pauses for your choice, and if you neither answer nor press <strong>Timeout</strong>, the server answers with a timeout result after the <code class="font-mono text-sm">--menu-timeout</code> seconds (default 60, <code class="font-mono text-sm">0</code> disables).</p>
<h3 id="subscribed-events" class="text-lg font-medium mb-2">6.2 Subscribed events (SET UP EVENT LIST)</h3>
<p class="text-sm mb-2">The events the card monitors. Each event has a <strong>Send</strong> button that opens a form specific to the event type:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>No-data events</strong> (User Activity, Idle Screen, Data Available, &hellip;) — one-click notification</li>
<li><strong>Location Status</strong> — dropdown: Normal / Limited / No service (tag <code class="font-mono text-sm">9B</code>)</li>
<li><strong>Access Technology Change</strong> — 13 RAT types (tag <code class="font-mono text-sm">BF</code>)</li>
<li><strong>Network Rejection</strong> — full adaptive form: registration type (LU / GPRS / EPS / 5GS), location fields (MCC, MNC, LAC, RAC, TAC), access technology, and a 53-cause unified rejection cause dropdown covering EMM, GMM, 5GMM and LU causes</li>
</ul>
<p class="text-sm mb-3">Sending an event uses <code class="font-mono text-sm">ENVELOPE(Event Download)</code> per TS 102 223 / TS 131 111.</p>
<h3 id="proactive-log" class="text-lg font-medium mb-2">6.3 Proactive command log</h3>
<p class="text-sm mb-2">Chronological list of fetched proactive commands. Each row shows the elapsed time, type code, name, and a decoded qualifier (for commands that have one). Commands with response data show a <code class="font-mono text-sm">Response:</code> line with the TERMINAL RESPONSE bytes (boilerplate TLVs stripped); PROVIDE LOCAL INFORMATION responses are decoded using the PLI data dictionary decoders.</p>
<h3 id="status-polling" class="text-lg font-medium mb-2">6.4 STATUS polling</h3>
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). A <strong>Polling</strong> toggle enables background polling: after a configurable idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;s, default 30&nbsp;s, <code class="font-mono text-sm">0</code> disables polling) the server sends STATUS and handles any pending proactive command. Polling stops and card state resets if the card is removed.</p>
<h3 id="pli-dict" class="text-lg font-medium mb-2">6.5 TR Config &mdash; PROVIDE LOCAL INFORMATION response data</h3>
<p class="text-sm mb-2">Editable hex values for all 22 PLI qualifiers (TS 102 223 &sect;8.6 + TS 131 111). Ten qualifiers have inline decode/encode forms:</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>00</strong> Location Info (MCC, MNC, LAC/TAC, Cell ID)</li>
<li><strong>01</strong> IMEI &middot; <strong>03</strong> Date/Time/TZ &middot; <strong>04</strong> Language &middot; <strong>05</strong> Timing Advance</li>
<li><strong>06</strong> Access Technology &middot; <strong>08</strong> IMEISV &middot; <strong>09</strong> Search Mode</li>
<li><strong>0A</strong> Battery &middot; <strong>0E</strong> Multiple Access Technologies</li>
</ul>
<p class="text-sm mb-3">Values persist server-side until restart. When the card issues PLI, the server injects the dictionary values into the TERMINAL RESPONSE.</p>
</section>
<section class="mb-10">
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">7. Server installation</h2>
<p class="mb-3">Live card operations (Card reader tab, Phone simulator, OTA delivery) require the local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> — a small HTTP server bundled with OTAMan that wraps pySim, talks to the reader over PC/SC or serial, and also serves the PWA itself (open <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
<h3 id="prerequisites" class="text-lg font-medium mb-2">7.1 Prerequisites</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Python 3.8+</strong> with <code class="font-mono text-sm">pip</code></li>
<li><strong>Git</strong></li>
@@ -444,20 +453,20 @@
<li><strong>Windows only</strong> — use <strong>Python 3.10&ndash;3.13</strong> (3.13 recommended): <code class="font-mono text-sm">pyscard</code> (the PC/SC driver wrapper) ships precompiled wheels for these versions. On Python 3.9 / 3.14 pip builds <code class="font-mono text-sm">pyscard</code> from source, which requires Microsoft C++ Build Tools (&ldquo;Desktop development with C++&rdquo;). The SMPP bridge (<code class="font-mono text-sm">smpp.twisted3</code>) is intentionally not installed on Windows, so no C++ Build Tools are needed for Python 3.10&ndash;3.13.</li>
</ul>
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">6.2 Quick start — Linux / macOS</h3>
<h3 id="quickstart-linux" class="text-lg font-medium mb-2">7.2 Quick start — Linux / macOS</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
cd otaman
chmod +x setup.sh start.sh
./setup.sh # creates .venv, installs pysim + server (run once)
./start.sh # starts the server (serves PWA + API, auto-detects reader)</pre>
<h3 id="quickstart-windows" class="text-lg font-medium mb-2">6.3 Quick start — Windows</h3>
<h3 id="quickstart-windows" class="text-lg font-medium mb-2">7.3 Quick start — Windows</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3">git clone https://github.com/anttro/otaman.git
cd otaman
setup.bat # creates .venv, installs pysim + server (run once)
start.bat # starts the server (serves PWA + API)</pre>
<h3 id="helper-scripts" class="text-lg font-medium mb-2">6.4 Helper scripts</h3>
<h3 id="helper-scripts" class="text-lg font-medium mb-2">7.4 Helper scripts</h3>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Script</th><th class="text-left py-1 px-2">Purpose</th></tr></thead>
<tbody>
@@ -466,7 +475,7 @@ start.bat # starts the server (serves PWA + API)</pre>
</tbody>
</table>
<h3 id="reader-autodetect" class="text-lg font-medium mb-2">6.5 Reader auto-detection</h3>
<h3 id="reader-autodetect" class="text-lg font-medium mb-2">7.5 Reader auto-detection</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>PC/SC (Linux)</strong><code class="font-mono text-sm">start.sh</code> passes <code class="font-mono text-sm">-p 0</code> when the <code class="font-mono text-sm">pcscd</code> daemon is running</li>
<li><strong>PC/SC (Windows)</strong><code class="font-mono text-sm">start.bat</code> always uses <code class="font-mono text-sm">-p 0</code> (PC/SC is built into Windows)</li>
@@ -475,7 +484,7 @@ start.bat # starts the server (serves PWA + API)</pre>
</ul>
<p class="text-sm mb-3">If no card is present, the Card reader tab shows &ldquo;No card detected&rdquo;. Insert the card and click <strong>Equip card</strong> to initialize it.</p>
<h3 id="manual-install" class="text-lg font-medium mb-2">6.6 Manual installation</h3>
<h3 id="manual-install" class="text-lg font-medium mb-2">7.6 Manual installation</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3"># Create and activate a venv
python3 -m venv .venv
source .venv/bin/activate # Linux/macOS
@@ -494,7 +503,7 @@ pysim-otaman-server --http-port 8080</pre>
<section class="mb-10">
<h2 id="compatibility" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">7. Version compatibility</h2>
<h2 id="compatibility" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">8. Version compatibility</h2>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">PWA (OTAMan)</th><th class="text-left py-1 px-2">Server</th><th class="text-left py-1 px-2">Status</th></tr></thead>
<tbody>
+333 -153
View File
@@ -18,7 +18,7 @@
<div class="max-w-7xl mx-auto px-6 py-2">
<div class="flex items-center justify-between mb-3">
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.9.28</span></h1>
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.0.0</span></h1>
<div class="flex items-center gap-4">
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
@@ -28,23 +28,25 @@
</div>
</div>
<div class="flex gap-1 mb-4 border-b border-gray-300 dark:border-slate-600">
<button class="tab-btn active px-4 py-1.5 text-sm rounded-t bg-blue-600 dark:bg-blue-500 text-white dark:text-white" data-tab="c-apdu">C-APDU</button>
<div class="flex flex-wrap gap-1 mb-4 border-b border-gray-300 dark:border-slate-600">
<button class="tab-btn active px-4 py-1.5 text-sm rounded-t bg-blue-600 dark:bg-blue-500 text-white dark:text-white" data-tab="c-apdu">Remote APDU</button>
<button class="tab-btn px-4 py-1.5 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="scp80">SCP80</button>
<button class="tab-btn px-4 py-1.5 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="response" data-l10n="Response parser">Response parser</button>
<button class="tab-btn px-4 py-1.5 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="pysim" data-l10n="Card reader">Card reader</button>
<button class="tab-btn px-4 py-1.5 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="profiler" data-l10n="Profiler">Profiler</button>
<button class="tab-btn px-4 py-1.5 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="phone" data-l10n="Phone simulator">Phone simulator</button>
</div>
<div id="tab-c-apdu" class="tab-content">
<div class="flex gap-4">
<div class="flex-1">
<div class="flex justify-center gap-2 mb-3">
<div class="flex flex-wrap justify-center gap-2 mb-3">
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-blue-600 text-white" data-sub="sim" onclick="cApduSwitchSubtab('sim')">SIM RFM</button>
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="usim" onclick="cApduSwitchSubtab('usim')">USIM RFM</button>
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="ber" onclick="cApduSwitchSubtab('ber')">Expanded Script</button>
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="ram" onclick="cApduSwitchSubtab('ram')">RAM/GP</button>
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="httpota" onclick="cApduSwitchSubtab('httpota')">HTTP OTA</button>
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="parse" onclick="cApduSwitchSubtab('parse')" data-l10n="C-APDU Parser">C-APDU Parser</button>
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="response" onclick="cApduSwitchSubtab('response')" data-l10n="Response parser">Response parser</button>
</div>
<div id="c-apdu-sub-sim">
<div id="chain-sim-rows"></div>
@@ -256,6 +258,76 @@
<textarea id="hota-preview" rows="5" readonly oninput="hotaPreviewEdited()" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800 mb-3" placeholder="RAM-over-HTTP payload will appear here..."></textarea>
<button onclick="packToSp('hota-preview')" id="hota-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
</div>
<div id="c-apdu-sub-response" class="hidden">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Command">Command</label>
<select id="resp-cmd" onchange="updateRespCmd()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="" data-l10n="— Select command —">— Select command —</option>
<optgroup label="SIM / USIM">
<option value="sim-select">SELECT</option>
<option value="sim-status">STATUS</option>
<option value="sim-read-binary">READ BINARY</option>
<option value="sim-update-binary">UPDATE BINARY</option>
<option value="sim-read-record">READ RECORD</option>
<option value="sim-update-record">UPDATE RECORD</option>
<option value="sim-verify">VERIFY PIN</option>
<option value="sim-change">CHANGE PIN</option>
<option value="sim-disable">DISABLE PIN</option>
<option value="sim-enable">ENABLE PIN</option>
<option value="sim-unblock">UNBLOCK PIN</option>
<option value="sim-deactivate">DEACTIVATE FILE</option>
<option value="sim-activate">ACTIVATE FILE</option>
<option value="sim-challenge">GET CHALLENGE</option>
<option value="sim-auth">AUTHENTICATE</option>
<option value="sim-profile">TERMINAL PROFILE</option>
<option value="sim-envelope">ENVELOPE</option>
<option value="sim-fetch">FETCH</option>
<option value="sim-terminal-rsp">TERMINAL RESPONSE</option>
<option value="sim-manage-channel">MANAGE CHANNEL</option>
<option value="sim-msc">MANAGE SECURE CHANNEL</option>
<option value="sim-retrieve">RETRIEVE DATA</option>
<option value="sim-set-data">SET DATA</option>
<option value="sim-suspend">SUSPEND UICC</option>
</optgroup>
<optgroup label="RAM (GlobalPlatform)">
<option value="ram-install-load">INSTALL [for load]</option>
<option value="ram-install-install">INSTALL [for install]</option>
<option value="ram-install-make-sel">INSTALL [for make selectable]</option>
<option value="ram-install-reg-update">INSTALL [for registry update]</option>
<option value="ram-install-extradition">INSTALL [for extradition]</option>
<option value="ram-load">LOAD</option>
<option value="ram-delete">DELETE</option>
<option value="ram-get-status">GET STATUS</option>
<option value="ram-get-data">GET DATA</option>
<option value="ram-store-data">STORE DATA</option>
<option value="ram-set-status">SET STATUS</option>
<option value="ram-ext-auth">EXTERNAL AUTHENTICATE</option>
<option value="ram-int-auth">INTERNAL AUTHENTICATE</option>
<option value="ram-select">SELECT</option>
<option value="ram-init-update">INITIALIZE UPDATE</option>
<option value="ram-put-key">PUT KEY</option>
<option value="ram-manage-channel">MANAGE CHANNEL</option>
</optgroup>
</select>
</div>
<div class="flex gap-3 mb-3">
<div class="w-24">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">SW</label>
<input id="resp-sw" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" maxlength="4" placeholder="9000">
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Context</label>
<input id="resp-ctx" readonly class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800" value="generic">
</div>
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Response data (hex)">Response data (hex)</label>
<textarea id="resp-data" rows="3" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="E3 1A 4F 08 A0 00 00 00 03 00 00 00 9F 70 01 07 C5 03 80 00 00"></textarea>
</div>
<button onclick="decodeResponse()" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Decode">Decode</button>
<div id="resp-output" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800 whitespace-pre-wrap" style="min-height:100px"></div>
</div>
</div>
<div class="w-1/4">
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800">
@@ -661,75 +733,6 @@
</div>
</div>
<div id="tab-response" class="tab-content hidden">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Command">Command</label>
<select id="resp-cmd" onchange="updateRespCmd()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="" data-l10n="— Select command —">— Select command —</option>
<optgroup label="SIM / USIM">
<option value="sim-select">SELECT</option>
<option value="sim-status">STATUS</option>
<option value="sim-read-binary">READ BINARY</option>
<option value="sim-update-binary">UPDATE BINARY</option>
<option value="sim-read-record">READ RECORD</option>
<option value="sim-update-record">UPDATE RECORD</option>
<option value="sim-verify">VERIFY PIN</option>
<option value="sim-change">CHANGE PIN</option>
<option value="sim-disable">DISABLE PIN</option>
<option value="sim-enable">ENABLE PIN</option>
<option value="sim-unblock">UNBLOCK PIN</option>
<option value="sim-deactivate">DEACTIVATE FILE</option>
<option value="sim-activate">ACTIVATE FILE</option>
<option value="sim-challenge">GET CHALLENGE</option>
<option value="sim-auth">AUTHENTICATE</option>
<option value="sim-profile">TERMINAL PROFILE</option>
<option value="sim-envelope">ENVELOPE</option>
<option value="sim-fetch">FETCH</option>
<option value="sim-terminal-rsp">TERMINAL RESPONSE</option>
<option value="sim-manage-channel">MANAGE CHANNEL</option>
<option value="sim-msc">MANAGE SECURE CHANNEL</option>
<option value="sim-retrieve">RETRIEVE DATA</option>
<option value="sim-set-data">SET DATA</option>
<option value="sim-suspend">SUSPEND UICC</option>
</optgroup>
<optgroup label="RAM (GlobalPlatform)">
<option value="ram-install-load">INSTALL [for load]</option>
<option value="ram-install-install">INSTALL [for install]</option>
<option value="ram-install-make-sel">INSTALL [for make selectable]</option>
<option value="ram-install-reg-update">INSTALL [for registry update]</option>
<option value="ram-install-extradition">INSTALL [for extradition]</option>
<option value="ram-load">LOAD</option>
<option value="ram-delete">DELETE</option>
<option value="ram-get-status">GET STATUS</option>
<option value="ram-get-data">GET DATA</option>
<option value="ram-store-data">STORE DATA</option>
<option value="ram-set-status">SET STATUS</option>
<option value="ram-ext-auth">EXTERNAL AUTHENTICATE</option>
<option value="ram-int-auth">INTERNAL AUTHENTICATE</option>
<option value="ram-select">SELECT</option>
<option value="ram-init-update">INITIALIZE UPDATE</option>
<option value="ram-put-key">PUT KEY</option>
<option value="ram-manage-channel">MANAGE CHANNEL</option>
</optgroup>
</select>
</div>
<div class="flex gap-3 mb-3">
<div class="w-24">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">SW</label>
<input id="resp-sw" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" maxlength="4" placeholder="9000">
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Context</label>
<input id="resp-ctx" readonly class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800" value="generic">
</div>
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Response data (hex)">Response data (hex)</label>
<textarea id="resp-data" rows="3" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="E3 1A 4F 08 A0 00 00 00 03 00 00 00 9F 70 01 07 C5 03 80 00 00"></textarea>
</div>
<button onclick="decodeResponse()" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Decode">Decode</button>
<div id="resp-output" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800 whitespace-pre-wrap" style="min-height:100px"></div>
</div>
<div id="tab-pysim" class="tab-content hidden">
<div id="pysim-connect-row" class="mb-3">
@@ -762,10 +765,8 @@
<div class="flex gap-1 mb-2">
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-blue-600 text-white" data-pysim-sub="files" data-l10n="File manager">File manager</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="custom" data-l10n="Custom files">Custom files</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="profiler" data-l10n="Profiler">Profiler</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd" data-l10n="pySim command line">pySim command line</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu" data-l10n="Raw APDU">Raw APDU</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="proactive">Proactive UICC</button>
</div>
<div id="pysim-sub-files">
@@ -811,7 +812,33 @@
<textarea id="pysim-cf-io" class="hidden mt-2 w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1.5 bg-gray-100 dark:bg-slate-800" rows="6"></textarea>
</div>
<div id="pysim-sub-profiler" class="hidden">
<div id="pysim-sub-cmd" class="hidden">
<div class="flex gap-2 items-center mb-2">
<div class="relative flex-1">
<input id="pysim-cmd" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="select MF" autocomplete="off" onkeydown="pysimCmdKeydown(event)" oninput="pysimCmdInput()">
<div id="pysim-cmd-suggest" class="hidden absolute bottom-full left-0 right-0 mb-1 max-h-32 overflow-y-auto border border-gray-300 dark:border-slate-600 rounded bg-white dark:bg-slate-800 text-xs font-mono shadow-lg z-10"></div>
</div>
<button onclick="pysimExec()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700" data-l10n="Execute">Execute</button>
</div>
<div id="pysim-cmd-hint" class="hidden mb-2 text-xs font-mono text-gray-500 dark:text-slate-400 border border-gray-200 dark:border-slate-700 rounded p-2 bg-gray-50 dark:bg-slate-800 whitespace-pre-wrap"></div>
<textarea id="pysim-cmd-output" rows="10" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
</div>
<div id="pysim-sub-apdu" class="hidden">
<div class="flex gap-2 items-center mb-2">
<input id="pysim-apdu" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="00A4040000" onkeydown="if(event.key==='Enter')pysimApdu()">
<button onclick="pysimApdu()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700" data-l10n="Send">Send</button>
</div>
<textarea id="pysim-apdu-output" rows="5" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
</div>
</div>
</div>
<div id="tab-profiler" class="tab-content hidden">
<div id="profiler-list">
<div class="flex gap-1 mb-3 border-b border-gray-300 dark:border-slate-600">
<button class="profiler-list-tab px-4 py-1.5 text-sm rounded-t bg-blue-600 dark:bg-blue-500 text-white dark:text-white" data-list-tab="profiles" onclick="profilerListSwitch('profiles')" data-l10n="Profiles">Profiles</button>
@@ -865,61 +892,53 @@
<span id="snapshot-iccid" class="text-xs font-mono text-gray-500 dark:text-slate-400"></span>
<button onclick="snapshotSaveName()" class="px-2.5 py-1 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700" data-l10n="Save">Save</button>
</div>
<div id="snapshot-summary" class="mb-3"></div>
<div id="snapshot-files"></div>
</div>
</div>
<div id="pysim-sub-cmd" class="hidden">
<div class="flex gap-2 items-center mb-2">
<div class="relative flex-1">
<input id="pysim-cmd" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="select MF" autocomplete="off" onkeydown="pysimCmdKeydown(event)" oninput="pysimCmdInput()">
<div id="pysim-cmd-suggest" class="hidden absolute bottom-full left-0 right-0 mb-1 max-h-32 overflow-y-auto border border-gray-300 dark:border-slate-600 rounded bg-white dark:bg-slate-800 text-xs font-mono shadow-lg z-10"></div>
</div>
<button onclick="pysimExec()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700" data-l10n="Execute">Execute</button>
</div>
<div id="pysim-cmd-hint" class="hidden mb-2 text-xs font-mono text-gray-500 dark:text-slate-400 border border-gray-200 dark:border-slate-700 rounded p-2 bg-gray-50 dark:bg-slate-800 whitespace-pre-wrap"></div>
<textarea id="pysim-cmd-output" rows="10" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
<div id="tab-phone" class="tab-content hidden">
<div class="flex flex-wrap justify-center gap-2 mb-3">
<button class="phone-subtab px-4 py-1.5 text-sm rounded-full bg-blue-600 text-white" data-phone-sub="phone" onclick="phoneSwitchSubtab('phone')" data-l10n="Phone">Phone</button>
<button class="phone-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-phone-sub="tr" onclick="phoneSwitchSubtab('tr')" data-l10n="TR Config">TR Config</button>
</div>
<div id="pysim-sub-apdu" class="hidden">
<div class="flex gap-2 items-center mb-2">
<input id="pysim-apdu" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="00A4040000" onkeydown="if(event.key==='Enter')pysimApdu()">
<button onclick="pysimApdu()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700" data-l10n="Send">Send</button>
</div>
<textarea id="pysim-apdu-output" rows="5" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
</div>
<div id="pysim-sub-proactive" class="hidden">
<div class="flex gap-6">
<div>
<div id="phone-sub-phone">
<div class="flex flex-wrap items-start gap-4 mb-4">
<div class="border border-gray-200 dark:border-slate-700 rounded p-3">
<div class="mb-1 text-sm text-gray-500 dark:text-slate-400" data-l10n="STK menu">STK menu</div>
<div class="flex items-center gap-2 mb-3">
<button id="stk-menu-btn" onclick="stkMenuOpen()" class="px-2.5 py-1 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700" style="display:none">STK: <span id="stk-menu-title"></span></button>
<span id="stk-menu-none" class="text-xs text-gray-500 dark:text-slate-400" style="display:none" data-l10n="No menu set by the card">No menu set by the card</span>
<div class="flex items-center gap-2">
<button id="stk-menu-btn" onclick="stkMenuOpen()" class="px-2.5 py-1 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700" style="display:none">STK: <span id="stk-menu-title"></span></button>
<span id="stk-menu-none" class="text-xs text-gray-500 dark:text-slate-400" style="display:none" data-l10n="No menu set by the card">No menu set by the card</span>
</div>
</div>
<div class="mb-1 text-sm text-gray-500 dark:text-slate-400" data-l10n="STATUS and Polling">STATUS and Polling</div>
<div class="flex items-center gap-2 mb-3">
<div class="border border-gray-200 dark:border-slate-700 rounded p-3">
<div class="mb-1 text-sm text-gray-500 dark:text-slate-400" data-l10n="STATUS and Polling">STATUS and Polling</div>
<div class="flex items-center gap-2">
<button id="pli-status-btn" onclick="pysimStatusPoll()" class="px-2.5 py-1 text-sm rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Send STATUS">Send STATUS</button>
<span class="text-xs text-gray-500 dark:text-slate-400">|</span>
<span class="text-xs text-gray-500 dark:text-slate-400" data-l10n="Polling">Polling</span>
<button id="pli-pause-btn" onclick="pysimPollToggle()" class="px-2.5 py-1 text-sm rounded bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600">OFF</button>
<span id="pli-poll-stat" class="text-xs text-gray-400"></span>
</div>
<div class="mt-2 mb-2 text-sm text-gray-500 dark:text-slate-400" data-l10n="Events that the card monitors (SET UP EVENT LIST):">Events that the card monitors (SET UP EVENT LIST):</div>
<div id="pysim-event-list" class="text-sm font-mono text-gray-600 dark:text-slate-400"></div>
<div class="mt-4 mb-2 text-sm text-gray-500 dark:text-slate-400" data-l10n="Fetched proactive commands:">Fetched proactive commands:</div>
<div id="pysim-proactive-log" class="text-sm font-mono text-gray-600 dark:text-slate-400"></div>
</div>
<div>
<div class="flex justify-between items-center mb-2">
<span class="text-sm text-gray-500 dark:text-slate-400" data-l10n="PROVIDE LOCAL INFORMATION response data">PROVIDE LOCAL INFORMATION response data</span>
<button id="pli-save-btn" onclick="pysimPliSave()" class="px-2.5 py-1 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700" data-l10n="Save">Save</button>
</div>
<div id="pysim-pli-dict" class="text-sm font-mono text-gray-600 dark:text-slate-400 max-h-[60vh] overflow-auto">
<span class="text-gray-400" data-l10n="Loading...">Loading...</span>
</div>
<div class="flex-1 min-w-0 border border-gray-200 dark:border-slate-700 rounded p-3">
<div class="mb-1 text-sm text-gray-500 dark:text-slate-400" data-l10n="Events that the card monitors (SET UP EVENT LIST):">Events that the card monitors (SET UP EVENT LIST):</div>
<div id="pysim-event-list" class="text-sm font-mono text-gray-600 dark:text-slate-400"></div>
</div>
</div>
<div class="mb-2 text-sm text-gray-500 dark:text-slate-400" data-l10n="Fetched proactive commands:">Fetched proactive commands:</div>
<div id="pysim-proactive-log" class="text-sm font-mono text-gray-600 dark:text-slate-400"></div>
</div>
<div id="phone-sub-tr" class="hidden">
<div class="flex justify-between items-center mb-2">
<span class="text-sm text-gray-500 dark:text-slate-400" data-l10n="PROVIDE LOCAL INFORMATION response data">PROVIDE LOCAL INFORMATION response data</span>
<button id="pli-save-btn" onclick="pysimPliSave()" class="px-2.5 py-1 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700" data-l10n="Save">Save</button>
</div>
<div id="pysim-pli-dict" class="text-sm font-mono text-gray-600 dark:text-slate-400 max-h-[60vh] overflow-auto">
<span class="text-gray-400" data-l10n="Loading...">Loading...</span>
</div>
</div>
</div>
<div id="event-send-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 max-w-sm w-full mx-4 max-h-[85vh] overflow-auto">
@@ -933,13 +952,12 @@
</div>
</div>
</div>
</div>
<div id="profiler-scan-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 max-w-sm w-full mx-4 max-h-[85vh] overflow-auto">
<h3 id="profiler-scan-title" class="text-lg font-semibold mb-4 text-gray-800 dark:text-slate-200" data-l10n="Profile from card">Profile from card</h3>
<label id="profiler-scan-name-label" class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Profile name">Profile name</label>
<input id="profiler-scan-name" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2 dark:bg-slate-800 mb-3" placeholder="Profile name">
<input id="profiler-scan-name" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2 dark:bg-slate-800 mb-3" placeholder="Profile name" onkeydown="profilerScanNameKeydown(event)">
<div id="profiler-scan-options">
<div class="flex items-center gap-2 mb-1">
<span class="text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Ignore contents of files:">Ignore contents of files:</span>
@@ -1007,8 +1025,6 @@
</div>
</div>
</div>
<script>
// ===== Tab switching =====
function switchTab(name) {
@@ -1023,8 +1039,9 @@ function switchTab(name) {
active.classList.add('bg-blue-600', 'text-white', 'dark:bg-blue-500', 'dark:text-white');
if (name === 'c-apdu') cApduSwitchSubtab('sim');
else if (name === 'scp80') setHelpAnchor(scp80HelpAnchor);
else if (name === 'response') setHelpAnchor('response-parser');
else if (name === 'pysim') setHelpAnchor(pysimHelpAnchor);
else if (name === 'profiler') { profilerSetView('list'); setHelpAnchor('profiler'); }
else if (name === 'phone') phoneSwitchSubtab('phone');
}
let scp80HelpAnchor = 'secured-packet';
@@ -1057,11 +1074,35 @@ function cApduSwitchSubtab(name) {
btn.classList.toggle('text-gray-700', !active);
btn.classList.toggle('dark:text-slate-300', !active);
});
['sim', 'usim', 'ber', 'ram', 'parse', 'httpota'].forEach(s => {
['sim', 'usim', 'ber', 'ram', 'parse', 'httpota', 'response'].forEach(s => {
const el = document.getElementById('c-apdu-sub-' + s);
if (el) el.classList.toggle('hidden', s !== name);
});
setHelpAnchor({sim:'sim-rfm', usim:'usim-rfm', ber:'ber-tlv', ram:'ram-gp', parse:'c-apdu-parser', httpota:'http-ota'}[name] || 'c-apdu');
setHelpAnchor({sim:'sim-rfm', usim:'usim-rfm', ber:'ber-tlv', ram:'ram-gp', parse:'c-apdu-parser', httpota:'http-ota', response:'response-parser'}[name] || 'c-apdu');
}
function phoneSwitchSubtab(name) {
document.querySelectorAll('.phone-subtab').forEach(btn => {
const active = btn.dataset.phoneSub === name;
btn.classList.toggle('bg-blue-600', active);
btn.classList.toggle('text-white', active);
btn.classList.toggle('bg-gray-200', !active);
btn.classList.toggle('dark:bg-slate-700', !active);
btn.classList.toggle('text-gray-700', !active);
btn.classList.toggle('dark:text-slate-300', !active);
});
['phone', 'tr'].forEach(s => {
document.getElementById('phone-sub-' + s).classList.toggle('hidden', s !== name);
});
if (name === 'tr') {
pysimPliRender();
} else {
stkCheckMenu();
pysimEventsRender();
pysimProactiveLogRender();
pysimPollStatusInit();
}
setHelpAnchor({ phone: 'stk-menu', tr: 'pli-dict' }[name] || 'stk-menu');
}
// ===== HTTP OTA constructor =====
@@ -4602,7 +4643,8 @@ async function pysimSendOta() {
if (data.por && data.por.decoded) {
document.getElementById('resp-sw').value = data.por.decoded.last_status_word || '';
document.getElementById('resp-data').value = data.por.decoded.last_response_data || '';
switchTab('response');
switchTab('c-apdu');
cApduSwitchSubtab('response');
}
let msg = 'OTA sent. SW: ' + data.sw;
const por = data.por;
@@ -5271,7 +5313,7 @@ async function stkCheckMenu() {
}
function stkSetPillsDisabled(disabled) {
document.querySelectorAll('.pysim-subtab').forEach(b => {
document.querySelectorAll('.tab-btn, .pysim-subtab').forEach(b => {
b.disabled = disabled;
b.classList.toggle('opacity-50', disabled);
});
@@ -5579,13 +5621,11 @@ function pysimSwitchSubtab(name) {
btn.classList.toggle('text-gray-700', !isActive);
btn.classList.toggle('dark:text-slate-300', !isActive);
});
['cmd', 'apdu', 'files', 'custom', 'proactive', 'profiler'].forEach(s => {
['cmd', 'apdu', 'files', 'custom'].forEach(s => {
document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name);
});
if (name === 'custom') pysimCustomRender();
if (name === 'proactive') { stkCheckMenu(); pysimEventsRender(); pysimProactiveLogRender(); pysimPliRender(); pysimPollStatusInit(); }
if (name === 'profiler') profilerSetView('list');
pysimHelpAnchor = {cmd:'pysim-cmdline', apdu:'raw-apdu', files:'file-manager', custom:'custom-files', proactive:'proactive-uicc', profiler:'profiler'}[name] || 'card-reader';
pysimHelpAnchor = {cmd:'pysim-cmdline', apdu:'raw-apdu', files:'file-manager', custom:'custom-files'}[name] || 'card-reader';
setHelpAnchor(pysimHelpAnchor);
}
@@ -7049,6 +7089,7 @@ let profilerView = 'list';
let profilerEditId = null;
let profilerDraft = null;
let profilerResults = null;
let profilerResultsLabels = null;
let profilerMismatchOnly = false;
let snapshots = [];
let snapshotViewId = null;
@@ -7253,7 +7294,7 @@ function profilerRenderSnapshotList() {
html += '<b>' + esc(s.name) + '</b>';
if (s.iccid) html += '<span class="text-xs font-mono text-gray-500 dark:text-slate-400">' + esc(s.iccid) + '</span>';
html += '<span class="text-xs text-gray-400 dark:text-slate-500">' + esc(new Date(s.created).toLocaleString()) + '</span>';
html += '<span class="text-xs text-gray-400 dark:text-slate-500">(' + s.files.length + ' ' + esc(t('files')) + ')</span>';
html += '<span class="text-xs text-gray-400 dark:text-slate-500">(' + esc(profilerSnapshotCountLabel(s)) + ')</span>';
html += '<span class="ml-auto flex gap-1">';
html += '<button onclick="snapshotOpen(' + i + ')" class="px-2 py-0.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700">' + esc(t('Open')) + '</button>';
html += '<button onclick="snapshotExport(' + i + ')" class="px-2 py-0.5 text-xs rounded bg-gray-600 text-white hover:bg-gray-700">' + esc(t('Export')) + '</button>';
@@ -7337,6 +7378,7 @@ function profilerFromCard() {
document.getElementById('profiler-scan-btn').disabled = false;
document.getElementById('profiler-scan-btn').textContent = t('Scan');
document.getElementById('profiler-scan-modal').classList.remove('hidden');
document.getElementById('profiler-scan-name').focus();
}
function profilerScanCancel() {
@@ -7355,6 +7397,14 @@ function snapshotNew() {
document.getElementById('profiler-scan-btn').disabled = false;
document.getElementById('profiler-scan-btn').textContent = t('Scan');
document.getElementById('profiler-scan-modal').classList.remove('hidden');
nameEl.focus();
}
function profilerScanNameKeydown(e) {
if (e.key !== 'Enter') return;
if (document.getElementById('profiler-scan-btn').disabled) return;
e.preventDefault();
profilerScanStart();
}
async function profilerScanStart() {
@@ -7395,8 +7445,12 @@ async function profilerScanStart() {
progEl.textContent = done + ' / ' + total + ' ' + t('files') + (path ? ' — ' + path : '');
};
if (_scanTarget === 'snapshot') {
const files = await profilerScanCard(new Set(), new Set(), 'exact', onProgress, new Set(), 'snapshot');
const snapshot = { id: profilerNewId(), name: name, created: new Date().toISOString(), iccid: profilerSnapshotIccid(files), files: files };
const timing = profilerTimingAccumulator();
const scanStart = performance.now();
const files = await profilerScanCard(new Set(), new Set(), 'exact', onProgress, new Set(), 'snapshot', timing);
const stats = timing.stats();
stats.total_ms = Math.round(performance.now() - scanStart);
const snapshot = { id: profilerNewId(), name: name, created: new Date().toISOString(), iccid: profilerSnapshotIccid(files), files: files, timing: stats };
snapshots.push(snapshot);
snapshotsSave();
profilerScanCancel();
@@ -7428,7 +7482,7 @@ async function profilerScanStart() {
// Phase 1 discovers every file (tree calls only) to know the total; phase 2
// builds a rule per file, reporting progress via the optional onProgress
// callback as onProgress(done, total, path).
async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress, maskFids, mode) {
async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress, maskFids, mode, timing) {
const rules = [];
const files = [];
const seen = new Set();
@@ -7483,7 +7537,7 @@ async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress, ma
const entry = files[i];
if (onProgress) onProgress(i + 1, total, entry.path);
const rule = mode === 'snapshot'
? await profilerBuildSnapshotFile(entry.path, entry.child)
? await profilerBuildSnapshotFile(entry.path, entry.child, timing)
: await profilerBuildFileRule(entry.path, entry.child, ignoreFids, ignoreNames, fciMode, maskFids);
if (rule) rules.push(rule);
}
@@ -7535,7 +7589,50 @@ async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames, fciMode,
// Card snapshot entry: metadata + raw FCI + exact contents for every readable
// file (no ignore list, no masking, no FCP/FCI check mode).
async function profilerBuildSnapshotFile(path, c) {
function profilerTimingStats(values) {
const vals = (values || []).filter(v => typeof v === 'number' && isFinite(v));
if (!vals.length) return null;
let min = vals[0], max = vals[0], sum = 0;
for (const v of vals) {
if (v < min) min = v;
if (v > max) max = v;
sum += v;
}
return { min: Math.round(min), max: Math.round(max), avg: Math.round(sum / vals.length * 10) / 10, count: vals.length };
}
function profilerTimingAccumulator() {
const buckets = { select: [], read_binary: [], read_record: [] };
return {
add(type, ms) {
if (buckets[type]) buckets[type].push(ms);
},
stats() {
const out = {};
for (const type in buckets) {
const s = profilerTimingStats(buckets[type]);
if (s) out[type] = s;
}
return out;
},
};
}
function profilerFormatMs(ms) {
if (ms === null || ms === undefined) return 'n/a';
if (ms >= 1000) return (ms / 1000).toFixed(2) + ' s';
return Math.round(ms) + ' ms';
}
function profilerSnapshotCountLabel(s) {
let label = (s.files ? s.files.length : 0) + ' ' + t('files');
if (s.timing && s.timing.total_ms !== undefined) {
label += ', ' + t('scanned in') + ' ' + (s.timing.total_ms / 1000).toFixed(2) + ' ' + t('sec');
}
return label;
}
async function profilerBuildSnapshotFile(path, c, timing) {
let sel;
try {
sel = await pysimFetch('/api/select', { path: path });
@@ -7551,14 +7648,37 @@ async function profilerBuildSnapshotFile(path, c) {
numRecords: (sel.num_of_rec === null || sel.num_of_rec === undefined) ? null : sel.num_of_rec,
fciHex: sel.fci_hex || null,
content: null,
timing: null,
};
const selectTimes = (sel.apdu_times || []).filter(t => t.type === 'select').map(t => t.ms);
if (selectTimes.length) {
if (timing) selectTimes.forEach(ms => timing.add('select', ms));
file.timing = { select_ms: selectTimes.reduce((a, b) => a + b, 0), read_ms: null };
}
try {
const rd = await pysimFetch('/api/read', { path: path, mode: 'raw' });
if (rd && rd.success) {
const readTimes = rd.apdu_times || [];
const recTimes = readTimes.filter(t => t.type === 'read_record').map(t => t.ms);
const binTimes = readTimes.filter(t => t.type === 'read_binary').map(t => t.ms);
if (rd.records) {
file.content = { kind: 'record', records: rd.records.map(r => ({ num: r.num, data: r.data })) };
file.content = { kind: 'record', records: rd.records.map((r, i) => {
const rec = { num: r.num, data: r.data };
if (recTimes[i] !== undefined) rec.ms = recTimes[i];
return rec;
}) };
if (timing) recTimes.forEach(ms => timing.add('read_record', ms));
if (recTimes.length) {
file.timing = file.timing || { select_ms: null, read_ms: null };
file.timing.read_ms = recTimes.reduce((a, b) => a + b, 0);
}
} else if (rd.data) {
file.content = { kind: 'transparent', data: rd.data };
if (timing) binTimes.forEach(ms => timing.add('read_binary', ms));
if (binTimes.length) {
file.timing = file.timing || { select_ms: null, read_ms: null };
file.timing.read_ms = binTimes.reduce((a, b) => a + b, 0);
}
}
}
} catch (e) {}
@@ -7686,9 +7806,38 @@ function profilerRenderSnapshotData() {
const s = snapshots.find(x => x.id === snapshotViewId);
if (!s) return;
document.getElementById('snapshot-iccid').textContent = s.iccid || '';
document.getElementById('snapshot-summary').innerHTML = profilerRenderSnapshotSummary(s);
document.getElementById('snapshot-files').innerHTML = profilerRenderSnapshotFiles(s);
}
function profilerRenderSnapshotSummary(s) {
const files = (s.files || []).length;
let records = 0;
for (const f of (s.files || [])) {
if (f.content && f.content.kind === 'record' && f.content.records) records += f.content.records.length;
}
let html = '<div class="text-xs text-gray-500 dark:text-slate-400">' +
esc(t('Files')) + ': <b>' + files + '</b> · ' + esc(t('Records')) + ': <b>' + records + '</b>';
const timing = s.timing;
if (timing && timing.total_ms !== undefined) {
html += ' · ' + esc(t('Scan time')) + ': <b>' + esc(profilerFormatMs(timing.total_ms)) + '</b>';
}
html += '</div>';
const labels = [['select', 'Select'], ['read_binary', 'Read binary'], ['read_record', 'Read record']];
let rows = '';
if (timing) {
for (const [key, label] of labels) {
const st = timing[key];
if (!st) continue;
rows += '<div>' + esc(t(label)) + ': ' + esc(t('min')) + ' ' + st.min + ' ms · ' + esc(t('avg')) + ' ' + st.avg + ' ms · ' + esc(t('max')) + ' ' + st.max + ' ms (n=' + st.count + ')</div>';
}
}
html += rows
? '<div class="text-xs font-mono text-gray-600 dark:text-slate-400 mt-0.5">' + rows + '</div>'
: '<div class="text-xs text-gray-400 dark:text-slate-500 mt-0.5">' + esc(t('No timing data')) + '</div>';
return html;
}
function snapshotSaveName() {
const s = snapshots.find(x => x.id === snapshotViewId);
if (!s) return;
@@ -7715,6 +7864,15 @@ function profilerRenderSnapshotFiles(s) {
if (f.recordLen !== null && f.recordLen !== undefined) attrs.push(esc(t('Record length')) + ': ' + esc(String(f.recordLen)));
if (f.numRecords !== null && f.numRecords !== undefined) attrs.push(esc(t('Record count')) + ': ' + esc(String(f.numRecords)));
if (attrs.length) html += '<div class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">' + attrs.join(' · ') + '</div>';
if (f.timing) {
const parts = [];
if (f.timing.select_ms !== null && f.timing.select_ms !== undefined) parts.push(esc(t('Select')) + ': ' + esc(profilerFormatMs(f.timing.select_ms)));
if (f.timing.read_ms !== null && f.timing.read_ms !== undefined) {
const readKey = profilerContentKindForFileType(f.fileType) === 'record' ? 'Read record' : 'Read binary';
parts.push(esc(t(readKey)) + ': ' + esc(profilerFormatMs(f.timing.read_ms)));
}
if (parts.length) html += '<div class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">' + parts.join(' · ') + '</div>';
}
if (f.fciHex) {
html += '<div class="mt-1 flex gap-3 items-start">';
html += '<div class="flex-1 min-w-0"><div class="text-xs text-gray-500 dark:text-slate-400 mb-0.5">' + esc(t('FCI hex (raw SELECT response)')) + '</div>' +
@@ -7730,7 +7888,9 @@ function profilerRenderSnapshotFiles(s) {
html += '<div class="space-y-0.5">';
for (const rec of (f.content.records || [])) {
html += '<div class="flex items-center gap-2"><span class="text-xs text-gray-500 w-8 shrink-0">' + rec.num + '</span>' +
'<input readonly value="' + escHtml(rec.data) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800"></div>';
'<input readonly value="' + escHtml(rec.data) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800">' +
(rec.ms !== undefined ? '<span class="text-xs text-gray-400 w-16 text-right shrink-0">' + esc(profilerFormatMs(rec.ms)) + '</span>' : '') +
'</div>';
}
html += '</div>';
} else {
@@ -7954,7 +8114,7 @@ async function profilerCheck(i) {
await profilerRunProfile(p, null, p.name);
}
async function profilerRunProfile(p, source, title, extraResults) {
async function profilerRunProfile(p, source, title, extraResults, labels) {
profilerSetView('results');
document.getElementById('profiler-results-title').textContent = title;
const prog = document.getElementById('profiler-progress');
@@ -7969,6 +8129,7 @@ async function profilerRunProfile(p, source, title, extraResults) {
if (extraResults && extraResults.length) results.push(...extraResults);
prog.textContent = '';
profilerResults = results;
profilerResultsLabels = labels || null;
profilerRenderResultsView();
}
@@ -8079,7 +8240,8 @@ async function snapshotCompareRun() {
});
document.getElementById('snapshot-compare-modal').classList.add('hidden');
const pseudo = { name: master.name + ' → ' + check.name, rules: profilerRulesFromSnapshot(master, maskFids) };
await profilerRunProfile(pseudo, profilerSnapshotSource(check), pseudo.name, profilerExtraFileResults(master, check));
await profilerRunProfile(pseudo, profilerSnapshotSource(check), pseudo.name, profilerExtraFileResults(master, check),
{ expected: master.name, actual: check.name });
}
function profilerToggleMismatchOnly(checked) {
@@ -8104,7 +8266,7 @@ function profilerRenderResultsView() {
' · <span class="text-yellow-600 font-semibold">' + errors + ' ' + t('errors') + '</span>';
const shown = profilerVisibleResults(profilerResults, profilerMismatchOnly);
document.getElementById('profiler-report').innerHTML = shown.length
? profilerRenderReport(shown)
? profilerRenderReport(shown, profilerResultsLabels)
: '<div class="text-sm text-emerald-600">' + esc(t('No mismatches')) + '</div>';
}
@@ -8525,7 +8687,9 @@ function fcpDecode(hex) {
return { ok: !error && items.length > 0, template, items, error };
}
function fcpDiffHtml(expectedHex, actualHex) {
function fcpDiffHtml(expectedHex, actualHex, labels) {
const expLabel = (labels && labels.expected) ? labels.expected : t('expected');
const actLabel = (labels && labels.actual) ? labels.actual : t('actual');
const e = fcpDecode(expectedHex);
const a = fcpDecode(actualHex);
const eBad = !!e.error, aBad = !!a.error;
@@ -8541,8 +8705,8 @@ function fcpDiffHtml(expectedHex, actualHex) {
html += '<table class="w-full text-xs border-collapse">';
html += '<thead><tr class="border-b border-gray-200 dark:border-slate-700">' +
'<th class="text-left py-0.5 px-1 font-medium text-gray-500 dark:text-slate-400">' + esc(t('Parameter')) + '</th>' +
'<th class="text-left py-0.5 px-1 font-medium text-gray-500 dark:text-slate-400">' + esc(t('expected')) + '</th>' +
'<th class="text-left py-0.5 px-1 font-medium text-gray-500 dark:text-slate-400">' + esc(t('actual')) + '</th></tr></thead><tbody>';
'<th class="text-left py-0.5 px-1 font-medium text-gray-500 dark:text-slate-400">' + esc(expLabel) + '</th>' +
'<th class="text-left py-0.5 px-1 font-medium text-gray-500 dark:text-slate-400">' + esc(actLabel) + '</th></tr></thead><tbody>';
for (const k of keys) {
const ev = eMap[k], av = aMap[k];
const ed = ev ? (ev.decoded === null ? '' : (ev.decoded || ev.value)) : '—';
@@ -8585,7 +8749,10 @@ function profilerFciInput(i, value) {
profilerUpdateFciPreview(i, value);
}
function profilerRenderReport(results) {
function profilerRenderReport(results, labels) {
const expLabel = (labels && labels.expected) ? labels.expected : t('expected');
const actLabel = (labels && labels.actual) ? labels.actual : t('actual');
const labelCls = 'text-xs text-gray-500 dark:text-slate-400 text-right shrink-0 ' + (labels ? 'w-40 break-all' : 'w-24');
let html = '';
for (const r of results) {
const color = r.status === 'pass' ? 'text-emerald-600' : (r.status === 'fail' ? 'text-red-600' : 'text-yellow-600');
@@ -8609,15 +8776,15 @@ function profilerRenderReport(results) {
html += '<div class="mt-1">';
html += '<div class="text-xs text-red-600 font-mono pl-2">' + esc(c.label) + '</div>';
html += '<div class="flex items-center gap-2 mt-0.5 pl-2">' +
'<span class="text-xs text-gray-500 dark:text-slate-400 w-24 text-right shrink-0">' + esc(t('expected')) + '</span>' +
'<span class="' + labelCls + '">' + esc(expLabel) + '</span>' +
'<input readonly title="' + escHtml(String(c.expected)) + '" value="' + escHtml(String(c.expected)) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800"></div>';
html += '<div class="flex items-center gap-2 mt-0.5 pl-2">' +
'<span class="text-xs text-gray-500 dark:text-slate-400 w-24 text-right shrink-0">' + esc(t('actual')) + '</span>' +
'<span class="' + labelCls + '">' + esc(actLabel) + '</span>' +
'<input readonly title="' + escHtml(String(c.actual)) + '" value="' + escHtml(String(c.actual)) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800"></div>';
if (c.label === 'fci') html += fcpDiffHtml(String(c.expected), String(c.actual));
if (c.label === 'fci') html += fcpDiffHtml(String(c.expected), String(c.actual), labels);
html += '</div>';
} else {
html += '<div class="text-xs text-red-600 mt-1">' + esc(c.label) + ': ' + esc(t('expected')) + ' <b>' + esc(String(c.expected)) + '</b>, ' + esc(t('actual')) + ' <b>' + esc(String(c.actual)) + '</b></div>';
html += '<div class="text-xs text-red-600 mt-1">' + esc(c.label) + ': ' + esc(expLabel) + ' <b>' + esc(String(c.expected)) + '</b>, ' + esc(actLabel) + ' <b>' + esc(String(c.actual)) + '</b></div>';
}
}
const recFailed = r.recordsMismatch || (r.checks || []).some(c => /^content\.rec\d+$/.test(c.label) && !c.ok);
@@ -8796,7 +8963,18 @@ const LANG_RU = {
'Verifying against pySim reference...': 'Сверка с эталоном pySim...',
'Verify vs pySim': 'Проверить в pySim',
'Send to Card': 'Отправить на карту',
'Proactive UICC': 'Проактивный UICC',
'Phone simulator': 'Симулятор телефона',
'Files': 'Файлов',
'Records': 'Записей',
'Scan time': 'Время сканирования',
'scanned in': 'за',
'sec': 'сек',
'No timing data': 'Нет данных о времени',
'min': 'мин',
'avg': 'сред',
'max': 'макс',
'Phone': 'Телефон',
'TR Config': 'Конфигурация TR',
'Events that the card monitors (SET UP EVENT LIST):': 'События, отслеживаемые картой (SET UP EVENT LIST):',
'No events configured.': 'Нет настроенных событий.',
'Fetched proactive commands:': 'Извлечённые проактивные команды:',
@@ -8974,7 +9152,7 @@ function isViewVisible(id) {
// Dynamic views build their labels with t() at render time; re-render the
// visible ones after a language switch so they don't stay in the old language.
function refreshDynamicI18n() {
if (isViewVisible('pysim-sub-profiler')) {
if (isViewVisible('tab-profiler')) {
if (profilerView === 'list') { profilerRenderList(); profilerRenderSnapshotList(); }
else if (profilerView === 'editor') profilerRenderEditor();
else if (profilerView === 'results') profilerRenderResultsView();
@@ -8982,10 +9160,12 @@ function refreshDynamicI18n() {
}
if (isViewVisible('profiler-scan-modal')) profilerScanRefreshOptions();
if (isViewVisible('scp80-sub-cards')) cardsRender();
if (isViewVisible('pysim-sub-proactive')) {
pysimEventsRender();
pysimProactiveLogRender();
pysimPliRender();
if (isViewVisible('tab-phone')) {
if (isViewVisible('phone-sub-phone')) {
pysimEventsRender();
pysimProactiveLogRender();
}
if (isViewVisible('phone-sub-tr')) pysimPliRender();
}
}
+2 -2
View File
@@ -4,8 +4,8 @@
"description": "Standalone offline HTML/JS tool for building APDU commands for SIM, USIM, and GlobalPlatform RAM, plus encoding conversions.",
"main": "index.js",
"scripts": {
"build": "npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js",
"build:prod": "NODE_ENV=production npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js --minify",
"build": "npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js && cat src/contrast.css >> style.css",
"build:prod": "NODE_ENV=production npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js --minify && cat src/contrast.css >> style.css",
"test": "node --test"
},
"repository": {
+112
View File
@@ -0,0 +1,112 @@
/* ===== Theme contrast adjustments ===== */
/* Light theme: darken softer (muted) text one step for higher contrast */
.text-gray-400 {
color: rgb(107 114 128 / var(--tw-text-opacity, 1));
}
.text-gray-500 {
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
}
.text-gray-600 {
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
}
/* Dark theme: lighten softer (muted) text one step for higher contrast */
.dark\:text-slate-500:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark\:text-slate-400:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
/* Dark theme: red text is too dark on dark backgrounds — lighten it */
.dark :where(.text-red-500) {
color: rgb(248 113 113 / var(--tw-text-opacity, 1));
}
.dark :where(.text-red-600) {
color: rgb(248 113 113 / var(--tw-text-opacity, 1));
}
.dark :where(.text-red-700) {
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
}
/* Light theme: darken light gray shades (backgrounds & borders) one step */
.bg-gray-50 {
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.bg-gray-100 {
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.bg-gray-200 {
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
}
.border-gray-100 {
border-color: rgb(229 231 235 / var(--tw-border-opacity, 1));
}
.border-gray-200 {
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.border-gray-300 {
border-color: rgb(156 163 175 / var(--tw-border-opacity, 1));
}
.text-gray-300 {
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
}
.hover\:bg-gray-100:hover {
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-200:hover {
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-300:hover {
background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
}
/* Dark theme: lighten gray fonts */
.dark\:text-gray-400:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark\:text-gray-600:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
/* Dark theme: gray text without an explicit dark variant (ids, timestamps,
expand markers, ...) — match the lighten-on-dark level used above */
.dark .text-gray-300:not([class*="dark:text-"]) {
color: rgb(148 163 184 / var(--tw-text-opacity, 1));
}
.dark .text-gray-400:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-500:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-600:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-700:not([class*="dark:text-"]) {
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
}
.dark .text-gray-800:not([class*="dark:text-"]) {
color: rgb(241 245 249 / var(--tw-text-opacity, 1));
}
/* Dark theme: brighten gray borders one step so they stay visible on dark
backgrounds */
.dark\:border-slate-600:is(.dark *) {
border-color: rgb(100 116 139 / var(--tw-border-opacity, 1));
}
.dark\:border-slate-700:is(.dark *) {
border-color: rgb(71 85 105 / var(--tw-border-opacity, 1));
}
.dark\:border-slate-700\/50:is(.dark *) {
border-color: rgb(71 85 105 / 0.5);
}
.dark\:border-slate-800:is(.dark *) {
border-color: rgb(51 65 85 / var(--tw-border-opacity, 1));
}
/* Dark theme: normal (non-muted) UI text — keep one step brighter than the
muted gray level so the two remain distinguishable */
.dark\:text-slate-300:is(.dark *) {
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
}
+45 -4
View File
@@ -1702,7 +1702,7 @@ video {
/* Dark theme: lighten softer (muted) text one step for higher contrast */
.dark\:text-slate-500:is(.dark *) {
color: rgb(148 163 184 / var(--tw-text-opacity, 1));
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark\:text-slate-400:is(.dark *) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
@@ -1753,8 +1753,49 @@ video {
/* Dark theme: lighten gray fonts */
.dark\:text-gray-400:is(.dark *) {
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark\:text-gray-600:is(.dark *) {
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
}
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
/* Dark theme: gray text without an explicit dark variant (ids, timestamps,
expand markers, ...) — match the lighten-on-dark level used above */
.dark .text-gray-300:not([class*="dark:text-"]) {
color: rgb(148 163 184 / var(--tw-text-opacity, 1));
}
.dark .text-gray-400:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-500:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-600:not([class*="dark:text-"]) {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark .text-gray-700:not([class*="dark:text-"]) {
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
}
.dark .text-gray-800:not([class*="dark:text-"]) {
color: rgb(241 245 249 / var(--tw-text-opacity, 1));
}
/* Dark theme: brighten gray borders one step so they stay visible on dark
backgrounds */
.dark\:border-slate-600:is(.dark *) {
border-color: rgb(100 116 139 / var(--tw-border-opacity, 1));
}
.dark\:border-slate-700:is(.dark *) {
border-color: rgb(71 85 105 / var(--tw-border-opacity, 1));
}
.dark\:border-slate-700\/50:is(.dark *) {
border-color: rgb(71 85 105 / 0.5);
}
.dark\:border-slate-800:is(.dark *) {
border-color: rgb(51 65 85 / var(--tw-border-opacity, 1));
}
/* Dark theme: normal (non-muted) UI text — keep one step brighter than the
muted gray level so the two remain distinguishable */
.dark\:text-slate-300:is(.dark *) {
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
}
+18 -4
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v92';
const CACHE = 'otaman-v103';
const URLS = [
'index.html',
'help.html',
@@ -31,19 +31,33 @@ self.addEventListener('activate', e => {
);
});
const OFFLINE_RESPONSE = new Response('Offline: page not cached', {
status: 503,
statusText: 'Offline',
headers: { 'Content-Type': 'text/plain' },
});
self.addEventListener('fetch', e => {
if (!e.request.url.startsWith('http')) return;
if (new URL(e.request.url).pathname.startsWith('/api/')) return; // live data, never cache
const path = new URL(e.request.url).pathname;
if (path.startsWith('/api/')) return; // live data, never cache
if (e.request.method !== 'GET') return;
const isNavigate = e.request.mode === 'navigate' || e.request.url.endsWith('sw.js');
const isNavigate = e.request.mode === 'navigate';
const isSwScript = path.endsWith('/sw.js');
if (isNavigate) {
e.respondWith(
fetch(e.request).then(res => {
const clone = res.clone();
caches.open(CACHE).then(c => c.put(e.request, clone));
return res;
}).catch(() => caches.match(e.request))
}).catch(() =>
caches.match(e.request)
.then(r => r || caches.match('index.html'))
.then(r => r || OFFLINE_RESPONSE)
)
);
} else if (isSwScript) {
e.respondWith(fetch(e.request).catch(() => OFFLINE_RESPONSE));
} else {
e.respondWith(
caches.match(e.request).then(r => r || fetch(e.request).then(res => {
+31
View File
@@ -10,3 +10,34 @@ const closes = (html.match(/<\/div>/g) || []).length;
test('HTML <div> tags are balanced', () => {
assert.strictEqual(opens, closes, `Unbalanced divs: ${opens} opens vs ${closes} closes`);
});
test('top-level tabs match the rearranged views', () => {
const tabs = [...html.matchAll(/class="tab-btn[^"]*" data-tab="([^"]+)"/g)].map(m => m[1]);
assert.deepStrictEqual(tabs, ['c-apdu', 'scp80', 'pysim', 'profiler', 'phone']);
assert.match(html, /data-tab="c-apdu">Remote APDU</);
});
test('response parser is a Remote APDU pill', () => {
assert.match(html, /data-sub="response" onclick="cApduSwitchSubtab\('response'\)"/);
assert.ok(html.includes('id="c-apdu-sub-response"'));
});
test('profiler and phone simulator are top-level tab contents', () => {
assert.ok(html.includes('id="tab-profiler" class="tab-content hidden"'));
assert.ok(html.includes('id="tab-phone" class="tab-content hidden"'));
});
test('phone simulator has Phone / TR Config pills', () => {
assert.match(html, /data-phone-sub="phone" onclick="phoneSwitchSubtab\('phone'\)"/);
assert.match(html, /data-phone-sub="tr" onclick="phoneSwitchSubtab\('tr'\)"/);
assert.ok(html.includes('id="phone-sub-phone"'));
assert.ok(html.includes('id="phone-sub-tr"'));
});
test('scan name input starts scanning on Enter', () => {
assert.match(html, /id="profiler-scan-name"[^>]*onkeydown="profilerScanNameKeydown\(event\)"/);
});
test('snapshot view has a timing summary block', () => {
assert.ok(html.includes('id="snapshot-summary"'));
});
+83
View File
@@ -0,0 +1,83 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
const code = extractFunc(html, 'phoneSwitchSubtab') + '\n' +
'globalThis.setHelpAnchor = a => { globalThis._anchor = a; };\n' +
'globalThis.stkCheckMenu = () => { globalThis._stk = (globalThis._stk || 0) + 1; };\n' +
'globalThis.pysimEventsRender = () => { globalThis._events = (globalThis._events || 0) + 1; };\n' +
'globalThis.pysimProactiveLogRender = () => { globalThis._log = (globalThis._log || 0) + 1; };\n' +
'globalThis.pysimPollStatusInit = () => { globalThis._poll = (globalThis._poll || 0) + 1; };\n' +
'globalThis.pysimPliRender = () => { globalThis._pli = (globalThis._pli || 0) + 1; };\n';
eval(code);
function makeClassList() {
const set = new Set();
return {
toggle: (c, on) => { on ? set.add(c) : set.delete(c); },
has: c => set.has(c),
};
}
function setup() {
const buttons = [
{ dataset: { phoneSub: 'phone' }, classList: makeClassList() },
{ dataset: { phoneSub: 'tr' }, classList: makeClassList() },
];
const panels = {
'phone-sub-phone': { classList: makeClassList() },
'phone-sub-tr': { classList: makeClassList() },
};
globalThis.document = {
querySelectorAll: sel => (sel === '.phone-subtab' ? buttons : []),
getElementById: id => panels[id] || null,
};
globalThis._anchor = null;
globalThis._stk = globalThis._events = globalThis._log = globalThis._poll = globalThis._pli = 0;
return { buttons, panels };
}
test('TR Config pill shows the TR panel and renders PLI data', () => {
const { buttons, panels } = setup();
phoneSwitchSubtab('tr');
assert.ok(!panels['phone-sub-tr'].classList.has('hidden'));
assert.ok(panels['phone-sub-phone'].classList.has('hidden'));
assert.ok(buttons[1].classList.has('bg-blue-600'));
assert.ok(!buttons[0].classList.has('bg-blue-600'));
assert.strictEqual(globalThis._anchor, 'pli-dict');
assert.strictEqual(globalThis._pli, 1);
assert.strictEqual(globalThis._stk, 0);
});
test('Phone pill shows the phone panel and renders CAT views', () => {
const { buttons, panels } = setup();
phoneSwitchSubtab('phone');
assert.ok(!panels['phone-sub-phone'].classList.has('hidden'));
assert.ok(panels['phone-sub-tr'].classList.has('hidden'));
assert.ok(buttons[0].classList.has('bg-blue-600'));
assert.strictEqual(globalThis._anchor, 'stk-menu');
assert.strictEqual(globalThis._stk, 1);
assert.strictEqual(globalThis._events, 1);
assert.strictEqual(globalThis._log, 1);
assert.strictEqual(globalThis._poll, 1);
assert.strictEqual(globalThis._pli, 0);
});
+156 -2
View File
@@ -21,7 +21,7 @@ function extractFunc(src, name, asyncFn) {
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
}
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerMaskFidForFile', 'profilerRulesFromSnapshot', 'profilerExtraFileResults'];
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerMaskFidForFile', 'profilerRulesFromSnapshot', 'profilerExtraFileResults', 'profilerScanNameKeydown', 'profilerTimingStats', 'profilerTimingAccumulator', 'profilerFormatMs', 'profilerRenderSnapshotSummary', 'profilerSnapshotCountLabel'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
code += extractFunc(html, 'profilerBuildFileRule', true) + '\n';
@@ -584,7 +584,7 @@ test('profilerRenderReport renders raw-data mismatches as aligned readonly field
assert.ok(html.includes('font-mono'));
assert.ok(html.includes('value="621082024021"'));
assert.ok(html.includes('value="621082024022"'));
assert.ok(html.includes('w-24 text-right'));
assert.ok(html.includes('text-right shrink-0 w-24'));
// non-raw check stays inline
assert.ok(html.includes('content.records: expected'));
assert.ok(!fciBlock.includes(': expected'));
@@ -1112,3 +1112,157 @@ test('profilerExtraFileResults reports files missing from the master', () => {
assert.strictEqual(extras[0].status, 'fail');
assert.deepStrictEqual(extras[0].checks, [{ label: 'extra file', expected: 'absent', actual: 'present', ok: false }]);
});
test('profilerScanNameKeydown starts the scan on Enter only', () => {
const btn = { disabled: false };
global.document = { getElementById: () => btn };
let started = 0;
global.profilerScanStart = () => { started++; };
let prevented = 0;
profilerScanNameKeydown({ key: 'Enter', preventDefault: () => prevented++ });
profilerScanNameKeydown({ key: 'a', preventDefault: () => prevented++ });
assert.strictEqual(started, 1);
assert.strictEqual(prevented, 1);
btn.disabled = true;
profilerScanNameKeydown({ key: 'Enter', preventDefault: () => prevented++ });
assert.strictEqual(started, 1);
assert.strictEqual(prevented, 1);
delete global.document;
delete global.profilerScanStart;
});
// --- snapshot command timings ---
test('profilerTimingStats computes min, max and average', () => {
assert.deepStrictEqual(profilerTimingStats([10, 20, 30]), { min: 10, max: 30, avg: 20, count: 3 });
assert.deepStrictEqual(profilerTimingStats([7, 7.5, 8]), { min: 7, max: 8, avg: 7.5, count: 3 });
assert.strictEqual(profilerTimingStats([]), null);
assert.strictEqual(profilerTimingStats(null), null);
});
test('profilerFormatMs formats milliseconds and seconds', () => {
assert.strictEqual(profilerFormatMs(12), '12 ms');
assert.strictEqual(profilerFormatMs(999), '999 ms');
assert.strictEqual(profilerFormatMs(1500), '1.50 s');
assert.strictEqual(profilerFormatMs(null), 'n/a');
});
test('profilerTimingAccumulator aggregates per command type', () => {
const acc = profilerTimingAccumulator();
acc.add('select', 10);
acc.add('select', 20);
acc.add('read_record', 5);
acc.add('bogus', 1);
assert.deepStrictEqual(acc.stats(), {
select: { min: 10, max: 20, avg: 15, count: 2 },
read_record: { min: 5, max: 5, avg: 5, count: 1 },
});
});
test('profilerBuildSnapshotFile records per-command timings', async () => {
const acc = profilerTimingAccumulator();
mockFetch({
'/api/select': () => ({ exists: true, name: 'EF.ADN', file_type: 'linear_fixed', file_size: null, record_len: 2, num_of_rec: 2,
apdu_times: [{ type: 'select', ms: 5 }, { type: 'select', ms: 7 }] }),
'/api/read': () => ({ success: true, file_type: 'linear_fixed', records: [{ num: 1, data: 'AA' }, { num: 2, data: 'BB' }],
apdu_times: [{ type: 'select', ms: 4 }, { type: 'read_record', ms: 11 }, { type: 'read_record', ms: 13 }] }),
});
const file = await profilerBuildSnapshotFile('MF/6F3A', { name: 'EF.ADN' }, acc);
assert.deepStrictEqual(file.timing, { select_ms: 12, read_ms: 24 });
assert.strictEqual(file.content.records[0].ms, 11);
assert.strictEqual(file.content.records[1].ms, 13);
const stats = acc.stats();
assert.strictEqual(stats.select.count, 2);
assert.strictEqual(stats.read_record.count, 2);
});
test('profilerBuildSnapshotFile without apdu_times has empty timing', async () => {
mockFetch({
'/api/select': () => ({ exists: true, name: 'EF.ADN', file_type: 'transparent', file_size: 2, record_len: null, num_of_rec: null }),
'/api/read': () => ({ success: true, file_type: 'transparent', data: 'AABB' }),
});
const file = await profilerBuildSnapshotFile('MF/6F3A', { name: 'EF.ADN' }, null);
assert.strictEqual(file.timing, null);
assert.deepStrictEqual(file.content, { kind: 'transparent', data: 'AABB' });
});
test('profilerRenderSnapshotSummary shows counts, scan time and stats', () => {
global.t = s => s;
const snap = {
files: [
{ content: { kind: 'record', records: [{ num: 1, data: 'AA', ms: 5 }, { num: 2, data: 'BB', ms: 7 }] } },
{ content: { kind: 'transparent', data: 'AA' } },
],
timing: {
select: { min: 3, max: 9, avg: 6, count: 3 },
read_record: { min: 5, max: 7, avg: 6, count: 2 },
total_ms: 1500,
},
};
const html = profilerRenderSnapshotSummary(snap);
assert.ok(html.includes('Files: <b>2</b>'), html);
assert.ok(html.includes('Records: <b>2</b>'), html);
assert.ok(html.includes('Scan time: <b>1.50 s</b>'), html);
assert.ok(html.includes('Select: min 3 ms'), html);
assert.ok(html.includes('Read record: min 5 ms'), html);
assert.ok(!html.includes('Read binary'), html);
delete global.t;
});
test('profilerRenderSnapshotSummary notes missing timing data', () => {
global.t = s => s;
const html = profilerRenderSnapshotSummary({ files: [], timing: undefined });
assert.ok(html.includes('Files: <b>0</b>'), html);
assert.ok(html.includes('No timing data'), html);
delete global.t;
});
test('profilerSnapshotCountLabel appends the scan time when available', () => {
global.t = s => (s === 'scanned in' ? 'scanned in' : s);
const withTime = profilerSnapshotCountLabel({ files: new Array(95).fill({}), timing: { total_ms: 21320 } });
assert.strictEqual(withTime, '95 files, scanned in 21.32 sec');
const noTime = profilerSnapshotCountLabel({ files: new Array(3).fill({}) });
assert.strictEqual(noTime, '3 files');
const empty = profilerSnapshotCountLabel({ files: [], timing: {} });
assert.strictEqual(empty, '0 files');
delete global.t;
});
test('profilerRenderReport labels mismatches with custom names', () => {
global.t = s => s;
global.pysimCustomFiles = [];
const html = profilerRenderReport([{
path: 'MF/6F3A', name: 'EF.ADN', status: 'fail',
checks: [
{ label: 'content', expected: 'AABB', actual: 'CCDD', ok: false },
{ label: 'fileSize', expected: 4, actual: 9, ok: false },
],
}], { expected: 'Master snap', actual: 'Check snap' });
assert.ok(html.includes('Master snap'), html);
assert.ok(html.includes('Check snap'), html);
assert.ok(!html.includes('>expected<'), html);
assert.ok(!html.includes('>actual<'), html);
delete global.t;
delete global.pysimCustomFiles;
});
test('profilerRenderReport keeps expected/actual without labels', () => {
global.t = s => s;
global.pysimCustomFiles = [];
const html = profilerRenderReport([{
path: 'MF/6F3A', status: 'fail',
checks: [{ label: 'fileSize', expected: 4, actual: 9, ok: false }],
}]);
assert.ok(html.includes(' expected '), html);
assert.ok(html.includes(' actual '), html);
delete global.t;
delete global.pysimCustomFiles;
});
test('fcpDiffHtml headers use custom labels', () => {
global.t = s => s;
const diff = fcpDiffHtml(FCP_TRANSPARENT, '62128002000A8202412183026F078A0105880110', { expected: 'Master', actual: 'Candidate' });
assert.ok(diff.includes('>Master<'), diff);
assert.ok(diff.includes('>Candidate<'), diff);
delete global.t;
});
+121
View File
@@ -0,0 +1,121 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const swSource = fs.readFileSync(path.join(__dirname, '..', 'sw.js'), 'utf8');
class FakeResponse {
constructor(body, init) {
this.body = body;
this.status = init && init.status;
this.statusText = init && init.statusText;
}
clone() {
return new FakeResponse(this.body, { status: this.status, statusText: this.statusText });
}
}
function loadSW({ fetchImpl, cacheMatch }) {
const listeners = {};
const puts = [];
const sandbox = {
self: {
addEventListener: (type, fn) => { listeners[type] = fn; },
skipWaiting: () => {},
},
caches: {
open: async () => ({
addAll: async () => {},
put: async (req, res) => { puts.push([String(req && req.url || req), res]); },
}),
keys: async () => [],
delete: async () => true,
match: cacheMatch,
},
clients: { claim: () => {} },
fetch: fetchImpl,
Response: FakeResponse,
URL,
console,
};
vm.createContext(sandbox);
vm.runInContext(swSource, sandbox);
return { listeners, puts };
}
function navigateEvent(url) {
const event = {
request: { url, method: 'GET', mode: 'navigate' },
responded: null,
};
event.respondWith = p => { event.responded = p; };
event.passThrough = () => { event.responded = null; };
return event;
}
test('offline navigation falls back to the cached index.html', async () => {
const index = new FakeResponse('html');
const { listeners } = loadSW({
fetchImpl: async () => { throw new Error('offline'); },
cacheMatch: async req => (String(req && req.url || req) === 'index.html' ? index : undefined),
});
const event = navigateEvent('http://127.0.0.1:8080/');
listeners.fetch(event);
const res = await event.responded;
assert.strictEqual(res, index);
});
test('offline navigation with empty cache resolves to an offline Response', async () => {
const { listeners } = loadSW({
fetchImpl: async () => { throw new Error('offline'); },
cacheMatch: async () => undefined,
});
const event = navigateEvent('http://127.0.0.1:8080/');
listeners.fetch(event);
const res = await event.responded;
assert.ok(res instanceof FakeResponse);
assert.strictEqual(res.status, 503);
});
test('a successful navigation is cached and returned', async () => {
const page = new FakeResponse('html');
const { listeners, puts } = loadSW({
fetchImpl: async () => page,
cacheMatch: async () => undefined,
});
const event = navigateEvent('http://127.0.0.1:8080/help.html');
listeners.fetch(event);
const res = await event.responded;
assert.strictEqual(res, page);
await new Promise(r => setImmediate(r));
assert.strictEqual(puts.length, 1);
assert.strictEqual(puts[0][0], 'http://127.0.0.1:8080/help.html');
});
test('api requests bypass the service worker', () => {
const { listeners } = loadSW({
fetchImpl: async () => { throw new Error('unexpected'); },
cacheMatch: async () => undefined,
});
const event = navigateEvent('http://127.0.0.1:8080/api/status');
event.request.mode = 'cors';
listeners.fetch(event);
assert.strictEqual(event.responded, null);
});
test('uncached asset hits the network and gets cached', async () => {
const asset = new FakeResponse('js');
const { listeners, puts } = loadSW({
fetchImpl: async () => asset,
cacheMatch: async () => undefined,
});
const event = navigateEvent('http://127.0.0.1:8080/des-bundle.js');
event.request.mode = 'cors';
listeners.fetch(event);
const res = await event.responded;
assert.strictEqual(res, asset);
await new Promise(r => setImmediate(r));
assert.strictEqual(puts.length, 1);
});
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-otaman-server"
version = "1.9.28"
version = "2.0.0"
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+74 -29
View File
@@ -19,7 +19,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
VERSION = '1.9.28'
VERSION = '2.0.0'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -57,6 +57,38 @@ def _tlog(msg):
sys.stderr.write('TIMING [+%7.3fs] %s\n' % (time.time() - _T0, msg))
_APDU_TIMES = []
_APDU_TIME_COLLECT = False
def _classify_apdu(cmd):
"""Map a command APDU to a snapshot timing category by instruction byte."""
if not cmd or len(cmd) < 4:
return None
return {'A4': 'select', 'B0': 'read_binary', 'B2': 'read_record'}.get(cmd[2:4].upper())
def _collect_apdu_times():
"""Start collecting per-command times. (Re)attaches our tracer if pySim
nulled it (equip does). Callers hold _CARD_LOCK, so collection cannot be
interleaved by the background poll thread."""
global _APDU_TIME_COLLECT
scc = getattr(_server_ref, 'scc', None) if _server_ref else None
tp = getattr(scc, '_tp', None) if scc else None
if tp is not None and tp.apdu_tracer is None:
tp.apdu_tracer = _LoggingApduTracer()
_APDU_TIMES.clear()
_APDU_TIME_COLLECT = True
def _end_apdu_time_collection():
"""Stop collecting and return the collected [{type, ms}, ...] list."""
global _APDU_TIME_COLLECT
_APDU_TIME_COLLECT = False
times = list(_APDU_TIMES)
_APDU_TIMES.clear()
return times
class StderrApduTracer(ApduTracer):
def __init__(self):
super().__init__()
@@ -75,6 +107,10 @@ class StderrApduTracer(ApduTracer):
global _APDU_N
_APDU_N += 1
elapsed = int((time.time() - self._cmd_start) * 1000)
if _APDU_TIME_COLLECT:
category = _classify_apdu(cmd)
if category:
_APDU_TIMES.append({'type': category, 'ms': elapsed})
if _TIMING:
msg = 'APDU-TRACE(+%7.3fs #%d, %dms): %s → SW: %s' % (time.time() - _T0, _APDU_N, elapsed, cmd, sw)
else:
@@ -1916,10 +1952,14 @@ class PysimHandler(BaseHTTPRequestHandler):
return
lchan = rs.lchan[0]
try:
if path:
_select_path(lchan, path, app)
else:
_select_with_parent(lchan, name, parent_sel, app)
_collect_apdu_times()
try:
if path:
_select_path(lchan, path, app)
else:
_select_with_parent(lchan, name, parent_sel, app)
finally:
apdu_times = _end_apdu_time_collection()
cur = lchan.selected_file
data = {
'name': cur.name if cur else None,
@@ -1929,6 +1969,7 @@ class PysimHandler(BaseHTTPRequestHandler):
'record_len': lchan.selected_file_record_len() if lchan else None,
'num_of_rec': lchan.selected_file_num_of_rec() if lchan else None,
'fci_hex': (lchan.selected_file_fcp_hex or '').upper() if lchan and lchan.selected_file_fcp_hex else None,
'apdu_times': apdu_times,
'exists': True,
}
self._send_json(data)
@@ -1957,28 +1998,32 @@ class PysimHandler(BaseHTTPRequestHandler):
return
lchan = rs.lchan[0]
try:
sel = fid if fid else name
if path:
_select_path(lchan, path, app)
else:
_select_with_parent(lchan, sel, parent_sel, app)
ft = _get_file_type(lchan, lchan.selected_file)
is_record = ft in ('linear_fixed', 'cyclic')
if mode == 'decoded':
cmd = 'read_records_decoded' if is_record else 'read_binary_decoded'
else:
cmd = 'read_records' if is_record else 'read_binary'
out = StringIO()
old_stdout = app.stdout
old_stderr = sys.stderr
app.stdout = out
sys.stderr = out
_collect_apdu_times()
try:
app.onecmd_plus_hooks(cmd)
output = _strip_ansi(out.getvalue())
sel = fid if fid else name
if path:
_select_path(lchan, path, app)
else:
_select_with_parent(lchan, sel, parent_sel, app)
ft = _get_file_type(lchan, lchan.selected_file)
is_record = ft in ('linear_fixed', 'cyclic')
if mode == 'decoded':
cmd = 'read_records_decoded' if is_record else 'read_binary_decoded'
else:
cmd = 'read_records' if is_record else 'read_binary'
out = StringIO()
old_stdout = app.stdout
old_stderr = sys.stderr
app.stdout = out
sys.stderr = out
try:
app.onecmd_plus_hooks(cmd)
output = _strip_ansi(out.getvalue())
finally:
app.stdout = old_stdout
sys.stderr = old_stderr
finally:
app.stdout = old_stdout
sys.stderr = old_stderr
apdu_times = _end_apdu_time_collection()
sw_match = re.search(r'SW:\s*(\w+)', output)
err_match = re.search(r'got (\w+)', output)
if err_match:
@@ -1995,18 +2040,18 @@ class PysimHandler(BaseHTTPRequestHandler):
if mode == 'decoded':
try:
parsed = json.loads(clean)
resp = {'success': True, 'sw': sw, 'file_type': ft, 'decoded': parsed}
resp = {'success': True, 'sw': sw, 'file_type': ft, 'decoded': parsed, 'apdu_times': apdu_times}
except json.JSONDecodeError:
resp = {'success': True, 'sw': sw, 'file_type': ft, 'data': clean}
resp = {'success': True, 'sw': sw, 'file_type': ft, 'data': clean, 'apdu_times': apdu_times}
elif is_record:
records = []
for line in clean.split('\n'):
m = re.match(r'^(\d+)\s(.+)', line)
if m:
records.append({'num': int(m.group(1)), 'data': m.group(2)})
resp = {'success': True, 'sw': sw, 'file_type': ft, 'records': records}
resp = {'success': True, 'sw': sw, 'file_type': ft, 'records': records, 'apdu_times': apdu_times}
else:
resp = {'success': True, 'sw': sw, 'file_type': ft, 'data': clean}
resp = {'success': True, 'sw': sw, 'file_type': ft, 'data': clean, 'apdu_times': apdu_times}
self._send_json(resp)
self._log_resp(resp)
except Exception as e:
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Tests for per-command APDU timing collection (card snapshot measurements)."""
import sys
import time
import types
import unittest
from pathlib import Path
from unittest import mock
PROJECTS = Path(__file__).resolve().parents[2]
PY_SIM = PROJECTS / 'pysim'
if str(PY_SIM) not in sys.path:
sys.path.insert(0, str(PY_SIM))
import pysim_otaman_server.server as S
class TestClassifyApdu(unittest.TestCase):
def test_select(self):
self.assertEqual(S._classify_apdu('00a40004023f0000'), 'select')
def test_read_binary(self):
self.assertEqual(S._classify_apdu('00b000000a'), 'read_binary')
def test_read_record(self):
self.assertEqual(S._classify_apdu('00b2010428'), 'read_record')
def test_other_not_classified(self):
self.assertIsNone(S._classify_apdu('80f2000c00'))
def test_short_input(self):
self.assertIsNone(S._classify_apdu(''))
self.assertIsNone(S._classify_apdu('00'))
class TestApduTimeCollection(unittest.TestCase):
def setUp(self):
self.saved = (S._APDU_TIME_COLLECT, list(S._APDU_TIMES), S._server_ref)
S._APDU_TIME_COLLECT = False
S._APDU_TIMES.clear()
S._server_ref = None
def tearDown(self):
S._APDU_TIME_COLLECT, times, S._server_ref = self.saved
S._APDU_TIMES[:] = times
def test_disabled_does_not_collect(self):
tracer = S.StderrApduTracer()
with mock.patch.object(S.os, 'write'):
tracer.trace_command('00a40004023f0000')
tracer.trace_response('00a40004023f0000', '9000', '')
self.assertEqual(S._APDU_TIMES, [])
def test_collects_only_classified_commands_with_ms(self):
S._collect_apdu_times()
tracer = S.StderrApduTracer()
with mock.patch.object(S.os, 'write'):
tracer._cmd_start = time.time() - 0.025
tracer.trace_response('00a40004023f0000', '9000', '')
tracer._cmd_start = time.time() - 0.010
tracer.trace_response('00b000000a', '9000', '')
tracer._cmd_start = time.time() - 0.005
tracer.trace_response('80f2000c00', '9000', '')
times = S._end_apdu_time_collection()
self.assertEqual([t['type'] for t in times], ['select', 'read_binary'])
self.assertGreaterEqual(times[0]['ms'], 20)
self.assertFalse(S._APDU_TIME_COLLECT)
self.assertEqual(S._APDU_TIMES, [])
def test_collect_reattaches_tracer_when_missing(self):
tp = types.SimpleNamespace(apdu_tracer=None)
scc = types.SimpleNamespace(_tp=tp)
S._server_ref = types.SimpleNamespace(scc=scc)
S._collect_apdu_times()
try:
self.assertIsInstance(tp.apdu_tracer, S._LoggingApduTracer)
finally:
S._end_apdu_time_collection()
def test_collect_keeps_existing_tracer(self):
tracer = S.StderrApduTracer()
tp = types.SimpleNamespace(apdu_tracer=tracer)
scc = types.SimpleNamespace(_tp=tp)
S._server_ref = types.SimpleNamespace(scc=scc)
S._collect_apdu_times()
try:
self.assertIs(tp.apdu_tracer, tracer)
finally:
S._end_apdu_time_collection()
if __name__ == '__main__':
unittest.main()