From 2a91eb3c687c3cdb0913d59f54bf8eab44dcc523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 6 Aug 2026 23:42:39 +0300 Subject: [PATCH] pysim tab: file browser, AID display, card detection, server simplification --- index.html | 315 ++++++++++++++++++++++++++++----------------------- sim.svg | 41 +++++++ sim_anim.svg | 47 ++++++++ style.css | 4 + sw.js | 1 + 5 files changed, 268 insertions(+), 140 deletions(-) create mode 100644 sim.svg create mode 100644 sim_anim.svg diff --git a/index.html b/index.html index 10413e8..30c71b9 100644 --- a/index.html +++ b/index.html @@ -824,47 +824,27 @@
-

To connect, install and start the pysim-otaman-server:

-

pip install pysim-otaman-server

-

pysim-otaman-server --http-port 8080

-

Then connect to a SIM card reader and click Connect above.

+

To connect, install pysim-otaman-server: git clone https://github.com/anttro/pysim-otaman-server.git and start it. Refer to README.md for details. Click Connect once pysim-otaman-server is ready.

- + + -
-
-
- - - -
- -
- - - - @@ -2490,14 +2487,34 @@ async function pysimConnect() { const btn = document.getElementById('pysim-connect-btn'); btn.disabled = true; btn.textContent = 'Connecting...'; - const ok = await pysimRefresh(); - btn.textContent = ok ? 'Connected' : 'Connect'; - btn.disabled = false; - if (ok) { + const statusEl = document.getElementById('pysim-status'); + statusEl.innerHTML = 'Checking card...'; + try { + const status = await pysimFetch('/api/status'); + if (!status.app_ready) { + throw new Error('Server not ready'); + } + const cmd = await pysimFetch('/api/command', { cmd: 'status' }); + if (cmd.output && (cmd.output.includes('Exception') || cmd.output.includes('ProtocolError'))) { + pysimSetDot(false); + statusEl.innerHTML = 'No card detected. Insert card and click Equip card button'; + pysimShowConnected(true); + btn.textContent = 'Connected'; + btn.disabled = false; + return; + } + pysimSetDot(true); pysimShowConnected(true); + await pysimRefresh(); pysimLoadCommands(); pysimFsRefresh(); + btn.textContent = 'Connected'; + } catch (e) { + pysimSetDot(false); + statusEl.innerHTML = 'Connection failed: ' + esc(e.message) + ''; + btn.textContent = 'Connect'; } + btn.disabled = false; } function pysimDisconnect() { @@ -2506,21 +2523,48 @@ function pysimDisconnect() { pysimSetDot(false); } +async function pysimRunCmd(cmd) { + const statusEl = document.getElementById('pysim-status'); + try { + const result = await pysimFetch('/api/command', { cmd }); + if (result.output && (result.output.includes('Exception') || result.output.includes('ProtocolError') || result.output.includes('SwMatchError'))) { + pysimSetDot(false); + statusEl.innerHTML = 'No card detected. Insert card and click Equip card button'; + return null; + } + return result; + } catch (e) { + pysimSetDot(false); + statusEl.innerHTML = 'No card detected. Insert card and click Equip card button'; + return null; + } +} + async function pysimStatusCmd() { - await pysimFetch('/api/command', { cmd: 'status' }); - pysimRefresh(); + const statusEl = document.getElementById('pysim-status'); + statusEl.innerHTML = 'Checking...'; + const result = await pysimRunCmd('status'); + if (result) await pysimRefresh(); } async function pysimResetCmd() { - await pysimFetch('/api/command', { cmd: 'reset' }); - pysimRefresh(); - pysimFsRefresh(); + const statusEl = document.getElementById('pysim-status'); + statusEl.innerHTML = 'Resetting...'; + const result = await pysimRunCmd('reset'); + if (result) { + await pysimRefresh(); + pysimFsRefresh(); + } } async function pysimEquipCmd() { - await pysimFetch('/api/command', { cmd: 'equip' }); - pysimRefresh(); - pysimFsRefresh(); + const statusEl = document.getElementById('pysim-status'); + statusEl.innerHTML = 'Equipping...'; + const result = await pysimRunCmd('equip'); + if (result) { + await pysimRefresh(); + pysimFsRefresh(); + } } async function pysimExec() { @@ -2614,68 +2658,66 @@ async function pysimLoadCommands() { // ===== pysim file browser ===== let pysimFsTreeRoot = null; -async function pysimFsSelectNode(node) { - let sel; - if (node.name === 'MF') { - sel = 'MF'; - } else if (node.name.startsWith('ADF.')) { - sel = node.name; - } else if (node.fid) { - sel = node.fid; - } else { - sel = node.name; - } - await pysimFetch('/api/command', { cmd: 'select ' + sel }); +function getParentSel(node) { + if (!node || !node.parent) return null; + const p = node.parent; + if (p.name === 'MF') return 'MF'; + if (p.name.startsWith('ADF.')) return p.name; + if (p.fid) return p.fid; + return p.name; } async function pysimFsRefresh() { const detailEl = document.getElementById('pysim-fs-detail'); detailEl.classList.add('hidden'); - await pysimFetch('/api/command', { cmd: 'select MF' }); - const status = await pysimFetch('/api/status'); - const cur = status.current_selection; + const data = await pysimFetch('/api/tree', { name: 'MF' }); pysimFsTreeRoot = { - name: cur ? cur.name : 'MF', - path: cur ? cur.path : 'MF', - fid: cur ? cur.fid : '3F00', + name: data.name || 'MF', + path: data.name || 'MF', + fid: data.fid || '3F00', isDir: true, children: null, expanded: true, parent: null, }; - await pysimFsLoadChildren(pysimFsTreeRoot); + pysimFsTreeRoot.children = (data.children || []).map(c => ({ + name: c.name, + fid: c.fid, + isDir: c.isDir, + aid: c.aid, + children: null, + expanded: false, + exists: true, + parent: pysimFsTreeRoot, + })); + pysimFsTreeRoot.exists = true; pysimFsRenderTree(); } async function pysimFsLoadChildren(node) { if (node.children) return; try { - await pysimFsSelectNode(node); - const st = await pysimFetch('/api/status'); - if (st.current_selection && st.current_selection.name !== node.name) { + let body = { name: node.name, fid: node.fid }; + const parentSel = getParentSel(node); + if (parentSel) body.parent_sel = parentSel; + let data = await pysimFetch('/api/tree', body); + if (!data || data.exists === false) { + body = { name: node.name, fid: node.fid }; + data = await pysimFetch('/api/tree', body); + } + if (!data || data.exists === false) { throw new Error('File not found'); } - const data = await pysimFetch('/api/command', { cmd: 'tree' }); - const lines = (data.output || '').split('\n'); - const children = []; - for (const line of lines) { - if (line.startsWith(' ')) continue; - const m = line.match(/^(\S+)\s+([0-9a-fA-F]{4})?(?:\s|$)/); - if (m) { - const name = m[1]; - const fid = m[2] ? m[2].toLowerCase() : null; - children.push({ - name: name, - fid: fid, - isDir: name.startsWith('ADF.') || name.startsWith('DF.') || name === 'MF', - children: null, - expanded: false, - exists: true, - parent: node, - }); - } - } - node.children = children; + node.children = (data.children || []).map(c => ({ + name: c.name, + fid: c.fid, + isDir: c.isDir, + aid: c.aid, + children: null, + expanded: false, + exists: true, + parent: node, + })); node.exists = true; } catch (e) { node.exists = false; @@ -2696,6 +2738,8 @@ function pysimFsRenderNode(node, depth) { const isDir = node.isDir; const exists = node.exists !== false; const color = exists ? (isDir ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-slate-300') : 'text-red-400 dark:text-red-400'; + const fidStr = node.fid ? ' (' + node.fid.toUpperCase() + ')' : ''; + const aidStr = node.aid ? ' [' + node.aid + ']' : ''; let html = '
'; html += pad; if (!exists) { @@ -2705,14 +2749,14 @@ function pysimFsRenderNode(node, depth) { if (exists) { const toggle = node.children && node.expanded ? '▼' : '▶'; html += '' + toggle + ' '; - html += '' + esc(node.name) + ''; + html += '' + esc(node.name) + fidStr + aidStr + ''; } else { - html += esc(node.name) + '
'; + html += esc(node.name) + fidStr + aidStr + ''; return html; } } else { html += '' + esc(node.name) + ''; + (exists ? ' onclick="pysimFsClickFile(\'' + esc(node.name) + '\')"' : '') + '>' + esc(node.name) + fidStr + aidStr + ''; } html += ''; if (isDir && node.children && node.expanded) { @@ -2749,77 +2793,68 @@ async function pysimFsToggleDir(name) { async function pysimFsClickFile(name) { pysimFsSetMode('raw'); const node = pysimFsFindNode(name, pysimFsTreeRoot); - if (node && node.fid) { - await pysimFetch('/api/command', { cmd: 'select ' + node.fid }); - } else { - await pysimFetch('/api/command', { cmd: 'select ' + name }); + let body = { name: name, fid: node ? node.fid : null }; + const parentSel = getParentSel(node); + if (parentSel) body.parent_sel = parentSel; + let sel = await pysimFetch('/api/select', body); + if (!sel || sel.exists === false) { + body = { name: name, fid: node ? node.fid : null }; + sel = await pysimFetch('/api/select', body); } - const status = await pysimFetch('/api/status'); - const ft = status.current_selection?.file_type; + const exists = sel && sel.exists !== false; + if (node) { + node.exists = exists; + if (!exists) node.children = []; + pysimFsRenderTree(); + } + const ft = exists ? sel.file_type : null; const label = ({ transparent: 'TR', linear_fixed: 'LF', cyclic: 'CY', ber_tlv: 'BT', df: 'DF' })[ft] || ''; - document.getElementById('pysim-fs-filename').textContent = name + (label ? ' [' + label + ']' : ''); + const prefix = exists ? '' : '✗ '; + document.getElementById('pysim-fs-filename').textContent = prefix + name + (label ? ' [' + label + ']' : ''); document.getElementById('pysim-fs-content').innerHTML = ''; - document.getElementById('pysim-fs-status').textContent = ''; - document.getElementById('pysim-fs-detail').classList.remove('hidden'); + document.getElementById('pysim-fs-status').textContent = exists ? '' : '✗ File not found on card'; + if (exists) { + document.getElementById('pysim-fs-detail').classList.remove('hidden'); + } else { + document.getElementById('pysim-fs-detail').classList.add('hidden'); + } pysimRefresh(); } async function pysimFsRead() { - const name = document.getElementById('pysim-fs-filename').textContent; + const rawName = document.getElementById('pysim-fs-filename').textContent.replace(/ \[.*?\]$/, '').replace(/^✗ /, ''); const out = document.getElementById('pysim-fs-content'); const statusEl = document.getElementById('pysim-fs-status'); out.innerHTML = ''; statusEl.textContent = 'Loading...'; try { - const node = pysimFsFindNode(name, pysimFsTreeRoot); - if (node && node.fid) { - await pysimFetch('/api/command', { cmd: 'select ' + node.fid }); - } else { - await pysimFetch('/api/command', { cmd: 'select ' + name }); + const node = pysimFsFindNode(rawName, pysimFsTreeRoot); + let body = { name: rawName, fid: node ? node.fid : null, mode: pysimFsDecodedMode ? 'decoded' : 'raw' }; + const parentSel = getParentSel(node); + if (parentSel) body.parent_sel = parentSel; + let data = await pysimFetch('/api/read', body); + if (!data.success) { + body = { name: rawName, fid: node ? node.fid : null, mode: pysimFsDecodedMode ? 'decoded' : 'raw' }; + data = await pysimFetch('/api/read', body); } - const status = await pysimFetch('/api/status'); - const ft = status.current_selection?.file_type; - const isRecord = ft === 'linear_fixed' || ft === 'cyclic'; - const cmd = pysimFsDecodedMode - ? (isRecord ? 'read_records_decoded' : 'read_binary_decoded') - : (isRecord ? 'read_records' : 'read_binary'); - const data = await pysimFetch('/api/command', { cmd }); - const output = data.output || ''; - const swMatch = output.match(/SW:\s*(\w+)/); - const sw = swMatch ? swMatch[1] : null; - const errMatch = output.match(/got (\w+)/); - if (errMatch) { - const swErr = errMatch[1]; - const desc = ({'6982':'Security status not satisfied (PIN required?)','6983':'PIN blocked','6985':'Conditions of use not satisfied','6A88':'Referenced data not found','6A82':'File not found'})[swErr] || 'Error'; - statusEl.textContent = 'SW: ' + swErr + ' — ' + desc; + if (!data.success) { + statusEl.textContent = 'SW: ' + (data.sw || '?') + ' — ' + (data.error || 'Error'); return; } - statusEl.textContent = sw === '9000' ? 'SW: 9000 OK' : sw ? 'SW: ' + sw : 'OK'; - if (pysimFsDecodedMode) { - try { - const parsed = JSON.parse(output); - out.innerHTML = '
' + esc(JSON.stringify(parsed, null, 2)) + '
'; - } catch (e) { - out.innerHTML = ''; + statusEl.textContent = 'SW: ' + data.sw + ' OK'; + if (data.decoded) { + out.innerHTML = '
' + esc(JSON.stringify(data.decoded, null, 2)) + '
'; + } else if (data.records) { + let html = ''; + for (const r of data.records) { + html += '
' + + '' + r.num + '' + + '' + + '
'; } + out.innerHTML = html || '(no records)'; } else { - const clean = output.replace(/^SW:\s*\w+\s*/m, '').replace(/^SW:\s*\w+\s*/m, '').trim(); - if (isRecord) { - const lines = clean.split('\n'); - let html = ''; - for (const line of lines) { - const m = line.match(/^(\d+)\s(.+)/); - if (m) { - html += '
' + - '' + m[1] + '' + - '' + - '
'; - } - } - out.innerHTML = html || '(no records)'; - } else { - out.innerHTML = ''; - } + out.innerHTML = ''; } } catch (e) { statusEl.textContent = 'Error: ' + e.message; diff --git a/sim.svg b/sim.svg new file mode 100644 index 0000000..e0eccc8 --- /dev/null +++ b/sim.svg @@ -0,0 +1,41 @@ + + + + diff --git a/sim_anim.svg b/sim_anim.svg new file mode 100644 index 0000000..0e9d7b1 --- /dev/null +++ b/sim_anim.svg @@ -0,0 +1,47 @@ + + + + diff --git a/style.css b/style.css index 0220ddd..34ea3ae 100644 --- a/style.css +++ b/style.css @@ -1041,6 +1041,10 @@ video { color: rgb(202 138 4 / var(--tw-text-opacity, 1)); } +.underline { + text-decoration-line: underline; +} + .filter { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } diff --git a/sw.js b/sw.js index 222de58..b3c71cc 100644 --- a/sw.js +++ b/sw.js @@ -26,6 +26,7 @@ self.addEventListener('activate', e => { self.addEventListener('fetch', e => { if (!e.request.url.startsWith('http')) return; + if (e.request.method !== 'GET') return; const isNavigate = e.request.mode === 'navigate' || e.request.url.endsWith('sw.js'); if (isNavigate) { e.respondWith(