Compare commits
11 Commits
fe10603aea
...
f1ed6a2a48
| Author | SHA1 | Date | |
|---|---|---|---|
| f1ed6a2a48 | |||
| 4b19b58bb0 | |||
| 6f63b5172c | |||
| 6d30e7095e | |||
| a261675aad | |||
| 450f1de68a | |||
| 74efc20bf5 | |||
| d2bd2ed798 | |||
| ccd0b6018c | |||
| 9363f209ee | |||
| 33443d4f28 |
@@ -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
@@ -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
@@ -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
@@ -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> открывает эту документацию на разделе, соответствующем текущему представлению (например, подвкладка «Профайлер» открывает §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>«Парсер ответов»</strong>), <strong>SCP80</strong> (<strong>Secured Packet</strong>, <strong>Карты</strong>, <strong>RAM</strong>), <strong>«Картридер»</strong> (<strong>Файловый менеджер</strong>, <strong>Пользовательские файлы</strong>, <strong>Командная строка pySim</strong>, <strong>Отправка APDU</strong>), <strong>«Профайлер»</strong> и <strong>«Симулятор телефона»</strong>.</li>
|
||||
<li>Ссылка <strong>справка</strong> открывает эту документацию на разделе, соответствующем текущему представлению (например, вкладка «Профайлер» открывает §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>«Парсер ответов»</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">§3.1</a>): статус PoR (TAR, счётчик, сырой PoR), а статусное слово и данные ответа последней команды подставляются на вкладку <strong>«Парсер ответов»</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">§3.1</a>): статус PoR (TAR, счётчик, сырой PoR), а статусное слово и данные ответа последней команды подставляются в подвкладку <strong>«Парсер ответов»</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 «Парсер ответов»</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">§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, …) или группа 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 b4 должны быть <code class="font-mono text-sm">10</code> (счётчик больше) или <code class="font-mono text-sm">11</code> (счётчик +1) согласно TS 102 225 §5.1.2/§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>«Парсер ответов»</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>«Парсер ответов»</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. Вкладка «Парсер ответов»</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">§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, …) или группа 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. Вкладка «Картридер» (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>«Профайлер»</strong> и <strong>«Симулятор телефона»</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. Вкладка «Картридер» (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>; добавленные файлы появляются в дереве «Файловый менеджер». Список сохраняется в <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 мс) и автодополнением.</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, вверху этого представления появляется блок «Меню STK» с изумрудной кнопкой <strong>STK: <название></strong>, открывающей оверлей меню (браузер STK-меню карты). Если карта не задала меню, вместо кнопки показывается «Меню не задано картой». Состояние меню обновляется при каждом открытии представления. Интерактивные проактивные команды всегда получают 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 — Работа с файлами, не входящими в модель pySim («Пользовательские файлы»)</h4>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
|
||||
<li>Получите FID целевого файла (документация вендора или анализ ATR/файловой системы; такие файлы часто отсутствуют в открытых спецификациях).</li>
|
||||
<li>Откройте вкладку <strong>«Картридер»</strong> → подвкладку <strong>«Пользовательские файлы»</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 — Симуляция реальной сетевой среды для тестирования 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> → <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 с), чтобы карта периодически выдавала 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> выберите тип регистрации → поля местоположения → технологию доступа → причину отклонения.</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, …) — уведомление в один клик</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 §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 · <strong>03</strong> Дата/время/TZ · <strong>04</strong> Язык · <strong>05</strong> Timing Advance</li>
|
||||
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
||||
<li><strong>0A</strong> Battery · <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–255 с, по умолчанию 30 с, <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>«Проверка FCP/FCI»</strong> (те же три режима, по умолчанию <strong>Тип файла + размер (FCP)</strong>), применяемый ко всем создаваемым правилам, а также список <strong>«Игнорировать содержимое файлов»</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>«Сравнивать первые 4 байта для»</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>«Пользовательские файлы»</strong> включаются с той же проверкой существования.</p>
|
||||
|
||||
<h4 id="card-snapshots" class="font-medium mb-1">Снимки карт</h4>
|
||||
<p class="text-sm mb-2">Представление списка имеет две вкладки — <strong>«Профили»</strong> и <strong>«Снимки карт»</strong>. Снимок карты — неизменяемая фиксация файловой системы карты: для каждого существующего файла сохраняются путь, символьное имя, тип, размер (или длина/число записей), сырой FCI из ответа SELECT и содержимое, если файл читается (без списка игнорирования и без масок). ICCID декодируется из EF.ICCID и показывается рядом с именем снимка.</p>
|
||||
<p class="text-sm mb-2">Представление списка имеет две вкладки — <strong>«Профили»</strong> и <strong>«Снимки карт»</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> — запрашивает имя и сканирует карту, затем возвращает к списку.</li>
|
||||
<li><strong>Импорт снимка</strong> — загружает снимок из 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 — Работа с файлами, не входящими в модель pySim («Пользовательские файлы»)</h4>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
|
||||
<li>Получите FID целевого файла (документация вендора или анализ ATR/файловой системы; такие файлы часто отсутствуют в открытых спецификациях).</li>
|
||||
<li>Откройте вкладку <strong>«Картридер»</strong> → подвкладку <strong>«Пользовательские файлы»</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 — Симуляция реальной сетевой среды для тестирования 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> → <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 с), чтобы карта периодически выдавала 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> выберите тип регистрации → поля местоположения → технологию доступа → причину отклонения.</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">Для работы с картой (вкладка «Картридер», «Проактивный UICC», доставка 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>«Телефон»</strong> (меню STK, STATUS и опрос, подписанные события, журнал проактивных команд) и <strong>«Конфигурация TR»</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, вверху этого представления появляется блок «Меню STK» с изумрудной кнопкой <strong>STK: <название></strong>, открывающей оверлей меню (браузер STK-меню карты). Если карта не задала меню, вместо кнопки показывается «Меню не задано картой». Состояние меню обновляется при каждом открытии представления. Интерактивные проактивные команды всегда получают 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, …) — уведомление в один клик</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–255 с, по умолчанию 30 с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
|
||||
|
||||
<h3 id="pli-dict" class="text-lg font-medium mb-2">6.5 «Конфигурация TR» — данные ответа PROVIDE LOCAL INFORMATION</h3>
|
||||
<p class="text-sm mb-2">Редактируемые hex-значения для всех 22 квалификаторов PLI (TS 102 223 §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 · <strong>03</strong> Дата/время/TZ · <strong>04</strong> Язык · <strong>05</strong> Timing Advance</li>
|
||||
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
||||
<li><strong>0A</strong> Battery · <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">Для работы с картой (вкладка «Картридер», «Симулятор телефона», доставка 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–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 («Desktop development with C++»). Мост SMPP (<code class="font-mono text-sm">smpp.twisted3</code>) на Windows намеренно не устанавливается, поэтому для Python 3.10–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">Если карта отсутствует, вкладка «Картридер» показывает «Карта не обнаружена. Вставьте карту и нажмите Подключить карту».</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
@@ -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 §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 §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">§3.1</a>): the PoR status (TAR, counter, raw PoR), with the last command’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">§3.1</a>): the PoR status (TAR, counter, raw PoR), with the last command’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 — 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’s status word and response data after a successful “Send to Card” (see <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">§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, …) 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’s status word and response data after a successful “Send to Card” (see <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">§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, …) 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’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 & 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 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 “STK menu” block appears at the top of this view with an emerald <strong>STK: <title></strong> button that opens the menu overlay (same as the card’s STK menu browser). If the card has not set up a menu, the block shows “No menu set by the card” 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 — Working with files not in pySim’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 → <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 — 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> → <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 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 → location fields → access technology → 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, …) — 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 §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 · <strong>03</strong> Date/Time/TZ · <strong>04</strong> Language · <strong>05</strong> Timing Advance</li>
|
||||
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
||||
<li><strong>0A</strong> Battery · <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–255 s, default 30 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>“FCP/FCI check”</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>“Ignore contents of files”</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>“Match first 4 bytes for”</strong> <code class="font-mono text-sm">EF.IMSI</code> and <code class="font-mono text-sm">EF.ICCID</code> capture those files’ 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 — <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 — <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> — asks for a name and scans the card, then returns to the list.</li>
|
||||
<li><strong>Import snapshot</strong> — 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 — Working with files not in pySim’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 → <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 — 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> → <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 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 → location fields → access technology → 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 “STK menu” block appears at the top of this view with an emerald <strong>STK: <title></strong> button that opens the menu overlay (same as the card’s STK menu browser). If the card has not set up a menu, the block shows “No menu set by the card” 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, …) — 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–255 s, default 30 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 — PROVIDE LOCAL INFORMATION response data</h3>
|
||||
<p class="text-sm mb-2">Editable hex values for all 22 PLI qualifiers (TS 102 223 §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 · <strong>03</strong> Date/Time/TZ · <strong>04</strong> Language · <strong>05</strong> Timing Advance</li>
|
||||
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
||||
<li><strong>0A</strong> Battery · <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–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 (“Desktop development with C++”). 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–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 “No card detected”. 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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 => {
|
||||
|
||||
@@ -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"'));
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user