esim: show the profile icon image and its data size

pySim already requests the Icon tag (0x94) together with the other
ProfileInfo tags; the mapping dropped it, so the PWA only had the icon
type.

- esim.profiles(): each profile now carries `icon` (the image bytes as
  hex) and `icon_size` (byte count), null when the card sent no image.
- PWA: esimIconDataUrl() builds a data: URL (png/jpg -> image/png|jpeg)
  and the profile card shows the image unscaled to the left of the
  metadata rows; the Icon row keeps the type and adds the data size
  (e.g. 'png · 1234 B').
- tests: profiles icon/icon_size mapping (+ the absent case) and the
  frontend data URL / row / render checks; docs and sw simple-v231.
This commit is contained in:
2026-09-22 00:21:43 +03:00
parent 86808ce55d
commit d3f5aaa443
10 changed files with 76 additions and 13 deletions
+1 -1
View File
@@ -498,7 +498,7 @@
<p class="text-sm mb-2">Для eUICC (SGP.22/SGP.32) подвкладка <strong>&laquo;eSIM&raquo;</strong> читает чип и управляет установленными профилями через локальный интерфейс ES10 (через pySim, без обращения к SM-DP+):</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Чип</strong> — EID, EUICCInfo1/2 со всеми декодированными полями (версия профиля, SVN, прошивка, расширенные ресурсы карты — число установленных приложений и свободная энергонезависимая/энергозависимая память —, битовые списки возможностей UICC и RSP, идентификаторы CI PKI, категория, запрещённые PPR, версия PP, номер аккредитации SS, данные сертификации), настроенные адреса SM-DP+ по умолчанию / корневого DS и <strong>таблица авторизации правил</strong> (ES10b GetRat: идентификаторы PPR, разрешённые операторы, флаг согласия). Неизвестные TLV сохраняются как сырой hex.</li>
<li><strong>Профили</strong> — все профили с состоянием (включён/отключён), ником, оператором, именем, ICCID, ISD-P AID, классом, владельцем и типом значка. Кнопки <strong>&laquo;Включить&raquo;</strong>/<strong>&laquo;Отключить&raquo;</strong> переключают профиль; карта обычно сначала отправляет REFRESH (виден в журнале проактивных команд) — терминальный ответ завершает переключение, после чего сессия карты переинициализируется как при equip: ICCID, сетевое состояние и все кэшированные данные перечитываются, а запрошенное состояние проверяется по перечитанному списку профилей.</li>
<li><strong>Профили</strong> — все профили с состоянием (включён/отключён), ником, оператором, именем, ICCID, ISD-P AID, классом, владельцем и типом значка — сам значок показывается в натуральную величину рядом с метаданными, а строка <strong>Icon</strong> содержит его тип и размер данных в байтах. Кнопки <strong>&laquo;Включить&raquo;</strong>/<strong>&laquo;Отключить&raquo;</strong> переключают профиль; карта обычно сначала отправляет REFRESH (виден в журнале проактивных команд) — терминальный ответ завершает переключение, после чего сессия карты переинициализируется как при equip: ICCID, сетевое состояние и все кэшированные данные перечитываются, а запрошенное состояние проверяется по перечитанному списку профилей.</li>
<li><strong>Уведомления</strong> — список ожидающих уведомлений только для чтения (номер последовательности, операция, адрес, ICCID); ничего не обрабатывается и не удаляется.</li>
</ul>
<p class="text-sm mb-3">Никаких загрузок профилей, обработки уведомлений и взаимодействия с SM-DP+ — используются только локальные функции ES10a/b/c. Если карта не eUICC, подвкладка сообщает об этом.</p>
+1 -1
View File
@@ -498,7 +498,7 @@
<p class="text-sm mb-2">For an eUICC (SGP.22/SGP.32) the <strong>eSIM</strong> pill reads the chip and manages the installed profiles through the local ES10 interface (via pySim, no SM-DP+ contact):</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Chip</strong> — EID, EUICCInfo1/2 with every field decoded (profile version, SVN, firmware, extended card resource — installed applications and free non-volatile/volatile memory —, UICC and RSP capability bit lists, CI PKI identifiers, category, forbidden profile policy rules, PP version, SS accreditation number, certification data), the configured default SM-DP+ / root DS addresses, and the <strong>rules authorisation table</strong> (ES10b GetRat: PPR IDs, allowed operators, consent flag). Unknown TLVs are preserved as raw hex.</li>
<li><strong>Profiles</strong> — every profile with its state (enabled/disabled), nickname, provider, name, ICCID, ISD-P AID, class, owner and icon type. <strong>Enable</strong>/<strong>Disable</strong> switches a profile; the card usually sends REFRESH first (logged in the proactive command log) — the terminal response completes the switch and the card session is then re-initialized like an equip, so the ICCID, network state and every cached card view are re-read, and the requested state is verified against the re-read profile list.</li>
<li><strong>Profiles</strong> — every profile with its state (enabled/disabled), nickname, provider, name, ICCID, ISD-P AID, class, owner and icon type — the icon image itself is shown at its natural size next to the metadata, and the <strong>Icon</strong> row carries its type and data size in bytes. <strong>Enable</strong>/<strong>Disable</strong> switches a profile; the card usually sends REFRESH first (logged in the proactive command log) — the terminal response completes the switch and the card session is then re-initialized like an equip, so the ICCID, network state and every cached card view are re-read, and the requested state is verified against the re-read profile list.</li>
<li><strong>Notifications</strong> — read-only list of pending notifications (sequence number, operation, address, ICCID); nothing is processed or removed.</li>
</ul>
<p class="text-sm mb-3">No profile downloads, no notification handling and no SM-DP+ interaction — only the local ES10a/b/c functions are used. The card must be an eUICC; otherwise the pill says so.</p>
+26 -2
View File
@@ -1644,10 +1644,27 @@ function esimProfileRows(p) {
if (p.name) rows.push([t('Profile name'), p.name]);
if (p.class) rows.push([t('Class'), p.class]);
if (p.owner) rows.push([t('Owner'), p.owner]);
if (p.icon_type) rows.push([t('Icon'), p.icon_type]);
if (p.icon_type) {
let icon = p.icon_type;
if (p.icon_size) icon += ' · ' + p.icon_size + ' B';
rows.push([t('Icon'), icon]);
}
return rows;
}
// Profile icon -> data URL at its natural size ('' when the card sent no
// image). The icon type is the SGP.22 iconType: png or jpg.
function esimIconDataUrl(p) {
if (!p || !p.icon || !p.icon_type) return '';
const hex = String(p.icon).replace(/[^0-9a-fA-F]/g, '');
let bin = '';
for (let i = 0; i + 1 < hex.length; i += 2) {
bin += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
const mime = p.icon_type === 'jpg' ? 'image/jpeg' : 'image/' + p.icon_type;
return 'data:' + mime + ';base64,' + btoa(bin);
}
function esimRenderChip() {
const el = document.getElementById('esim-chip');
if (!el) return;
@@ -1705,10 +1722,17 @@ function esimRenderProfiles() {
'class="ml-auto px-2 py-0.5 text-xs rounded text-white disabled:opacity-40 disabled:cursor-not-allowed ' +
(enabled ? 'bg-gray-600 hover:bg-gray-700' : 'bg-emerald-600 hover:bg-emerald-700') + '">' +
esc(t(enabled ? 'Disable' : 'Enable')) + '</button></div>';
let rowsHtml = '';
for (const [k, v] of esimProfileRows(p)) {
html += '<div class="flex gap-2"><span class="w-40 shrink-0 text-right text-gray-500 dark:text-slate-400 break-all">' + esc(k) + '</span>' +
rowsHtml += '<div class="flex gap-2"><span class="w-40 shrink-0 text-right text-gray-500 dark:text-slate-400 break-all">' + esc(k) + '</span>' +
'<span class="font-mono break-all">' + esc(v) + '</span></div>';
}
const iconUrl = esimIconDataUrl(p);
html += '<div class="flex gap-3 items-start">';
if (iconUrl) {
html += '<img src="' + esc(iconUrl) + '" alt="" class="shrink-0">';
}
html += '<div class="min-w-0 flex-1">' + rowsHtml + '</div></div>';
html += '</div>';
}
el.innerHTML = html;
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v230';
const CACHE = 'simple-v231';
const URLS = [
'index.html',
'help.html',
+24 -2
View File
@@ -23,7 +23,8 @@ function extractFunc(src, name) {
let code = '';
for (const fn of ['esimLabel', 'esimGroupEid', 'esimFieldRows', 'esimResultText',
'esimStateLabel', 'esimOperationsText', 'esimProfileRows', 'esimSwitchStatus']) {
'esimStateLabel', 'esimOperationsText', 'esimProfileRows', 'esimIconDataUrl',
'esimSwitchStatus']) {
code += extractFunc(html, fn) + '\n';
}
for (const c of ['ESIM_CHIP_LABELS', 'ESIM_RESULT_KEYS']) {
@@ -124,11 +125,32 @@ test('esimProfileRows renders the metadata in a stable order', () => {
const rows = esimProfileRows({
iccid: '8970119000004002667', isdp_aid: 'A0000005591010FFFFFFFF8900000100',
provider: 'Miranda', name: 'Miranda LTE', class: 'operational',
owner: '250-99', icon_type: 'png',
owner: '250-99', icon_type: 'png', icon_size: 1234,
});
assert.deepStrictEqual(rows.map(r => r[0]),
['ICCID', 'ISD-P AID', 'Provider', 'Profile name', 'Class', 'Owner', 'Icon']);
assert.strictEqual(rows[0][1], '8970119000004002667');
assert.strictEqual(rows[rows.length - 1][1], 'png · 1234 B');
});
test('esimProfileRows shows the bare icon type when the card sent no image', () => {
assert.deepStrictEqual(esimProfileRows({ icon_type: 'jpg' }), [['Icon', 'jpg']]);
});
test('esimIconDataUrl builds a data URL for png and jpg icons', () => {
assert.strictEqual(esimIconDataUrl({ icon_type: 'png', icon: '89504E47' }),
'data:image/png;base64,iVBORw==');
assert.strictEqual(esimIconDataUrl({ icon_type: 'jpg', icon: 'FFD8' }),
'data:image/jpeg;base64,/9g=');
assert.strictEqual(esimIconDataUrl({ icon_type: 'png' }), '');
assert.strictEqual(esimIconDataUrl({ icon: '89504E47' }), '');
assert.strictEqual(esimIconDataUrl(null), '');
});
test('the profile card renders the icon image next to the rows', () => {
const fn = extractFunc(html, 'esimRenderProfiles');
assert.match(fn, /esimIconDataUrl\(p\)/);
assert.match(fn, /<img src=/);
});
test('the eSIM pill is wired into the Phone simulator tab', () => {