net: network-state monitor and permanent-rejection FPLMN (v2.7.0)
server: - netsim: LOCI/PSLOCI dummy builders take a status; the EPSLOCI dummy is wiped to 0B F6 + FF per UICC_NAA.md C3; roaming_denied now emulates the permanent 'PLMN not allowed' rejection (C3a): status 010, EPSNSC dropped, EF.FPLMN append with TS 31.102 4.2.16 shift semantics and a home-PLMN guard (HPLMNwAcT/EHPLMN, IMSI fallback), optional 'Rejection: write FPLMN' toggle; write steps carry the logical key; SCENARIO_SERVICE map; insert_fplmn/fplmn_entries/parse_imsi helpers - netstate.py: cached per-session monitor state for the 12 network EFs, step-based patch, simulated service state, derived location with country/ operator (optional MCC/MNC list) and roaming class - server: monitor read at equip right after a readable ICCID (skipped otherwise), cleared on card removal; GET /api/net-state and POST /api/net-state-refresh; net-sim patches the cache from the written bytes and re-reads EF.IMSI; Location-status events set the service state and re-read EF.IMSI (multi-IMSI applets) frontend: - Phone tab: 'Network state' panel next to Network simulation with the simulated service badge (Undefined until simulated; normal/limited/no service + rejection marker), location/roaming line, compact per-file summaries with full-decode tooltips and a Refresh button; no card polling - EF decoders: EF.FPLMN (FFFFFF gaps are not terminators) and EF.EHPLMN - i18n EN/RU, help updated; SW cache simple-v203 tests: 295 Python / 453 frontend
This commit is contained in:
@@ -483,13 +483,16 @@
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Холодная загрузка</strong> — инвалидация EPSNSC (KSI 07, ключ стирается) и фиктивные location-файлы.</li>
|
||||
<li><strong>Подключение EPS / 2G</strong> — запись реального контекста EPS NAS (KSI, KASME, счётчики NAS, алгоритм) и реальных LOCI/PSLOCI/EPSLOCI (в 2G дополнительно реальные Kc/KcGPRS).</li>
|
||||
<li><strong>Потеря сервиса / ограниченный сервис / запрет роуминга</strong> — событие Location status (только если карта на него подписана), инвалидация EPSNSC (по желанию с сохранением старого KASME), фиктивные location-файлы (PLMN сохраняется, LAC <code class="font-mono text-sm">FFFE</code>, статус <code class="font-mono text-sm">01</code>/<code class="font-mono text-sm">02</code>) и инвалидация Kc.</li>
|
||||
<li><strong>Потеря сервиса / ограниченный сервис / запрет роуминга</strong> — событие Location status (только если карта на него подписана), инвалидация EPSNSC (по желанию с сохранением старого KASME), фиктивные location-файлы (PLMN сохраняется, LAC <code class="font-mono text-sm">FFFE</code>, статус <code class="font-mono text-sm">01</code>) и инвалидация Kc. <strong>Запрет роуминга</strong> эмулирует постоянный отказ «PLMN not allowed» (NAS cause #11): location-файлы получают статус <code class="font-mono text-sm">010</code> (EPSLOCI стирается до <code class="font-mono text-sm">0B F6</code> + статус), запрещённый VPLMN дописывается в <strong>EF.FPLMN</strong> по семантике сдвига из TS 31.102 §4.2.16 (домашняя сеть не записывается), ключевой контекст стирается.</li>
|
||||
<li><strong>Серия переподключений</strong> — реальная → невалидная запись EPSNSC подряд (число циклов и задержка настраиваются).</li>
|
||||
<li><strong>Принято SMS</strong> — инкремент счётчика EF.SMSstatus (чтение-изменение-запись) и, по желанию, перезапись location-файлов.</li>
|
||||
<li><strong>Перенастройка CB</strong> — запись списков CBMI/CBMIR или их очистка (все FF).</li>
|
||||
<li><strong>AUTHENTICATE</strong> — команда AUTHENTICATE (3G/EPS/5G, <code class="font-mono text-sm">00 88 00 81 22</code>) с заданными или случайными RAND/AUTN и показ ответа (успех <code class="font-mono text-sm">DB</code> или ошибка синхронизации <code class="font-mono text-sm">DC</code> с AUTS).</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3"><strong>Параметры</strong> (свёрнуты) задают оператора (поиск по мировому списку MCC/MNC с сервера плюс выбор случайного роуминг-оператора), LAC/Cell ID/TAC/RAC, необязательные идентификаторы (пусто = случайно: TMSI, GUTI, KSI, KASME, Kc, счётчики NAS, алгоритм, RAND/AUTN), переключатели сценария и число циклов/задержку. Журнал шагов показывает каждую запись, ENVELOPE и AUTHENTICATE с их SW. Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE — FPLMN и 5GS location-файлы никогда не записываются (ни один наблюдаемый телефон этого не делает); записи меняют карту и видны в последующих сравнениях снимков.</p>
|
||||
<p class="text-sm mb-3"><strong>Параметры</strong> (свёрнуты) задают оператора (поиск по мировому списку MCC/MNC с сервера плюс выбор случайного роуминг-оператора), LAC/Cell ID/TAC/RAC, необязательные идентификаторы (пусто = случайно: TMSI, GUTI, KSI, KASME, Kc, счётчики NAS, алгоритм, RAND/AUTN), переключатели сценария (включая <strong>«Отказ: записать FPLMN»</strong>) и число циклов/задержку. Журнал шагов показывает каждую запись, ENVELOPE и AUTHENTICATE с их SW. Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE — FPLMN записывается только сценарием постоянного отказа (TS 31.102 §4.2.16), а 5GS location-файлы не записываются; записи меняют карту и видны в последующих сравнениях снимков.</p>
|
||||
|
||||
<h4 id="network-state" class="font-medium mb-1">Монитор сетевого состояния</h4>
|
||||
<p class="text-sm mb-3">Рядом с кнопками симуляции компактная панель <strong>«Сетевое состояние»</strong> показывает, что сейчас хранит карта и что было сэмулировано последним. В заголовке — <strong>сэмулированное состояние сервиса</strong>: <em>Не определено</em>, пока его не задаст сценарий или событие Location status, затем <em>Обычный сервис</em> (зелёный), <em>Ограниченный сервис</em> (жёлтый) или <em>Нет сервиса</em> (красный), с красной пометкой <em>PLMN не разрешён</em>, если location-файлы или EF.FPLMN указывают на отказ регистрации — плюс текущее <strong>местоположение</strong>: PLMN, страна и оператор (из необязательного мирового списка MCC/MNC, если он загружен), LAI/RAI/TAI и <strong>класс роуминга</strong> (<em>Домашняя сеть</em>, если PLMN совпадает с HPLMN; <em>Эквивалентная домашней</em>, если он есть в EF.EHPLMN; иначе <em>Гостевая (роуминг)</em>). Ниже — по одной компактной строке на контролируемый файл (IMSI, EHPLMN, SPDI, HPLMNwAcT, LOCI, PSLOCI, EPSLOCI, EPSNSC, CBMI, CBMIR, SMSstatus, FPLMN) с декодированной сводкой и признаком последнего обновления (<code class="font-mono text-sm">init</code>, <code class="font-mono text-sm">write</code>, <code class="font-mono text-sm">read</code>, <code class="font-mono text-sm">refresh</code>); при наведении — все декодированные поля. Панель читает файлы один раз при подключении карты (только если ICCID читается), обновляет их на месте по записанным симулятором байтам, перечитывает EF.IMSI после каждого сценария и события Location status (мульти-IMSI апплеты) и никогда не опрашивает карту — кнопка <strong>«Обновить»</strong> перечитывает все файлы по требованию.</p>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
+5
-2
@@ -483,13 +483,16 @@
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Cold boot</strong> — invalidate EPSNSC (KSI 07, key wiped) and dummy the location files.</li>
|
||||
<li><strong>EPS attach / 2G attach</strong> — store a real EPS NAS context (KSI, KASME, NAS counts, algorithm) and write real LOCI/PSLOCI/EPSLOCI (2G also writes real Kc/KcGPRS).</li>
|
||||
<li><strong>Service lost / Limited service / Roaming denied</strong> — send the Location status event (only when the card subscribed to it), invalidate EPSNSC (optionally keeping the old KASME), dummy the location files (PLMN kept, LAC <code class="font-mono text-sm">FFFE</code>, status <code class="font-mono text-sm">01</code>/<code class="font-mono text-sm">02</code>) and invalidate Kc.</li>
|
||||
<li><strong>Service lost / Limited service / Roaming denied</strong> — send the Location status event (only when the card subscribed to it), invalidate EPSNSC (optionally keeping the old KASME), dummy the location files (PLMN kept, LAC <code class="font-mono text-sm">FFFE</code>, status <code class="font-mono text-sm">01</code>) and invalidate Kc. <strong>Roaming denied</strong> emulates a permanent “PLMN not allowed” rejection (NAS cause #11): the location files carry status <code class="font-mono text-sm">010</code> (EPSLOCI wiped to <code class="font-mono text-sm">0B F6</code> + status), the denied VPLMN is appended to <strong>EF.FPLMN</strong> with the shift-list semantics of TS 31.102 §4.2.16 (never the home PLMN) and the key context is dropped.</li>
|
||||
<li><strong>Churn</strong> — replay real → invalid EPSNSC records back-to-back (count and delay configurable).</li>
|
||||
<li><strong>SMS received</strong> — bump the EF.SMSstatus counter (read-modify-write) and optionally rewrite the location files.</li>
|
||||
<li><strong>CB reconfig</strong> — write the CBMI/CBMIR message-ID lists or clear them (all FF).</li>
|
||||
<li><strong>AUTHENTICATE</strong> — send AUTHENTICATE (3G/EPS/5G, <code class="font-mono text-sm">00 88 00 81 22</code>) with the given or random RAND/AUTN and show the response (success <code class="font-mono text-sm">DB</code> or synchronisation failure <code class="font-mono text-sm">DC</code> with AUTS).</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3"><strong>Parameters</strong> (collapsed) provide the operator (searchable worldwide MCC/MNC list served from the server, plus a random roaming picker), LAC/Cell ID/TAC/RAC, optional identity values (empty = random: TMSI, GUTI, KSI, KASME, Kc, NAS counts, algorithm, RAND/AUTN), the scenario toggles and the churn count/delay. The step log lists every write, ENVELOPE and AUTHENTICATE with its SW. Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent — FPLMN and 5GS location files are never written (no observed phone does); the writes change the card and are visible to later snapshot comparisons.</p>
|
||||
<p class="text-sm mb-3"><strong>Parameters</strong> (collapsed) provide the operator (searchable worldwide MCC/MNC list served from the server, plus a random roaming picker), LAC/Cell ID/TAC/RAC, optional identity values (empty = random: TMSI, GUTI, KSI, KASME, Kc, NAS counts, algorithm, RAND/AUTN), the scenario toggles (including <strong>Rejection: write FPLMN</strong>) and the churn count/delay. The step log lists every write, ENVELOPE and AUTHENTICATE with its SW. Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent — FPLMN is written only by the permanent rejection scenario (TS 31.102 §4.2.16) and the 5GS location files are never written; the writes change the card and are visible to later snapshot comparisons.</p>
|
||||
|
||||
<h4 id="network-state" class="font-medium mb-1">Network state monitor</h4>
|
||||
<p class="text-sm mb-3">Next to the simulation buttons a compact <strong>Network state</strong> panel shows what the card currently holds and what was last simulated. Its header carries the <strong>simulated service state</strong> — <em>Undefined</em> until a scenario or a Location status event sets it, then <em>Normal service</em> (green), <em>Limited service</em> (amber) or <em>No service</em> (red), with a red <em>PLMN not allowed</em> marker when the location files or EF.FPLMN show a rejection — plus the current <strong>location</strong>: PLMN, country and operator (from the optional worldwide MCC/MNC list when loaded), the LAI/RAI/TAI, and the <strong>roaming class</strong> (<em>Home</em> when the PLMN equals the HPLMN, <em>Home equivalent</em> when it is in EF.EHPLMN, otherwise <em>Guest</em>). Below it, one compact line per monitored file (IMSI, EHPLMN, SPDI, HPLMNwAcT, LOCI, PSLOCI, EPSLOCI, EPSNSC, CBMI, CBMIR, SMSstatus, FPLMN) with its decoded summary and how it was last updated (<code class="font-mono text-sm">init</code>, <code class="font-mono text-sm">write</code>, <code class="font-mono text-sm">read</code>, <code class="font-mono text-sm">refresh</code>); hover for the full decoded fields. The panel reads the files once at equip (only when the ICCID was readable), updates them in place from the bytes the simulator wrote, re-reads EF.IMSI after every scenario and Location-status event (multi-IMSI applets) and never polls the card — use <strong>Refresh</strong> to re-read all files on demand.</p>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
+199
-4
@@ -939,9 +939,10 @@
|
||||
<div id="pysim-event-list" class="text-sm font-mono text-gray-600 dark:text-slate-400"></div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<fieldset class="border border-gray-200 dark:border-slate-700 rounded p-3">
|
||||
<div class="flex flex-wrap items-start gap-4 mb-4">
|
||||
<fieldset class="flex-1 border border-gray-200 dark:border-slate-700 rounded p-3" style="min-width:30rem">
|
||||
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Network simulation">Network simulation</legend>
|
||||
<div class="text-xs text-gray-500 dark:text-slate-400 mb-2" data-l10n="Replays the write patterns a real phone performs on network-condition changes (EPS attach, service loss, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration, AUTHENTICATE). Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN and 5GS location files are never touched.">Replays the write patterns a real phone performs on network-condition changes (EPS attach, service loss, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration, AUTHENTICATE). Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN and 5GS location files are never touched.</div>
|
||||
<div class="text-xs text-gray-500 dark:text-slate-400 mb-2" data-l10n="Replays the write patterns a real phone performs on network-condition changes (EPS attach, service loss, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration, AUTHENTICATE). Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN is written only by the permanent "PLMN not allowed" rejection (TS 31.102 4.2.16), and the 5GS location files are never touched.">Replays the write patterns a real phone performs on network-condition changes (EPS attach, service loss, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration, AUTHENTICATE). Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN is written only by the permanent "PLMN not allowed" rejection (TS 31.102 4.2.16), and the 5GS location files are never touched.</div>
|
||||
<div class="flex flex-wrap gap-1.5 mb-2">
|
||||
<button data-needs="card" onclick="netSimRun('cold_boot')" class="px-2.5 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Cold boot">Cold boot</button>
|
||||
<button data-needs="card" onclick="netSimRun('attach_eps')" class="px-2.5 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="EPS attach">EPS attach</button>
|
||||
@@ -1017,6 +1018,7 @@
|
||||
<label class="flex items-center gap-1"><input type="checkbox" id="netsim-invalidate-epsnsc" checked class="rounded"> <span data-l10n="Invalidate EPSNSC">Invalidate EPSNSC</span></label>
|
||||
<label class="flex items-center gap-1"><input type="checkbox" id="netsim-keep-kasme" checked class="rounded"> <span data-l10n="Keep KASME (KSI 07 only)">Keep KASME (KSI 07 only)</span></label>
|
||||
<label class="flex items-center gap-1"><input type="checkbox" id="netsim-write-kc" checked class="rounded"> <span data-l10n="Write/invalidate Kc">Write/invalidate Kc</span></label>
|
||||
<label class="flex items-center gap-1"><input type="checkbox" id="netsim-write-fplmn" checked class="rounded"> <span data-l10n="Rejection: write FPLMN">Rejection: write FPLMN</span></label>
|
||||
<label class="flex items-center gap-1"><input type="checkbox" id="netsim-sms-location" checked class="rounded"> <span data-l10n="SMS: rewrite location too">SMS: rewrite location too</span></label>
|
||||
<label class="flex items-center gap-1"><input type="checkbox" id="netsim-cb-clear" class="rounded"> <span data-l10n="CB: clear lists instead of writing IDs">CB: clear lists instead of writing IDs</span></label>
|
||||
<label class="flex items-center gap-1"><span data-l10n="Churn count">Churn count</span><input id="netsim-churn-count" type="number" value="3" min="1" max="20" class="w-14 border border-gray-300 dark:border-slate-600 text-xs rounded px-1 py-0.5 dark:bg-slate-800"></label>
|
||||
@@ -1027,6 +1029,18 @@
|
||||
<div id="netsim-status" class="text-xs text-gray-400 dark:text-slate-500 mb-1"></div>
|
||||
<div id="netsim-log" class="text-xs font-mono text-gray-600 dark:text-slate-400 max-h-[30vh] overflow-auto"></div>
|
||||
</fieldset>
|
||||
<fieldset class="w-72 shrink-0 border border-gray-200 dark:border-slate-700 rounded p-3">
|
||||
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Network state">Network state</legend>
|
||||
<div class="flex items-center justify-between gap-2 mb-1">
|
||||
<span id="netstate-badge" class="text-sm font-semibold text-gray-400 dark:text-slate-500"></span>
|
||||
<button id="netstate-refresh-btn" data-needs="card" onclick="netStateRefresh()" class="px-2 py-0.5 text-xs rounded bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Refresh">Refresh</button>
|
||||
</div>
|
||||
<div id="netstate-location" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
|
||||
<div id="netstate-area" class="text-xs font-mono text-gray-400 dark:text-slate-500"></div>
|
||||
<div id="netstate-read" class="text-xs text-gray-400 dark:text-slate-500 mt-1"></div>
|
||||
<div id="netstate-body" class="mt-2 text-xs font-mono text-gray-600 dark:text-slate-400 max-h-[45vh] overflow-auto"></div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<fieldset class="border border-gray-200 dark:border-slate-700 rounded p-3 mt-4">
|
||||
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Fetched proactive commands:">Fetched proactive commands:</legend>
|
||||
<div id="pysim-proactive-log" class="text-sm font-mono text-gray-600 dark:text-slate-400"></div>
|
||||
@@ -1400,7 +1414,7 @@
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '2.6.2';
|
||||
const SIMPLE_VERSION = '2.7.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -1478,6 +1492,7 @@ function phoneSwitchSubtab(name) {
|
||||
stkCheckMenu();
|
||||
pysimEventsRender();
|
||||
pysimProactiveLogRender();
|
||||
netStateFetch();
|
||||
pysimPollStatusInit();
|
||||
tpRefresh();
|
||||
}
|
||||
@@ -4993,6 +5008,7 @@ function pysimSetServerAvailable(available) {
|
||||
pysimUpdateAdmIndicator(null);
|
||||
pysimUpdateIccidIndicator(null);
|
||||
pysimUpdatePresetIndicators(null);
|
||||
netStateRender(null);
|
||||
}
|
||||
pysimApplyAvailability();
|
||||
}
|
||||
@@ -7953,6 +7969,8 @@ async function pysimEventSend() {
|
||||
result.classList.remove('hidden');
|
||||
result.classList.add('text-amber-600');
|
||||
}
|
||||
if (resp.net_state) netStateRender(resp.net_state);
|
||||
else netStateFetch();
|
||||
} catch (e2) {
|
||||
err.textContent = 'Error: ' + e2.message;
|
||||
err.classList.remove('hidden');
|
||||
@@ -8031,6 +8049,7 @@ async function pysimResetCardData(refreshStatus) {
|
||||
pysimProactiveLogRender();
|
||||
pysimPliRender();
|
||||
if (refreshStatus) await pysimRefresh();
|
||||
if (isViewVisible('tab-phone')) netStateFetch();
|
||||
}
|
||||
|
||||
function pysimCardStateUpdate(status) {
|
||||
@@ -11907,6 +11926,19 @@ function efDecPlmnList(b) {
|
||||
return { plmns: plmns };
|
||||
}
|
||||
|
||||
// EF.FPLMN: 3-byte PLMN entries; an FFFFFF empty slot may appear in any
|
||||
// position and must not terminate the list (TS 31.102 4.2.16).
|
||||
function efDecFplmn(b) {
|
||||
const plmns = [];
|
||||
for (let i = 0; i + 2 < b.length; i += 3) {
|
||||
const chunk = b.subarray(i, i + 3);
|
||||
if (efAllFf(chunk)) continue;
|
||||
const p = efPlmn(chunk);
|
||||
if (p) plmns.push(p);
|
||||
}
|
||||
return { plmns: plmns };
|
||||
}
|
||||
|
||||
function efDecPlmnWact(b) {
|
||||
const plmns = [];
|
||||
for (let i = 0; i + 4 < b.length; i += 5) {
|
||||
@@ -12254,6 +12286,7 @@ const EF_DECODERS = [
|
||||
{ name: 'EF.PLMNwAcT', fid: '6f60', fn: efDecPlmnWact },
|
||||
{ name: 'EF.OPLMNwAcT', fid: '6f61', fn: efDecPlmnWact },
|
||||
{ name: 'EF.HPLMNwAcT', fid: '6f62', fn: efDecPlmnWact },
|
||||
{ name: 'EF.EHPLMN', fid: '6fd9', fn: efDecPlmnList },
|
||||
{ name: 'EF.OPL', fid: '6fc6', fn: efDecOpl },
|
||||
{ name: 'EF.SPDI', fid: '6fcd', fn: efDecSpdi },
|
||||
{ name: 'EF.PNN', fid: '6fc5', fn: efDecPnn },
|
||||
@@ -12261,6 +12294,7 @@ const EF_DECODERS = [
|
||||
{ name: 'EF.PSLOCI', fid: '6f73', fn: efDecPsLoci },
|
||||
{ name: 'EF.EPSLOCI', fid: '6fe3', fn: efDecEpsLoci },
|
||||
{ name: 'EF.EPSNSC', fid: '6fe4', fn: efDecEpsNsc },
|
||||
{ name: 'EF.FPLMN', fid: '6f7b', fn: efDecFplmn },
|
||||
{ name: 'EF.LOCIGPRS', fid: '6f53', fn: efDecPsLoci },
|
||||
{ name: 'EF.Kc', fid: '6f20', fn: efDecKc },
|
||||
{ name: 'EF.KcGPRS', fid: '6f52', fn: efDecKc },
|
||||
@@ -12477,6 +12511,7 @@ function netSimParams() {
|
||||
write_kc: netSimChk('netsim-write-kc'),
|
||||
sms_location: netSimChk('netsim-sms-location'),
|
||||
cb_clear: netSimChk('netsim-cb-clear'),
|
||||
write_fplmn: netSimChk('netsim-write-fplmn'),
|
||||
};
|
||||
const map = {
|
||||
mcc: 'netsim-mcc', mnc: 'netsim-mnc', lac: 'netsim-lac', cell_id: 'netsim-cell',
|
||||
@@ -12492,6 +12527,151 @@ function netSimParams() {
|
||||
return p;
|
||||
}
|
||||
|
||||
// ===== Network state monitor =====
|
||||
// Cached server-side view of the network-related EFs (read at equip, patched
|
||||
// from net-sim writes, refreshed on demand) plus the simulated service state
|
||||
// and the derived location/roaming view. No card polling: the panel fetches
|
||||
// the cached state when the Phone tab opens and after net-sim/event actions.
|
||||
const NET_STATE_FILES = [
|
||||
['imsi', 'EF.IMSI'], ['ehplmn', 'EF.EHPLMN'], ['spdi', 'EF.SPDI'],
|
||||
['hplmnwact', 'EF.HPLMNwAcT'], ['loci', 'EF.LOCI'], ['psloci', 'EF.PSLOCI'],
|
||||
['epsloci', 'EF.EPSLOCI'], ['epsnsc', 'EF.EPSNSC'], ['cbmi', 'EF.CBMI'],
|
||||
['cbmir', 'EF.CBMIR'], ['smsstatus', 'EF.SMSS'], ['fplmn', 'EF.FPLMN'],
|
||||
];
|
||||
const NET_STATE_STATUS = {
|
||||
loci: { 0: 'updated', 1: 'not updated', 2: 'PLMN not allowed', 3: 'LA not allowed' },
|
||||
psloci: { 0: 'updated', 1: 'not updated', 2: 'PLMN not allowed' },
|
||||
epsloci: { 0: 'updated', 1: 'not updated', 2: 'roaming not allowed' },
|
||||
};
|
||||
const NET_STATE_AREA = {
|
||||
loci: ['LAI', 'location_update_status'],
|
||||
psloci: ['RAI', 'update_status'],
|
||||
epsloci: ['TAI', 'eps_update_status'],
|
||||
};
|
||||
const NET_STATE_ROAMING = { home: 'Home', equivalent: 'Home equivalent', guest: 'Guest (roaming)' };
|
||||
const NET_STATE_SERVICE = {
|
||||
normal: ['text-emerald-600 dark:text-emerald-400', '● ', 'Normal service'],
|
||||
limited: ['text-amber-600 dark:text-amber-500', '⚠ ', 'Limited service'],
|
||||
none: ['text-red-500', '⛔ ', 'No service'],
|
||||
};
|
||||
const NET_STATE_SOURCE = { init: 'init', write: 'write', read: 'read', refresh: 'refresh' };
|
||||
|
||||
function netStateFetch() {
|
||||
return pysimFetch('/api/net-state').then(res => {
|
||||
netStateRender(res && res.available ? res.state : null);
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function netStateRefresh() {
|
||||
return pysimFetch('/api/net-state-refresh', {}).then(res => {
|
||||
netStateRender(res ? res.state : null);
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function netStateStatusWord(key, raw) {
|
||||
const v = parseInt(String(raw || '').replace(/^0x/i, ''), 16);
|
||||
const word = (NET_STATE_STATUS[key] || {})[v];
|
||||
return word ? t(word) : (raw || '');
|
||||
}
|
||||
|
||||
function netStateFlattenTitle(decoded) {
|
||||
const data = decoded && decoded.data ? decoded.data : {};
|
||||
return efFlatten(data).map(r => (r[0] ? r[0] + ': ' : '') + r[1]).join(' · ');
|
||||
}
|
||||
|
||||
function netStatePlmnList(d, withTech) {
|
||||
return (d.plmns || []).map(p => p.mcc + '-' + p.mnc +
|
||||
(withTech && p.access_tech ? ' (' + p.access_tech + ')' : '')).join(', ');
|
||||
}
|
||||
|
||||
function netStateSummary(key, entry) {
|
||||
if (!entry || !entry.present) {
|
||||
return { text: '—', title: t('not present'), cls: 'text-gray-400' };
|
||||
}
|
||||
const name = (NET_STATE_FILES.find(f => f[0] === key) || [])[1];
|
||||
const content = entry.kind === 'record'
|
||||
? { kind: 'record', records: entry.records || [] }
|
||||
: { kind: 'transparent', data: entry.data || '' };
|
||||
const dec = name ? efDecodeFile(name, null, content) : null;
|
||||
if (!dec) {
|
||||
const raw = entry.kind === 'record' ? '' : (entry.data || '');
|
||||
return raw ? { text: raw, title: raw, cls: '' }
|
||||
: { text: '—', title: t('empty'), cls: 'text-gray-400' };
|
||||
}
|
||||
const d = dec.data;
|
||||
if (!d || d.empty) return { text: '—', title: t('empty'), cls: 'text-gray-400' };
|
||||
let text = '';
|
||||
if (key === 'imsi') {
|
||||
text = d.imsi || '';
|
||||
} else if (key === 'ehplmn' || key === 'spdi' || key === 'fplmn') {
|
||||
text = netStatePlmnList(d);
|
||||
} else if (key === 'hplmnwact') {
|
||||
text = netStatePlmnList(d, true);
|
||||
} else if (NET_STATE_AREA[key]) {
|
||||
const [area, statusKey] = NET_STATE_AREA[key];
|
||||
const plmn = (d.mcc && d.mnc) ? d.mcc + '-' + d.mnc : null;
|
||||
const lac = d.lac ? String(d.lac).replace(/^0x/i, '') : '';
|
||||
const status = d[statusKey] !== undefined ? netStateStatusWord(key, d[statusKey]) : '';
|
||||
text = (plmn ? area + ' ' + plmn + (lac ? '/' + lac : '') : area + ' —') +
|
||||
(status ? ' · ' + status : '');
|
||||
} else if (key === 'epsnsc') {
|
||||
const parts = [];
|
||||
if (d.ksi_asme !== undefined) parts.push('KSI ' + d.ksi_asme);
|
||||
if (d.uplink_nas_count !== undefined || d.downlink_nas_count !== undefined) {
|
||||
parts.push('UL/DL ' + (d.uplink_nas_count || 0) + '/' + (d.downlink_nas_count || 0));
|
||||
}
|
||||
if (d.nas_algorithms) parts.push('algo ' + d.nas_algorithms);
|
||||
text = parts.join(' · ');
|
||||
} else if (key === 'cbmi') {
|
||||
text = (d.message_ids || []).map(v => v.toString(16).toUpperCase().padStart(4, '0')).join(', ');
|
||||
} else if (key === 'cbmir') {
|
||||
text = (d.ranges || []).map(r => r[0] + '–' + r[1]).join(', ');
|
||||
} else if (key === 'smsstatus') {
|
||||
text = entry.data || '';
|
||||
}
|
||||
return { text: text || '—', title: netStateFlattenTitle(dec) || (entry.data || ''), cls: '' };
|
||||
}
|
||||
|
||||
function netStateRender(state) {
|
||||
const badge = document.getElementById('netstate-badge');
|
||||
if (!badge) return;
|
||||
const svc = (state && state.service) || {};
|
||||
const known = NET_STATE_SERVICE[svc.state];
|
||||
badge.className = 'text-sm font-semibold ' +
|
||||
(known ? known[0] : 'text-gray-400 dark:text-slate-500');
|
||||
badge.textContent = known ? known[1] + t(known[2]) : '○ ' + t('Undefined');
|
||||
const loc = state && state.network ? state.network.location : null;
|
||||
const locEl = document.getElementById('netstate-location');
|
||||
if (loc) {
|
||||
let h = '<span class="font-mono">' + esc(loc.mcc + '-' + loc.mnc) + '</span>';
|
||||
if (loc.country) h += ' · ' + esc(loc.country);
|
||||
if (loc.operator) h += ' · ' + esc(loc.operator);
|
||||
if (loc.roaming) h += ' · ' + esc(t(NET_STATE_ROAMING[loc.roaming] || loc.roaming));
|
||||
if (loc.rejected) h += ' <span class="text-red-500">⛔ ' + esc(t('PLMN not allowed')) + '</span>';
|
||||
locEl.innerHTML = h;
|
||||
} else {
|
||||
locEl.innerHTML = '<span class="text-gray-400">—</span>';
|
||||
}
|
||||
document.getElementById('netstate-area').textContent = loc ? (loc.area + ' ' + loc.lac) : '';
|
||||
document.getElementById('netstate-read').textContent =
|
||||
state && state.read_at ? new Date(state.read_at * 1000).toLocaleTimeString() : '';
|
||||
const files = (state && state.files) || {};
|
||||
let html = '';
|
||||
for (const [key, name] of NET_STATE_FILES) {
|
||||
const entry = files[key];
|
||||
const s = netStateSummary(key, entry);
|
||||
const src = entry && entry.present
|
||||
? (NET_STATE_SOURCE[entry.source] || entry.source || '') : '';
|
||||
html += '<div class="flex items-baseline gap-1" title="' + esc(s.title || '') + '">' +
|
||||
'<span class="w-24 shrink-0 text-gray-500 dark:text-slate-400">' + esc(name) + '</span>' +
|
||||
'<span class="flex-1 min-w-0 truncate ' + (s.cls || '') + '">' + esc(s.text) + '</span>' +
|
||||
'<span class="text-gray-300 dark:text-slate-600">' + esc(src) + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
document.getElementById('netstate-body').innerHTML = html ||
|
||||
'<span class="text-gray-400">' + esc(t('No data')) + '</span>';
|
||||
}
|
||||
|
||||
async function netSimRun(scenario) {
|
||||
const statusEl = document.getElementById('netsim-status');
|
||||
statusEl.textContent = t('Running') + '...';
|
||||
@@ -12509,6 +12689,7 @@ async function netSimRun(scenario) {
|
||||
statusEl.className = 'text-xs text-red-500 mb-1';
|
||||
}
|
||||
pysimEventsRender();
|
||||
if (res.net_state) netStateRender(res.net_state);
|
||||
} catch (e) {
|
||||
statusEl.textContent = t('Error') + ': ' + e.message;
|
||||
statusEl.className = 'text-xs text-red-500 mb-1';
|
||||
@@ -12848,7 +13029,7 @@ const LANG_RU = {
|
||||
'No events configured.': 'Нет настроенных событий.',
|
||||
'Fetched proactive commands:': 'Извлечённые проактивные команды:',
|
||||
'Network simulation': 'Симуляция сети',
|
||||
'Replays the write patterns a real phone performs on network-condition changes (EPS attach, service loss, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration, AUTHENTICATE). Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN and 5GS location files are never touched.': 'Воспроизводит шаблоны записи реального телефона при смене сетевых условий (подключение EPS, потеря сервиса, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast, AUTHENTICATE). Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE; FPLMN и 5GS location-файлы не затрагиваются.',
|
||||
'Replays the write patterns a real phone performs on network-condition changes (EPS attach, service loss, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration, AUTHENTICATE). Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN is written only by the permanent "PLMN not allowed" rejection (TS 31.102 4.2.16), and the 5GS location files are never touched.': 'Воспроизводит шаблоны записи реального телефона при смене сетевых условий (подключение EPS, потеря сервиса, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast, AUTHENTICATE). Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE; FPLMN записывается только при постоянном отказе «PLMN not allowed» (TS 31.102 4.2.16), а 5GS location-файлы не затрагиваются.',
|
||||
'Cold boot': 'Холодная загрузка',
|
||||
'EPS attach': 'Подключение EPS',
|
||||
'2G attach': 'Подключение 2G',
|
||||
@@ -13126,6 +13307,20 @@ const LANG_RU = {
|
||||
'No ADM key in the card preset — not verified': 'Ключа ADM в предустановке нет — не подтверждён',
|
||||
'SCP80 preset complete': 'Предустановка SCP80 заполнена',
|
||||
'SCP81 preset complete': 'Предустановка SCP81 заполнена',
|
||||
'Network state': 'Сетевое состояние',
|
||||
'Undefined': 'Не определено',
|
||||
'PLMN not allowed': 'PLMN не разрешён',
|
||||
'roaming not allowed': 'роуминг не разрешён',
|
||||
'LA not allowed': 'LA не разрешён',
|
||||
'updated': 'обновлён',
|
||||
'not updated': 'не обновлён',
|
||||
'not present': 'отсутствует',
|
||||
'empty': 'пусто',
|
||||
'No data': 'Нет данных',
|
||||
'Home': 'Домашняя сеть',
|
||||
'Home equivalent': 'Эквивалентная домашней',
|
||||
'Guest (roaming)': 'Гостевая (роуминг)',
|
||||
'Rejection: write FPLMN': 'Отказ: записать FPLMN',
|
||||
'TERMINAL PROFILE': 'TERMINAL PROFILE',
|
||||
'Preset (device model)': 'Пресет (модель устройства)',
|
||||
'Profile (hex)': 'Профиль (hex)',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v202';
|
||||
const CACHE = 'simple-v203';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -90,6 +90,7 @@ function setup() {
|
||||
globalThis.pysimSetConnected = v => calls.connected.push(v);
|
||||
globalThis.pysimResetCardData = refresh => calls.resets.push(refresh);
|
||||
globalThis.pysimApplyAvailability = () => {};
|
||||
globalThis.netStateRender = () => {};
|
||||
globalThis.isViewVisible = () => true;
|
||||
globalThis.pysimProactiveLogRender = () => { calls.proactive++; };
|
||||
globalThis.cardsAutoSelectByIccid = iccid => { calls.autoIccid.push(iccid); return -1; };
|
||||
|
||||
@@ -24,7 +24,7 @@ function extractFunc(src, name) {
|
||||
const FNS = [
|
||||
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efTlv', 'efUcs2Be',
|
||||
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
|
||||
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecPlmnList', 'efDecPlmnWact',
|
||||
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
|
||||
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
|
||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||
'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
|
||||
@@ -84,6 +84,12 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: '0x01' });
|
||||
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
|
||||
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
|
||||
// EF.FPLMN: a FFFFFF gap in any position is skipped, not a terminator
|
||||
assert.deepStrictEqual(efDecFplmn(efBytes('22F860FFFFFF62F210')),
|
||||
{ plmns: [{ mcc: '228', mnc: '06' }, { mcc: '262', mnc: '01' }] });
|
||||
assert.deepStrictEqual(efDecFplmn(efBytes('FFFFFFFF')), { plmns: [] });
|
||||
assert.strictEqual(efFindDecoder('EF.EHPLMN', null).fn, efDecPlmnList);
|
||||
assert.strictEqual(efFindDecoder('EF.FPLMN', null).fn, efDecFplmn);
|
||||
assert.deepStrictEqual(efDecCbmi(efBytes('0010FFFF0020')), { message_ids: [16, 32] });
|
||||
assert.deepStrictEqual(efDecCbmir(efBytes('0000FFFEFFFF')), { ranges: [['0000', 'FFFE']] });
|
||||
assert.deepStrictEqual(efDecPhase(efBytes('03')), { phase: 'phase 2 and higher' });
|
||||
|
||||
@@ -70,6 +70,16 @@ test('card-dependent profiler buttons show the ICCID and need a readable one', (
|
||||
assert.match(html, /id="snapshot-new-btn"[\s\S]*?<span class="profiler-iccid-label" data-iccid-sep=": "><\/span><\/button>/);
|
||||
});
|
||||
|
||||
test('network state monitor panel lives next to the network simulation', () => {
|
||||
assert.match(html, /data-l10n="Network state"/);
|
||||
assert.match(html, /id="netstate-badge"/);
|
||||
assert.match(html, /id="netstate-refresh-btn" data-needs="card"/);
|
||||
assert.match(html, /id="netstate-body"/);
|
||||
assert.match(html, /id="netsim-write-fplmn"/);
|
||||
// the monitor is rendered from the cached server state (no file polling)
|
||||
assert.ok(html.includes("netStateFetch()"));
|
||||
});
|
||||
|
||||
test('labelled containers use fieldsets with embedded legends', () => {
|
||||
const legendCls = '<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400"';
|
||||
for (const label of ['STK menu', 'STATUS and Polling', 'TERMINAL PROFILE',
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
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);
|
||||
}
|
||||
|
||||
let code = '';
|
||||
for (const fn of ['netStateStatusWord', 'netStateFlattenTitle', 'netStatePlmnList',
|
||||
'netStateSummary', 'netStateRender']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
const CONSTS = {
|
||||
NET_STATE_FILES: ['[', '];'],
|
||||
NET_STATE_STATUS: ['{', '};'],
|
||||
NET_STATE_AREA: ['{', '};'],
|
||||
NET_STATE_ROAMING: ['{', '};'],
|
||||
NET_STATE_SERVICE: ['{', '};'],
|
||||
NET_STATE_SOURCE: ['{', '};'],
|
||||
};
|
||||
for (const [name, [open, close]] of Object.entries(CONSTS)) {
|
||||
const re = new RegExp('const ' + name + ' = \\' + open + '[\\s\\S]*?\\' + close);
|
||||
const m = re.exec(html);
|
||||
if (!m) throw new Error('const ' + name + ' not found');
|
||||
code += m[0].replace(/^const /, 'var ') + '\n';
|
||||
}
|
||||
code += 'globalThis.t = s => s;\n';
|
||||
code += 'globalThis.esc = s => String(s);\n';
|
||||
code += 'globalThis.efFlatten = d => Object.entries(d || {}).map(([k, v]) => [k, String(v)]);\n';
|
||||
eval(code);
|
||||
|
||||
function setup() {
|
||||
const els = {};
|
||||
for (const id of ['netstate-badge', 'netstate-location', 'netstate-area',
|
||||
'netstate-read', 'netstate-body']) {
|
||||
els[id] = { className: '', textContent: '', innerHTML: '' };
|
||||
}
|
||||
globalThis.document = { getElementById: id => els[id] || null };
|
||||
return els;
|
||||
}
|
||||
|
||||
function decoder(map) {
|
||||
globalThis.efDecodeFile = (name, fid, content) =>
|
||||
map[name] ? { data: map[name] } : null;
|
||||
}
|
||||
|
||||
test('netStateSummary renders decoded file contents compactly', () => {
|
||||
setup();
|
||||
decoder({
|
||||
'EF.LOCI': { tmsi: '3E905D6B', mcc: '262', mnc: '01', lac: '0x6CD7',
|
||||
tmsi_time: '0xFF', location_update_status: '0x00' },
|
||||
'EF.FPLMN': { plmns: [{ mcc: '262', mnc: '01' }, { mcc: '246', mnc: '81' }] },
|
||||
'EF.HPLMNwAcT': { plmns: [{ mcc: '262', mnc: '01', access_tech: 'LTE' }] },
|
||||
});
|
||||
const loci = netStateSummary('loci', { present: true, kind: 'transparent', data: 'AA' });
|
||||
assert.strictEqual(loci.text, 'LAI 262-01/6CD7 · updated');
|
||||
assert.ok(loci.title.includes('3E905D6B'));
|
||||
const fplmn = netStateSummary('fplmn', { present: true, kind: 'transparent', data: 'AA' });
|
||||
assert.strictEqual(fplmn.text, '262-01, 246-81');
|
||||
const hplmn = netStateSummary('hplmnwact', { present: true, kind: 'transparent', data: 'AA' });
|
||||
assert.strictEqual(hplmn.text, '262-01 (LTE)');
|
||||
const absent = netStateSummary('loci', { present: false });
|
||||
assert.strictEqual(absent.text, '—');
|
||||
assert.strictEqual(absent.title, 'not present');
|
||||
});
|
||||
|
||||
test('netStateSummary marks rejection statuses', () => {
|
||||
setup();
|
||||
decoder({
|
||||
'EF.LOCI': { mcc: '246', mnc: '81', lac: '0xFFFE', location_update_status: '0x02' },
|
||||
});
|
||||
const loci = netStateSummary('loci', { present: true, kind: 'transparent', data: 'AA' });
|
||||
assert.strictEqual(loci.text, 'LAI 246-81/FFFE · PLMN not allowed');
|
||||
});
|
||||
|
||||
test('netStateRender paints the service badge, location and rows', () => {
|
||||
const els = setup();
|
||||
decoder({ 'EF.IMSI': { imsi: '262011234567890' } });
|
||||
netStateRender({
|
||||
service: { state: 'limited', source: 'net-sim:roaming_denied' },
|
||||
read_at: 1700000000,
|
||||
files: { imsi: { present: true, kind: 'transparent', data: 'AA', source: 'write' } },
|
||||
network: { location: { mcc: '262', mnc: '01', country: 'Germany',
|
||||
operator: 'Telekom', roaming: 'guest', rejected: true,
|
||||
area: 'LAI', lac: '6CD7', source: 'write' } },
|
||||
});
|
||||
assert.strictEqual(els['netstate-badge'].textContent, '⚠ Limited service');
|
||||
assert.ok(els['netstate-badge'].className.includes('text-amber-600'));
|
||||
assert.ok(els['netstate-location'].innerHTML.includes('262-01'));
|
||||
assert.ok(els['netstate-location'].innerHTML.includes('Germany'));
|
||||
assert.ok(els['netstate-location'].innerHTML.includes('Guest (roaming)'));
|
||||
assert.ok(els['netstate-location'].innerHTML.includes('PLMN not allowed'));
|
||||
assert.strictEqual(els['netstate-area'].textContent, 'LAI 6CD7');
|
||||
assert.ok(els['netstate-body'].innerHTML.includes('EF.IMSI'));
|
||||
assert.ok(els['netstate-body'].innerHTML.includes('262011234567890'));
|
||||
assert.ok(els['netstate-body'].innerHTML.includes('>write<'));
|
||||
});
|
||||
|
||||
test('netStateRender shows Undefined until something is simulated', () => {
|
||||
const els = setup();
|
||||
decoder({});
|
||||
netStateRender(null);
|
||||
assert.strictEqual(els['netstate-badge'].textContent, '○ Undefined');
|
||||
assert.ok(els['netstate-badge'].className.includes('text-gray-400'));
|
||||
assert.ok(els['netstate-body'].innerHTML.includes('EF.LOCI'));
|
||||
assert.ok(els['netstate-body'].innerHTML.includes('—'));
|
||||
});
|
||||
|
||||
test('netStateRender shows the green normal-service badge', () => {
|
||||
const els = setup();
|
||||
decoder({});
|
||||
netStateRender({ service: { state: 'normal' }, files: {} });
|
||||
assert.strictEqual(els['netstate-badge'].textContent, '● Normal service');
|
||||
assert.ok(els['netstate-badge'].className.includes('text-emerald-600'));
|
||||
});
|
||||
@@ -28,7 +28,8 @@ const code = extractFunc(html, 'phoneSwitchSubtab') + '\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' +
|
||||
'globalThis.tpRefresh = () => { globalThis._tp = (globalThis._tp || 0) + 1; };\n';
|
||||
'globalThis.tpRefresh = () => { globalThis._tp = (globalThis._tp || 0) + 1; };\n' +
|
||||
'globalThis.netStateFetch = () => { globalThis._netstate = (globalThis._netstate || 0) + 1; };\n';
|
||||
eval(code);
|
||||
|
||||
function makeClassList() {
|
||||
|
||||
Reference in New Issue
Block a user