Compare commits

..

7 Commits

2 changed files with 121 additions and 26 deletions
+117 -26
View File
@@ -16,7 +16,7 @@
<div class="max-w-7xl mx-auto px-6 py-4">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.5.0</span></h1>
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.5.1</span></h1>
<div class="flex items-center gap-4">
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
@@ -927,12 +927,19 @@
<textarea id="pysim-apdu-output" rows="5" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
</div>
<div id="pysim-sub-proactive" class="hidden">
<div class="flex gap-4">
<div class="flex gap-6">
<div>
<button id="pli-status-btn" onclick="pysimStatusPoll()" class="mb-1 px-2 py-0.5 text-[10px] rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Send STATUS">Send STATUS</button>
<div class="mb-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Events that the card monitors (SET UP EVENT LIST):">Events that the card monitors (SET UP EVENT LIST):</div>
<div class="mb-1 text-xs text-gray-500 dark:text-slate-400" data-l10n="STATUS and Polling">STATUS and Polling</div>
<div class="flex items-center gap-2 mb-3">
<button id="pli-status-btn" onclick="pysimStatusPoll()" class="px-2 py-0.5 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Send STATUS">Send STATUS</button>
<span class="text-[10px] text-gray-500 dark:text-slate-400">|</span>
<span class="text-[10px] text-gray-500 dark:text-slate-400" data-l10n="Polling">Polling</span>
<button id="pli-pause-btn" onclick="pysimPollToggle()" 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">OFF</button>
<span id="pli-poll-stat" class="text-[10px] text-gray-400"></span>
</div>
<div class="mt-2 mb-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Events that the card monitors (SET UP EVENT LIST):">Events that the card monitors (SET UP EVENT LIST):</div>
<div id="pysim-event-list" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
<div class="mt-3 mb-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Fetched proactive commands:">Fetched proactive commands:</div>
<div class="mt-4 mb-2 text-xs text-gray-500 dark:text-slate-400" data-l10n="Fetched proactive commands:">Fetched proactive commands:</div>
<div id="pysim-proactive-log" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
</div>
<div>
@@ -2634,6 +2641,8 @@ async function pysimConnect() {
await pysimRefresh();
pysimLoadCommands();
pysimFsRefresh();
pysimPollStatusInit();
pysimStartBackendPoll();
btn.textContent = t('Connected');
stkCheckMenu();
} catch (e) {
@@ -2645,6 +2654,7 @@ async function pysimConnect() {
}
function pysimDisconnect() {
if (_pysimPollTimer) { clearInterval(_pysimPollTimer); _pysimPollTimer = null; }
pysimShowConnected(false);
document.getElementById('pysim-status').textContent = '';
pysimSetConnected(false);
@@ -3096,7 +3106,7 @@ function pysimSwitchSubtab(name) {
document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name);
});
if (name === 'custom') pysimCustomRender();
if (name === 'proactive') { pysimEventsRender(); pysimProactiveLogRender(); pysimPliRender(); }
if (name === 'proactive') { pysimEventsRender(); pysimProactiveLogRender(); pysimPliRender(); pysimPollStatusInit(); }
}
function pysimCmdKeydown(e) {
@@ -3758,7 +3768,7 @@ async function pysimEventsRender() {
html += '<div class="flex items-center gap-2 py-0.5"><span class="w-6 text-right text-gray-400">'
+ e.toString(16).padStart(2, '0').toUpperCase()
+ '</span> ' + esc(name)
+ '<button onclick="pysimEventSendForm(' + e + ')" class="ml-auto px-1.5 py-0.5 text-[10px] rounded bg-blue-600 text-white hover:bg-blue-700">' + t('Send') + '</button></div>';
+ '<button onclick="pysimEventSendForm(' + e + ')" class="ml-auto px-1.5 py-0.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700">' + t('Send') + '</button></div>';
});
el.innerHTML = html;
} catch (e) {
@@ -3910,6 +3920,65 @@ async function pysimStatusPoll() {
}
}
let _pysimPollTimer = null;
async function pysimPollToggle() {
const btn = document.getElementById('pli-pause-btn');
if (!btn) return;
try {
const status = await pysimFetch('/api/poll-status');
const newEnabled = !status.enabled;
const resp = await pysimFetch('/api/poll-toggle', { enabled: newEnabled });
pysimUpdatePollUI(resp.enabled, resp.interval);
} catch (e) { /* ignore */ }
}
function pysimUpdatePollUI(enabled, interval) {
const btn = document.getElementById('pli-pause-btn');
const stat = document.getElementById('pli-poll-stat');
if (!btn) return;
if (enabled) {
btn.textContent = 'ON';
btn.classList.remove('bg-gray-200', 'dark:bg-slate-700', 'text-gray-700', 'dark:text-slate-300');
btn.classList.add('bg-emerald-600', 'text-white');
} else {
btn.textContent = 'OFF';
btn.classList.remove('bg-emerald-600', 'text-white');
btn.classList.add('bg-gray-200', 'dark:bg-slate-700', 'text-gray-700', 'dark:text-slate-300');
}
if (stat) stat.textContent = enabled ? '(' + interval + 's)' : '';
}
async function pysimPollStatusInit() {
try {
const status = await pysimFetch('/api/poll-status');
pysimUpdatePollUI(status.enabled, status.interval);
} catch (e) { /* ignore */ }
}
function pysimStartBackendPoll() {
if (_pysimPollTimer) clearInterval(_pysimPollTimer);
let _hadData = false;
_pysimPollTimer = setInterval(async () => {
try {
const [log, stk, ps] = await Promise.all([
pysimFetch('/api/proactive-log'),
pysimFetch('/api/stk-status'),
pysimFetch('/api/poll-status'),
]);
pysimUpdatePollUI(ps.enabled, ps.interval);
if (Array.isArray(log) && log.length > 0) _hadData = true;
if (_hadData && !ps.enabled && Array.isArray(log) && log.length === 0) {
_hadData = false;
const statusEl = document.getElementById('pysim-status');
if (statusEl && statusEl.textContent.trim() !== '') {
statusEl.innerHTML = '<span class="text-red-500">Card disconnected</span>';
}
}
} catch (e) { /* ignore */ }
}, 5000);
}
// ===== proactive command log =====
const CMD_NAMES = {
'03': 'POLL INTERVAL', '05': 'SET UP EVENT LIST',
@@ -3921,6 +3990,16 @@ const CMD_NAMES = {
};
const CMD_QUALIFIER_SHORT = {
'01': {0x00:'NAA Init+FC', 0x01:'File Chg', 0x02:'NAA Init+FC', 0x03:'NAA Init',
0x04:'UICC Reset', 0x05:'App Reset', 0x06:'Sess Reset', 0x07:'SoR', 0x08:'SoR I-WLAN'},
'10': {0x00:'IfNotBusy', 0x01:'IfNotBusy+Redial', 0x02:'HoldOthers', 0x03:'HoldOthers+Redial',
0x04:'DiscOthers', 0x05:'DiscOthers+Redial'},
'15': {0x00:'New', 0x01:'Existing', 0x02:'Close+New'},
'21': {0x00:'Normal', 0x01:'HighPri', 0x80:'WaitUser'},
'22': {0x00:'Digits', 0x01:'Alpha', 0x04:'YesNo', 0x08:'ImmDigit'},
'23': {0x00:'Digits+Echo', 0x01:'Alpha+Echo', 0x04:'NoEcho'},
'24': {0x00:'Normal', 0x02:'Help', 0x04:'DefaultItem', 0x80:'Nav'},
'25': {0x00:'Normal', 0x04:'Help', 0x80:'Nav'},
'26': {0x00:'Loc', 0x01:'IMEI', 0x02:'NMR', 0x03:'Time', 0x04:'Lang', 0x05:'TA',
0x06:'AccTech', 0x08:'IMEISV', 0x09:'Search', 0x0A:'Batt', 0x0C:'WSID',
0x0D:'BCInfo', 0x0E:'MultiAT', 0x0F:'MultiLoc', 0x10:'MultiNMR',
@@ -3943,19 +4022,42 @@ async function pysimProactiveLogRender() {
let qual = '';
if (e.qualifier) {
const qs = CMD_QUALIFIER_SHORT[e.type_hex];
if (qs) qual = ' [' + (qs[parseInt(e.qualifier,16)] || '0x'+e.qualifier.toUpperCase()) + ']';
else qual = ' [0x' + e.qualifier.toUpperCase() + ']';
if (qs) {
const qv = qs[parseInt(e.qualifier,16)];
if (qv) qual = ' [' + qv + ']';
}
html += '<div data-plog-row="' + i + '">'
}
html += '<div>'
+ '<div class="flex items-center gap-2 py-0.5">'
+ (e.tr_hex ? '<span class="cursor-pointer select-none text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" onclick="pysimPlogToggle(' + i + ')" id="plog-tog-' + i + '">▶</span>' : '<span class="w-3"></span>')
+ '<span class="w-12 text-right text-gray-400">' + e.elapsed + 's</span>'
+ '<span class="font-mono text-gray-400">0x' + e.type_hex.toUpperCase() + '</span>'
+ '<span class="truncate">' + esc(name) + '</span>'
+ (qual ? '<span class="text-[10px] text-gray-500 dark:text-slate-400">' + esc(qual) + '</span>' : '')
+ '</div>'
+ (e.tr_hex ? '<div class="pli-decode-panel hidden ml-4 pl-2 border-l border-gray-300 dark:border-slate-600" data-plog-data="' + i + '"><div class="text-[10px] font-mono text-gray-500 dark:text-slate-400 max-h-20 overflow-auto whitespace-pre-wrap">Response: ' + esc(e.tr_hex) + '\nSW: ' + esc(e.tr_sw) + '</div></div>' : '')
+ '</div>';
if (e.tr_hex) {
html += '<div class="text-[10px] pl-[5.5rem] text-gray-400 dark:text-slate-500">Response: ';
if (e.type_hex === '26' && e.qualifier) {
const codec = PLI_CODECS[e.qualifier];
if (codec && codec.decode) {
const vals = codec.decode(e.tr_hex);
if (vals) {
const parts = [];
codec.fields.forEach(f => {
if (vals[f.id] !== undefined) parts.push(f.label + ': ' + vals[f.id]);
});
html += esc(parts.length ? parts.join(', ') : e.tr_hex);
} else {
html += esc(e.tr_hex);
}
} else {
html += esc(e.tr_hex);
}
} else {
html += esc(e.tr_hex);
}
html += '</div>';
}
html += '</div>';
});
el.innerHTML = html;
} catch (err) {
@@ -3963,19 +4065,6 @@ async function pysimProactiveLogRender() {
}
}
function pysimPlogToggle(idx) {
const panel = document.querySelector('[data-plog-data="' + idx + '"]');
const tog = document.getElementById('plog-tog-' + idx);
if (!panel || !tog) return;
if (panel.classList.contains('hidden')) {
panel.classList.remove('hidden');
tog.textContent = '▼';
} else {
panel.classList.add('hidden');
tog.textContent = '▶';
}
}
// ===== PLI data dictionary =====
function decPlmn(hex3) {
const h = hex3.replace(/\s/g, '').slice(0, 6);
@@ -4541,6 +4630,8 @@ const LANG_RU = {
'Card reader inserted': 'Картридер вставлен',
'PROVIDE LOCAL INFORMATION response data': 'Данные для PROVIDE LOCAL INFORMATION',
'Send STATUS': 'Отправить STATUS',
'STATUS and Polling': 'STATUS и опрос',
'Polling': 'Опрос',
'STATUS...': 'STATUS...',
'Saving...': 'Сохранение...',
'Saved': 'Сохранено',
+4
View File
@@ -870,6 +870,10 @@ video {
gap: 1rem;
}
.gap-6 {
gap: 1.5rem;
}
.gap-x-3 {
-moz-column-gap: 0.75rem;
column-gap: 0.75rem;