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.
Connected
- Status
- Reset
- Equip
- Disconnect
+ Equip card
+ Check status
+ Reset card
+ Disconnect from pySim
- Command
+ File manager
+ pySim command line
Raw APDU
- Files
-
-
-
-
-
@@ -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(