scp81 passthru, proactive decoders, ADM badge, file manager layout (v2.2.1)

SCP81:
- New listener mode "passthru": no local listener - the card's BIP
  channels connect straight to a configured external platform
  (host/port required), which terminates TLS and runs the dialog.
  The status API reports mode/target (_SCP81_MODE/_SCP81_TARGET) and
  clears them on stop. PWA mode select, hint, Start validation; TLS PSK
  stays the default.

Proactive command decoding (Phone tab log):
- SEND SHORT MESSAGE (0x13) is now decoded: alpha, address (TON/NPI +
  number), 3GPP-SMS TPDU (type, TP-MR, TP-DA, TP-PID 0x7F flagged as
  SIM data download, TP-DCS, TP-VP, TP-UDL), UDH concatenation IEs, and
  the TP-UD as text (GSM-7 with a septet unpacker, UCS2, 8-bit) or as a
  TS 31.115 secured packet for PID 0x7F; malformed TPDUs fall back to
  the raw hex line.
- PROVIDE LOCAL INFORMATION qualifier names completed per TS 102 223
  V18.3.0: ESN (07), MEID (0B), Supported RATs (1A); 05 relabelled
  "Reserved for GSM (Timing Advance)". Fixed in the server dict and
  both frontend tables.

Header:
- Compact ADM badge next to the card indicator: "ADM ✓" green when
  pySim's adm_verified is set, "ADM ✗" red otherwise, hidden without a
  card session or when the server is down; updated ahead of the card
  state-key early return so it never disturbs the connect/reset flow.

File manager:
- Sort pills (FID/Name), Probe all files button and progress line are
  pinned above the tree instead of scrolling with it; the tree box cap
  grows from 420px to 65vh.

SW cache otaman-v165; help EN/RU updated; tests 234 Python + 361 frontend.
This commit is contained in:
2026-09-16 15:29:01 +03:00
parent 8c90958718
commit 20fbfd99a5
12 changed files with 479 additions and 39 deletions
+3 -3
View File
@@ -42,7 +42,7 @@
<h3 id="interface" class="text-lg font-medium mb-2">1.1 Интерфейс</h3>
<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><strong>Шапка</strong> — версия приложения, кнопка <strong>INSTALL PWA</strong> (появляется, когда браузер предлагает установку, для офлайн-работы), ссылки на проект на GitHub и на эту справку, переключатель языка <strong>EN/RU</strong> и переключатель тёмной/светлой <strong>темы</strong>. Рядом с заголовком небольшой индикатор показывает состояние сервера/карты (серая точка = подключение, красная = нет сервера, значки карты = нет карты / инициализация / карта готова) и компактный значок <strong>ADM ✓</strong> (зелёный, администраторский PIN подтверждён) или <strong>ADM ✗</strong> (красный, не подтверждён) для текущей сессии карты.</li>
<li>Выбор языка и темы хранится в <code class="font-mono text-sm">localStorage</code> и сохраняется между перезагрузками.</li>
<li>Вкладки верхнего уровня: <strong>Remote APDU</strong> (<strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, <strong>Разбор C-APDU</strong>, <strong>&laquo;Парсер ответов&raquo;</strong>), <strong>SCP80</strong> (<strong>Secured Packet</strong>, <strong>RAM</strong>), <strong>SCP81</strong> (<strong>&laquo;Слушатель&raquo;</strong>, <strong>&laquo;Скрипты&raquo;</strong>), <strong>&laquo;Карты&raquo;</strong>, <strong>&laquo;Профайлер&raquo;</strong> (вкладки <strong>&laquo;Профили&raquo;</strong>, <strong>&laquo;Снимки карт&raquo;</strong>, <strong>&laquo;Пользовательские файлы&raquo;</strong>), <strong>&laquo;Картридер&raquo;</strong> (<strong>Файловый менеджер</strong>, <strong>Командная строка pySim</strong>, <strong>Отправка APDU</strong>) и <strong>&laquo;Симулятор телефона&raquo;</strong>.</li>
<li>Ссылка <strong>справка</strong> открывает эту документацию на разделе, соответствующем текущему представлению (например, вкладка &laquo;Профайлер&raquo; открывает &sect;5).</li>
@@ -319,7 +319,7 @@
<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>Командная строка pySim</strong> и <strong>Отправка APDU</strong>. <strong>&laquo;Профайлер&raquo;</strong> и <strong>&laquo;Симулятор телефона&raquo;</strong> — отдельные вкладки верхнего уровня.</p>
<h3 id="file-manager" class="text-lg font-medium mb-2">4.1 Файловый менеджер</h3>
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по <strong>FID</strong> или символьному <strong>имени</strong> (пиллы над деревом; выбор сохраняется в <code class="font-mono text-sm">localStorage</code>). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI.</p>
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по <strong>FID</strong> или символьному <strong>имени</strong> (пиллы и кнопка <strong>«Проверить все файлы»</strong> закреплены над прокручиваемым деревом; выбор сохраняется в <code class="font-mono text-sm">localStorage</code>). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Прочитать</strong> — чтение файла (автоопределение transparent/record)</li>
<li><strong>Редактировать</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>)</li>
@@ -429,7 +429,7 @@
<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>
<p class="text-sm mb-2">Хронологический список извлечённых проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор (для команд, у которых он есть). Раскрытие строки показывает декодированную команду: текст и пункты DISPLAY TEXT / SELECT ITEM / SET UP MENU, параметры BIP-каналов, действия TIMER MANAGEMENT, названия квалификаторов PROVIDE LOCAL INFORMATION (все стандартные, включая ESN 07, MEID 0B и Supported RATs 1A) и SEND SHORT MESSAGE — разбирается SMS TPDU (тип, TP-MR, TP-DA, TP-PID, TP-DCS, срок действия, TP-UDL), а пользовательские данные показываются как текст для текстовых кодировок (GSM&nbsp;7-бит, UCS2, 8-бит) или как secured-пакет (TS&nbsp;31.115) при TP-PID&nbsp;=&nbsp;<code class="font-mono text-sm">7F</code> (SIM data download); заголовки конкатенации UDH декодируются. Для команд с данными ответа показывается строка <code class="font-mono text-sm">Ответ:</code> с байтами TERMINAL RESPONSE (без служебных TLV); ответы PROVIDE LOCAL INFORMATION декодируются через словарь данных PLI.</p>
<h3 id="status-polling" class="text-lg font-medium mb-2">6.4 Опрос STATUS</h3>
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Переключатель <strong>Опрос</strong> включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;с, по умолчанию 30&nbsp;с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
+3 -3
View File
@@ -42,7 +42,7 @@
<h3 id="interface" class="text-lg font-medium mb-2">1.1 Interface</h3>
<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><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. Next to the title a small status indicator shows the server/card state (gray dot = connecting, red dot = no server connection, card icons = no card / equipping / equipped) and a compact <strong>ADM ✓</strong> (green, administrator PIN verified) or <strong>ADM ✗</strong> (red, not verified) badge for the current card session.</li>
<li>Language and theme choices are stored in <code class="font-mono text-sm">localStorage</code> and persist across reloads.</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>RAM</strong>), <strong>SCP81</strong> (<strong>Listener</strong>, <strong>Scripts</strong>), <strong>Cards</strong>, <strong>Profiler</strong> (list tabs <strong>Profiles</strong>, <strong>Card snapshots</strong>, <strong>Custom files</strong>), <strong>Card reader</strong> (<strong>File manager</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong>), and <strong>Phone simulator</strong>.</li>
<li>The <strong>help</strong> link opens this documentation at the section matching the current view (e.g. the Profiler tab opens &sect;5).</li>
@@ -319,7 +319,7 @@
<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>pySim command line</strong>, and <strong>Raw APDU</strong>. The <strong>Profiler</strong> and <strong>Phone simulator</strong> are separate top-level tabs.</p>
<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. Entries are grouped with DFs above EFs and sorted by <strong>FID</strong> or symbolic <strong>Name</strong> (pills above the tree; the choice is remembered in <code class="font-mono text-sm">localStorage</code>). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI above the content pane.</p>
<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. Entries are grouped with DFs above EFs and sorted by <strong>FID</strong> or symbolic <strong>Name</strong> (pills pinned above the scrolling tree together with <strong>Probe all files</strong>; the choice is remembered in <code class="font-mono text-sm">localStorage</code>). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI above the content pane.</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>
<li><strong>Edit</strong> — modify hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>)</li>
@@ -429,7 +429,7 @@
<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>
<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). Expanding a row shows the decoded command: DISPLAY TEXT / SELECT ITEM / SET UP MENU text and items, BIP channel parameters, TIMER MANAGEMENT actions, PROVIDE LOCAL INFORMATION qualifier names (all standard qualifiers, including ESN 07, MEID 0B and Supported RATs 1A), and SEND SHORT MESSAGE &mdash; the SMS TPDU is parsed (type, TP-MR, TP-DA, TP-PID, TP-DCS, validity period, TP-UDL) with the user data shown as text for text codings (GSM&nbsp;7-bit, UCS2, 8-bit) or as a secured packet (TS&nbsp;31.115) when TP-PID&nbsp;=&nbsp;<code class="font-mono text-sm">7F</code> (SIM data download); UDH concatenation headers are decoded. Commands with response data show a <code class="font-mono text-sm">Response:</code> line with the TERMINAL RESPONSE bytes (boilerplate TLVs stripped); PROVIDE LOCAL INFORMATION responses are decoded using the PLI data dictionary decoders.</p>
<h3 id="status-polling" class="text-lg font-medium mb-2">6.4 STATUS polling</h3>
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). A <strong>Polling</strong> toggle enables background polling: after a configurable idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;s, default 30&nbsp;s, <code class="font-mono text-sm">0</code> disables polling) the server sends STATUS and handles any pending proactive command. Polling stops and card state resets if the card is removed.</p>
+69 -17
View File
@@ -18,11 +18,12 @@
<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">v2.2.0</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.2.1</span></h1>
<div class="flex items-center gap-4">
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting..."></span>
<img id="state-indicator-img" class="hidden dark:invert" style="width:30px;height:30px" alt="" src="nosim.svg">
<span id="state-indicator-adm" class="hidden ml-0.5 text-xs font-mono" title=""></span>
</span>
<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>
@@ -824,15 +825,19 @@
<div id="pysim-sub-files">
<div class="flex gap-2">
<div class="w-1/3 border border-gray-300 dark:border-slate-600 rounded p-2 text-xs font-mono overflow-y-auto bg-gray-50 dark:bg-slate-800" style="max-height:420px">
<div class="flex items-center gap-1 mb-1 text-gray-500 dark:text-slate-400">
<span data-l10n="Sort:">Sort:</span>
<span class="pysim-fs-sort-pill px-2 py-0.5 rounded cursor-pointer bg-blue-600 text-white" data-fs-sort="fid" onclick="pysimFsSetSort('fid')">FID</span>
<span class="pysim-fs-sort-pill px-2 py-0.5 rounded cursor-pointer bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300" data-fs-sort="name" onclick="pysimFsSetSort('name')" data-l10n="Name">Name</span>
<div class="w-1/3 flex flex-col">
<div class="shrink-0 text-xs font-mono">
<div class="flex items-center gap-1 mb-1 text-gray-500 dark:text-slate-400">
<span data-l10n="Sort:">Sort:</span>
<span class="pysim-fs-sort-pill px-2 py-0.5 rounded cursor-pointer bg-blue-600 text-white" data-fs-sort="fid" onclick="pysimFsSetSort('fid')">FID</span>
<span class="pysim-fs-sort-pill px-2 py-0.5 rounded cursor-pointer bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300" data-fs-sort="name" onclick="pysimFsSetSort('name')" data-l10n="Name">Name</span>
</div>
<button id="pysim-fs-probe-btn" data-needs="card" onclick="pysimFsProbeAll()" class="w-full mb-1 px-2 py-0.5 rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Probe all files">Probe all files</button>
<div id="pysim-fs-probe-status" class="hidden mb-1 text-gray-500 dark:text-slate-400"></div>
</div>
<div class="border border-gray-300 dark:border-slate-600 rounded p-2 text-xs font-mono overflow-y-auto bg-gray-50 dark:bg-slate-800" style="max-height:65vh">
<div id="pysim-fs-tree"></div>
</div>
<button id="pysim-fs-probe-btn" data-needs="card" onclick="pysimFsProbeAll()" class="w-full mb-1 px-2 py-0.5 rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Probe all files">Probe all files</button>
<div id="pysim-fs-probe-status" class="hidden mb-1 text-gray-500 dark:text-slate-400"></div>
<div id="pysim-fs-tree"></div>
</div>
<div class="flex-1">
<div id="pysim-fs-detail" class="hidden">
@@ -939,6 +944,7 @@
<label class="block mb-1 text-xs font-medium text-gray-600 dark:text-slate-400" data-l10n="Mode">Mode</label>
<select id="scp81-mode" onchange="scp81ModeChanged()" class="border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
<option value="tls" data-l10n="PSK TLS server" selected>PSK TLS server</option>
<option value="passthru" data-l10n="Pass-through (external server)">Pass-through (external server)</option>
<option value="dump" data-l10n="Capture (dump)">Capture (dump)</option>
</select>
</div>
@@ -959,7 +965,8 @@
<button id="scp81-restart-btn" data-needs="server" onclick="scp81RestartScript()" class="px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Restart script">Restart script</button>
</div>
<div id="scp81-msg" class="text-xs mt-2 hidden"></div>
<div class="mt-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="PSK keys come from the card presets (Cards tab): the listener picks the key by the identity the card sends in the TLS handshake.">PSK keys come from the card presets (Cards tab): the listener picks the key by the identity the card sends in the TLS handshake.</div>
<div id="scp81-psk-note" class="mt-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="PSK keys come from the card presets (Cards tab): the listener picks the key by the identity the card sends in the TLS handshake.">PSK keys come from the card presets (Cards tab): the listener picks the key by the identity the card sends in the TLS handshake.</div>
<div id="scp81-passthru-note" class="hidden mt-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Pass-through: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, the card's requested address is only logged.">Pass-through: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, the card's requested address is only logged.</div>
</div>
<div class="border border-gray-200 dark:border-slate-700 rounded p-3 mb-3">
<div class="flex justify-between items-center mb-2">
@@ -4585,6 +4592,7 @@ async function pysimFetch(path, body) {
let _pysimServerAvailable = null; // null until probed
let _pysimCardEquipped = false;
let _pysimEquipping = false;
let _pysimAdmVerified = null; // null = no card session
let _pysimAvailabilityTimer = null;
function pysimAvailabilityState() {
@@ -4638,9 +4646,33 @@ function pysimApplyAvailability() {
pysimUpdateStateIndicator();
}
// Compact ADM state next to the header's card indicator: "ADM ✓" when the
// administrator PIN was verified (pySim rs.adm_verified), "ADM ✗" otherwise.
// Hidden without a card session; only rewrites the DOM when the state changes
// (the 2s /api/status poll calls this on every update).
function pysimUpdateAdmIndicator(status) {
const el = document.getElementById('state-indicator-adm');
if (!el) return;
const verified = (status && status.connected) ? !!status.adm_verified : null;
if (verified === _pysimAdmVerified) return;
_pysimAdmVerified = verified;
el.classList.remove('text-emerald-600', 'dark:text-emerald-400', 'text-red-500');
if (verified === null) {
el.classList.add('hidden');
el.removeAttribute('title');
return;
}
el.textContent = verified ? 'ADM ✓' : 'ADM ✗';
el.classList.remove('hidden');
el.classList.add(verified ? 'text-emerald-600' : 'text-red-500');
if (verified) el.classList.add('dark:text-emerald-400');
el.setAttribute('title', t(verified ? 'ADM verified' : 'ADM not verified'));
}
function pysimSetServerAvailable(available) {
if (_pysimServerAvailable === available) return;
_pysimServerAvailable = available;
if (available !== true) pysimUpdateAdmIndicator(null);
pysimApplyAvailability();
}
@@ -6851,15 +6883,18 @@ const EVENT_FORMS = {
const PLI_QUALIFIERS = [
{code:'00',name:'Location Info (MCC, MNC, LAC/TAC, Cell ID)'},{code:'01',name:'IMEI'},
{code:'02',name:'Network Measurement results'},{code:'03',name:'Date, time and time zone'},
{code:'04',name:'Language setting'},{code:'05',name:'Timing Advance'},
{code:'06',name:'Access Technology (single)'},{code:'08',name:'IMEISV'},
{code:'04',name:'Language setting'},{code:'05',name:'Reserved for GSM (Timing Advance)'},
{code:'06',name:'Access Technology (single)'},{code:'07',name:'ESN of the terminal'},
{code:'08',name:'IMEISV'},
{code:'09',name:'Search Mode'},{code:'0A',name:'Battery charge state'},
{code:'0B',name:'MEID of the terminal'},
{code:'0C',name:'Current WSID'},{code:'0D',name:'Broadcast Network info'},
{code:'0E',name:'Multiple Access Technologies'},{code:'0F',name:'Location Info (multi-RAT)'},
{code:'10',name:'NMR (multi-RAT)'},{code:'11',name:'CSG ID list + HNB name'},
{code:'12',name:'H(e)NB IP address'},{code:'13',name:'H(e)NB surrounding macrocells'},
{code:'14',name:'Current WLAN identifier'},{code:'15',name:'Slices information'},
{code:'16',name:'CAG information list'},{code:'17',name:'Rejected slices information'},
{code:'1A',name:'Supported Radio Access Technologies'},
];
async function pysimEventsRender() {
@@ -7085,6 +7120,7 @@ function pysimCardStateUpdate(status) {
_pysimServerAvailable = true;
_pysimCardEquipped = !!status.connected;
_pysimEquipping = !!status.equipping;
pysimUpdateAdmIndicator(status);
pysimApplyAvailability();
if (pysimProactiveSeqChanged(status.proactive_seq)
&& isViewVisible('tab-phone') && isViewVisible('phone-sub-phone')) {
@@ -7180,11 +7216,12 @@ const CMD_QUALIFIER_SHORT = {
'23': {0x00:'Digits+Echo', 0x01:'Alpha+Echo', 0x04:'NoEcho'},
'24': {0x00:'Normal', 0x02:'Help', 0x04:'DefaultItem', 0x80:'Nav'},
'25': {0x00:'Normal', 0x04:'Help', 0x80:'Nav'},
'26': {0x00:'Loc', 0x01:'IMEI', 0x02:'NMR', 0x03:'Time', 0x04:'Lang', 0x05:'TA',
0x06:'AccTech', 0x08:'IMEISV', 0x09:'Search', 0x0A:'Batt', 0x0C:'WSID',
'26': {0x00:'Loc', 0x01:'IMEI', 0x02:'NMR', 0x03:'Time', 0x04:'Lang', 0x05:'TA (GSM)',
0x06:'AccTech', 0x07:'ESN', 0x08:'IMEISV', 0x09:'Search', 0x0A:'Batt',
0x0B:'MEID', 0x0C:'WSID',
0x0D:'BCInfo', 0x0E:'MultiAT', 0x0F:'MultiLoc', 0x10:'MultiNMR',
0x11:'CSG', 0x12:'HNB-IP', 0x13:'HNB-Macro', 0x14:'WLAN', 0x15:'Slices',
0x16:'CAG', 0x17:'RejSlice'},
0x16:'CAG', 0x17:'RejSlice', 0x1A:'SupRAT'},
'27': {0x00:'Start', 0x01:'Deactivate', 0x02:'Get'},
};
@@ -7293,6 +7330,10 @@ function scp81ModeChanged() {
const mode = document.getElementById('scp81-mode').value;
const row = document.getElementById('scp81-script-row');
if (row) row.style.display = (mode === 'tls') ? '' : 'none';
const pskNote = document.getElementById('scp81-psk-note');
if (pskNote) pskNote.classList.toggle('hidden', mode !== 'tls');
const directNote = document.getElementById('scp81-passthru-note');
if (directNote) directNote.classList.toggle('hidden', mode !== 'passthru');
}
function scp81SwitchSubtab(name) {
@@ -7666,10 +7707,16 @@ async function scp81LogRefresh() {
async function scp81Start() {
const mode = document.getElementById('scp81-mode').value;
const hostVal = document.getElementById('scp81-host').value.trim();
const portVal = document.getElementById('scp81-port').value.trim();
if (mode === 'passthru' && (!hostVal || !portVal)) {
scp81Msg(t('Pass-through requires the target host and port'), 'text-red-500');
return;
}
const body = {
action: 'start', mode: mode,
host: document.getElementById('scp81-host').value.trim() || '127.0.0.1',
port: parseInt(document.getElementById('scp81-port').value.trim() || '8443', 10),
host: hostVal || '127.0.0.1',
port: parseInt(portVal || '8443', 10),
};
if (mode === 'tls') {
const map = cardsPskMap();
@@ -10734,6 +10781,11 @@ const LANG_RU = {
'LOAD blocks': 'Блоки LOAD',
'clamped from': 'ограничено с',
'auto-fit': 'авто',
'Pass-through (external server)': 'Проброс (внешний сервер)',
'Pass-through: every BIP channel the card opens is connected to this Host:Port (the external HTTP OTA platform); TLS is terminated there, the card\'s requested address is only logged.': 'Проброс: каждый открываемый картой BIP-канал подключается к этому Host:Port (внешняя платформа HTTP OTA); TLS завершается там, запрошенный картой адрес только журналируется.',
'Pass-through requires the target host and port': 'Для режима проброса нужны host и port внешнего сервера',
'ADM verified': 'ADM подтверждён',
'ADM not verified': 'ADM не подтверждён',
};
let currentLang = 'en';
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v161';
const CACHE = 'otaman-v165';
const URLS = [
'index.html',
'help.html',
+60 -3
View File
@@ -23,29 +23,52 @@ function extractFunc(src, name) {
let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n'
+ 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\n'
+ 'var _pysimProactiveSeq = null;\nvar _pysimStkSig = null;\n';
+ 'var _pysimProactiveSeq = null;\nvar _pysimStkSig = null;\nvar _pysimAdmVerified = null;\n';
code += extractFunc(html, 'pysimCardStateUpdate') + '\n';
code += extractFunc(html, 'pysimAvailabilityState') + '\n';
code += extractFunc(html, 'pysimControlDisabled') + '\n';
code += extractFunc(html, 'pysimProactiveSeqChanged') + '\n';
code += extractFunc(html, 'pysimStkStatusChanged') + '\n';
code += extractFunc(html, 'pysimUpdateAdmIndicator') + '\n';
code += extractFunc(html, 'pysimSetServerAvailable') + '\n';
code += '\nglobalThis.esc = s => s;\n';
code += 'globalThis.t = s => s;\n';
eval(code);
function fakeIndicator() {
const classes = new Set();
const el = {
classes, textContent: '', title: null,
classList: {
add: (...c) => c.forEach(x => classes.add(x)),
remove: (...c) => c.forEach(x => classes.delete(x)),
contains: c => classes.has(c),
},
setAttribute: (k, v) => { if (k === 'title') el.title = v; },
removeAttribute: (k) => { if (k === 'title') el.title = null; },
};
return el;
}
function setup() {
const el = { textContent: 'status line', innerHTML: '' };
const adm = fakeIndicator();
const calls = { connected: [], resets: [], refreshStatus: [], proactive: 0 };
_pysimCardStateKey = null;
_pysimCardSession = null;
_pysimProactiveSeq = null;
globalThis.document = { getElementById: () => el, querySelectorAll: () => [] };
_pysimAdmVerified = null;
_pysimServerAvailable = null;
globalThis.document = {
getElementById: id => id === 'state-indicator-adm' ? adm : el,
querySelectorAll: () => [],
};
globalThis.pysimSetConnected = v => calls.connected.push(v);
globalThis.pysimResetCardData = refresh => calls.resets.push(refresh);
globalThis.pysimApplyAvailability = () => {};
globalThis.isViewVisible = () => true;
globalThis.pysimProactiveLogRender = () => { calls.proactive++; };
return { el, calls };
return { el, adm, calls };
}
function status(extra) {
@@ -168,3 +191,37 @@ test('pysimStkStatusChanged detects menu state transitions', () => {
assert.ok(pysimStkStatusChanged({ active: true, pending: false }));
assert.ok(!pysimStkStatusChanged(null));
});
test('the header ADM badge shows verified / not verified / hidden', () => {
const { adm, calls } = setup();
pysimCardStateUpdate(status({ connected: true, adm_verified: true }));
assert.ok(!adm.classes.has('hidden'));
assert.strictEqual(adm.textContent, 'ADM ✓');
assert.ok(adm.classes.has('text-emerald-600'));
assert.ok(adm.classes.has('dark:text-emerald-400'));
assert.strictEqual(adm.title, 'ADM verified');
// an unchanged state must not rewrite the badge
adm.textContent = '';
pysimCardStateUpdate(status({ connected: true, adm_verified: true }));
assert.strictEqual(adm.textContent, '', 'unchanged ADM state rewrote the badge');
// verification lost (e.g. card reset)
pysimCardStateUpdate(status({ connected: true, adm_verified: false }));
assert.strictEqual(adm.textContent, 'ADM ✗');
assert.ok(adm.classes.has('text-red-500'));
assert.ok(!adm.classes.has('text-emerald-600'));
assert.strictEqual(adm.title, 'ADM not verified');
// no card session hides it
pysimCardStateUpdate(status({ connected: false }));
assert.ok(adm.classes.has('hidden'));
assert.strictEqual(adm.title, null);
// the ADM update must not disturb the connect/reset flow
assert.deepStrictEqual(calls.connected, [true, false]);
});
test('losing the server hides the ADM badge', () => {
const { adm } = setup();
pysimCardStateUpdate(status({ connected: true, adm_verified: true }));
assert.ok(!adm.classes.has('hidden'));
pysimSetServerAvailable(false);
assert.ok(adm.classes.has('hidden'));
});
+29
View File
@@ -25,6 +25,23 @@ test('cards list shows the SCP81 PSK column with blue/red row buttons', () => {
assert.match(fn[0], /cardsRemove\(' \+ i \+ '\)" class="[^"]*bg-red-600 text-white/);
});
test('PLI qualifier tables cover all standard qualifiers', () => {
// ESN (07), MEID (0B) and Supported RATs (1A) must at least be named, in
// both the TR Config dictionary and the proactive-log short labels.
const pli = /const PLI_QUALIFIERS = \[([\s\S]*?)\];/.exec(html);
assert.ok(pli, 'PLI_QUALIFIERS not found');
for (const code of ['07', '0B', '1A']) {
assert.ok(pli[1].includes("{code:'" + code + "'"), 'PLI_QUALIFIERS missing ' + code);
}
const block = /const CMD_QUALIFIER_SHORT = \{([\s\S]*?)\n\};/.exec(html);
assert.ok(block, 'CMD_QUALIFIER_SHORT not found');
const short = /'26': \{([^}]*)\}/.exec(block[1]);
assert.ok(short, "CMD_QUALIFIER_SHORT['26'] not found");
for (const key of ['0x07', '0x0B', '0x1A']) {
assert.ok(short[1].includes(key + ':'), 'CMD_QUALIFIER_SHORT 26 missing ' + key);
}
});
test('profile rows have a Clone action', () => {
assert.match(html, /onclick="profilerClone\(' \+ i \+ '\)"/);
assert.match(html, /t\('Clone'\)/);
@@ -62,12 +79,24 @@ test('header state indicator and profiler custom-files tab', () => {
assert.ok(!html.includes('data-pysim-sub="custom"'));
});
test('header status indicator has a compact ADM badge', () => {
assert.ok(html.includes('id="state-indicator-adm"'));
});
test('file manager has FID / Name sort pills', () => {
assert.match(html, /data-fs-sort="fid" onclick="pysimFsSetSort\('fid'\)"/);
assert.match(html, /data-fs-sort="name" onclick="pysimFsSetSort\('name'\)"/);
assert.ok(html.includes('pysim-fs-sort-pill'));
});
test('file manager keeps sort/probe controls above the scrolling tree', () => {
// The sort pills and the Probe all files button/status must sit outside
// the scrolling tree container so they stay visible while it scrolls.
assert.ok(html.indexOf('id="pysim-fs-probe-btn"') < html.indexOf('id="pysim-fs-tree"'));
assert.ok(html.indexOf('pysim-fs-sort-pill') < html.indexOf('id="pysim-fs-tree"'));
assert.match(html, /style="max-height:65vh"[^>]*>\s*<div id="pysim-fs-tree">/);
});
test('custom files form has add/save and cancel controls', () => {
assert.match(html, /id="pysim-cf-add-btn"[^>]*data-l10n="Add"/);
assert.match(html, /id="pysim-cf-cancel-btn"[^>]*class="hidden[^"]*"[^>]*data-l10n="Cancel"/);