ui: decode and abbreviate long PLMN lists in the network monitor (v2.7.16)

EF.HPLMNwAcT fell back to raw hex: efFindDecoder uppercases the requested
name but compared it against the mixed-case registry names
('EF.HPLMNwAcT'), and the monitor passes no FID, so the lookup failed.
Name matching is now case-insensitive, which also fixes EF.PLMNwAcT,
EF.OPLMNwAcT, EF.PLMNsel, EF.KcGPRS and EF.LOCIGPRS for name-only callers.

The decoded HPLMNwAcT list can still be long (one entry per PLMN/AcT
combination), so netStatePlmnList keeps the first three networks and
appends a '… +N' counter; the row tooltip keeps the full decoded list.

- tests: registry case-insensitivity, netStatePlmnList limit, HPLMNwAcT
  summary abbreviation; help EN/RU.
This commit is contained in:
2026-09-21 00:18:36 +03:00
parent 4730e81ec2
commit ef3726441b
8 changed files with 43 additions and 12 deletions
+1 -1
View File
@@ -492,7 +492,7 @@
<p class="text-sm mb-3"><strong>Параметры</strong> (свёрнуты) задают оператора (поиск по мировому списку MCC/MNC с сервера плюс выбор случайного роуминг-оператора), LAC/Cell ID/TAC/RAC, необязательные идентификаторы (пусто = случайно: TMSI, GUTI, KSI, KASME, Kc, счётчики NAS, алгоритм, RAND/AUTN), переключатели сценария (включая <strong>&laquo;Отказ: записать FPLMN&raquo;</strong>) и число циклов/задержку. Журнал шагов показывает каждую запись, ENVELOPE и AUTHENTICATE с их SW. Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE — FPLMN записывается только сценарием постоянного отказа (TS 31.102 §4.2.16), а 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>&laquo;Отказ: записать FPLMN&raquo;</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> <h4 id="network-state" class="font-medium mb-1">Монитор сетевого состояния</h4>
<p class="text-sm mb-3">Рядом с кнопками симуляции компактная панель <strong>&laquo;Сетевое состояние&raquo;</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>&laquo;Обновить&raquo;</strong> перечитывает все файлы по требованию.</p> <p class="text-sm mb-3">Рядом с кнопками симуляции компактная панель <strong>&laquo;Сетевое состояние&raquo;</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>); при наведении — все декодированные поля; длинные списки PLMN (например, EF.HPLMNwAcT) сокращаются до первых сетей с пометкой <code class="font-mono text-sm">… +N</code>. Панель читает файлы один раз при подключении карты (только если ICCID читается), обновляет их на месте по записанным симулятором байтам, перечитывает EF.IMSI после каждого сценария и события Location status (мульти-IMSI апплеты) и никогда не опрашивает карту — кнопка <strong>&laquo;Обновить&raquo;</strong> перечитывает все файлы по требованию.</p>
</section> </section>
+1 -1
View File
@@ -492,7 +492,7 @@
<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> <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> <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> <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 — long PLMN lists (e.g. EF.HPLMNwAcT) are abbreviated to the first few networks plus a <code class="font-mono text-sm">… +N</code> counter. 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> </section>
+11 -7
View File
@@ -1413,7 +1413,7 @@
// ===== Version ===== // ===== Version =====
// Single source of truth for the PWA version: shown in the header and used // Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect(). // by the server version check in pysimConnect().
const SIMPLE_VERSION = '2.7.15'; const SIMPLE_VERSION = '2.7.16';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching ===== // ===== Tab switching =====
@@ -12522,7 +12522,7 @@ const EF_DECODERS = [
function efFindDecoder(name, fid) { function efFindDecoder(name, fid) {
let n = (name || '').toUpperCase(); let n = (name || '').toUpperCase();
if (n.includes('/')) n = n.split('/').pop(); if (n.includes('/')) n = n.split('/').pop();
for (const d of EF_DECODERS) if (d.name === n) return d; for (const d of EF_DECODERS) if (d.name.toUpperCase() === n) return d;
const f = (fid || '').toLowerCase(); const f = (fid || '').toLowerCase();
if (f) for (const d of EF_DECODERS) if (d.fid === f) return d; if (f) for (const d of EF_DECODERS) if (d.fid === f) return d;
return null; return null;
@@ -12772,9 +12772,13 @@ function netStateFlattenTitle(decoded) {
return efFlatten(data).map(r => (r[0] ? r[0] + ': ' : '') + r[1]).join(' · '); return efFlatten(data).map(r => (r[0] ? r[0] + ': ' : '') + r[1]).join(' · ');
} }
function netStatePlmnList(d, withTech) { // Compact PLMN list for the monitor rows: `max` keeps the row short (the
return (d.plmns || []).map(p => p.mcc + '-' + p.mnc + // full decoded list stays in the row's title tooltip).
(withTech && p.access_tech ? ' (' + p.access_tech + ')' : '')).join(', '); function netStatePlmnList(d, withTech, max) {
const list = (d.plmns || []).map(p => p.mcc + '-' + p.mnc +
(withTech && p.access_tech ? ' (' + p.access_tech + ')' : ''));
if (max && list.length > max) return list.slice(0, max).join(', ') + ' … +' + (list.length - max);
return list.join(', ');
} }
function netStateSummary(key, entry) { function netStateSummary(key, entry) {
@@ -12797,9 +12801,9 @@ function netStateSummary(key, entry) {
if (key === 'imsi') { if (key === 'imsi') {
text = d.imsi || ''; text = d.imsi || '';
} else if (key === 'ehplmn' || key === 'spdi' || key === 'fplmn') { } else if (key === 'ehplmn' || key === 'spdi' || key === 'fplmn') {
text = netStatePlmnList(d); text = netStatePlmnList(d, false, 3);
} else if (key === 'hplmnwact') { } else if (key === 'hplmnwact') {
text = netStatePlmnList(d, true); text = netStatePlmnList(d, true, 3);
} else if (NET_STATE_AREA[key]) { } else if (NET_STATE_AREA[key]) {
const [area, statusKey] = NET_STATE_AREA[key]; const [area, statusKey] = NET_STATE_AREA[key];
const plmn = (d.mcc && d.mnc) ? d.mcc + '-' + d.mnc : null; const plmn = (d.mcc && d.mnc) ? d.mcc + '-' + d.mnc : null;
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v218'; const CACHE = 'simple-v219';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+3
View File
@@ -159,6 +159,9 @@ test('decoder registry resolves by name first, then FID; unknown returns null',
assert.strictEqual(efFindDecoder('ADF.USIM/EF.LOCI', null).name, 'EF.LOCI'); assert.strictEqual(efFindDecoder('ADF.USIM/EF.LOCI', null).name, 'EF.LOCI');
assert.strictEqual(efFindDecoder('EF.SUME', null).fid, '6f54'); assert.strictEqual(efFindDecoder('EF.SUME', null).fid, '6f54');
assert.strictEqual(efFindDecoder('EF.NOPE', 'ABCD'), null); assert.strictEqual(efFindDecoder('EF.NOPE', 'ABCD'), null);
// mixed-case registry names resolve case-insensitively without a FID
assert.strictEqual(efFindDecoder('EF.HPLMNwAcT', null).fid, '6f62');
assert.strictEqual(efFindDecoder('ef.plmnwact', null).name, 'EF.PLMNwAcT');
assert.strictEqual(efFidFromPath('ADF.USIM/6F07'), '6F07'); assert.strictEqual(efFidFromPath('ADF.USIM/6F07'), '6F07');
assert.strictEqual(efFidFromPath('MF/7F10/6F3A'), '6F3A'); assert.strictEqual(efFidFromPath('MF/7F10/6F3A'), '6F3A');
assert.strictEqual(efFidFromPath('ADF.USIM'), null); assert.strictEqual(efFidFromPath('ADF.USIM'), null);
+24
View File
@@ -89,6 +89,30 @@ test('netStateSummary marks rejection statuses', () => {
assert.strictEqual(loci.text, 'LAI 246-81/FFFE · PLMN not allowed'); assert.strictEqual(loci.text, 'LAI 246-81/FFFE · PLMN not allowed');
}); });
test('netStatePlmnList limits long lists with a count', () => {
const d = { plmns: [
{ mcc: '262', mnc: '01' }, { mcc: '246', mnc: '81' },
{ mcc: '204', mnc: '04' }, { mcc: '234', mnc: '15' },
] };
assert.strictEqual(netStatePlmnList(d, false, 3), '262-01, 246-81, 204-04 … +1');
assert.strictEqual(netStatePlmnList(d, false), '262-01, 246-81, 204-04, 234-15');
});
test('netStateSummary abbreviates a long HPLMNwAcT list', () => {
setup();
decoder({
'EF.HPLMNwAcT': { plmns: [
{ mcc: '250', mnc: '99', access_tech: 'LTE' },
{ mcc: '250', mnc: '32', access_tech: 'LTE' },
{ mcc: '250', mnc: '54', access_tech: 'LTE' },
{ mcc: '262', mnc: '01', access_tech: 'GSM' },
{ mcc: '246', mnc: '81', access_tech: 'GSM' },
] },
});
const s = netStateSummary('hplmnwact', { present: true, kind: 'transparent', data: 'AA' });
assert.strictEqual(s.text, '250-99 (LTE), 250-32 (LTE), 250-54 (LTE) … +2');
});
test('netStateRender paints the service badge, location and rows', () => { test('netStateRender paints the service badge, location and rows', () => {
const els = setup(); const els = setup();
decoder({ 'EF.IMSI': { imsi: '262011234567890' } }); decoder({ 'EF.IMSI': { imsi: '262011234567890' } });
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pysim-simple-server" name = "pysim-simple-server"
version = "2.7.15" version = "2.7.16"
description = "HTTP REST server wrapping pysim for the SIMple PWA" description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8" requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+1 -1
View File
@@ -26,7 +26,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad from osmocom.utils import rpad
VERSION = '2.7.15' VERSION = '2.7.16'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE