Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69a20baa9f | |||
| f764760b0a | |||
| a2f7b60333 | |||
| 3b7dbf1743 | |||
| 7c5d0051d4 | |||
| 302a1f87a2 | |||
| 52bcd31387 |
+117
-26
@@ -16,7 +16,7 @@
|
|||||||
<div class="max-w-7xl mx-auto px-6 py-4">
|
<div class="max-w-7xl mx-auto px-6 py-4">
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
<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">
|
<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>
|
<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>
|
<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>
|
<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>
|
||||||
<div id="pysim-sub-proactive" class="hidden">
|
<div id="pysim-sub-proactive" class="hidden">
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-6">
|
||||||
<div>
|
<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-1 text-xs text-gray-500 dark:text-slate-400" data-l10n="STATUS and Polling">STATUS and Polling</div>
|
||||||
<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="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 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 id="pysim-proactive-log" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -2634,6 +2641,8 @@ async function pysimConnect() {
|
|||||||
await pysimRefresh();
|
await pysimRefresh();
|
||||||
pysimLoadCommands();
|
pysimLoadCommands();
|
||||||
pysimFsRefresh();
|
pysimFsRefresh();
|
||||||
|
pysimPollStatusInit();
|
||||||
|
pysimStartBackendPoll();
|
||||||
btn.textContent = t('Connected');
|
btn.textContent = t('Connected');
|
||||||
stkCheckMenu();
|
stkCheckMenu();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -2645,6 +2654,7 @@ async function pysimConnect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pysimDisconnect() {
|
function pysimDisconnect() {
|
||||||
|
if (_pysimPollTimer) { clearInterval(_pysimPollTimer); _pysimPollTimer = null; }
|
||||||
pysimShowConnected(false);
|
pysimShowConnected(false);
|
||||||
document.getElementById('pysim-status').textContent = '';
|
document.getElementById('pysim-status').textContent = '';
|
||||||
pysimSetConnected(false);
|
pysimSetConnected(false);
|
||||||
@@ -3096,7 +3106,7 @@ function pysimSwitchSubtab(name) {
|
|||||||
document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name);
|
document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name);
|
||||||
});
|
});
|
||||||
if (name === 'custom') pysimCustomRender();
|
if (name === 'custom') pysimCustomRender();
|
||||||
if (name === 'proactive') { pysimEventsRender(); pysimProactiveLogRender(); pysimPliRender(); }
|
if (name === 'proactive') { pysimEventsRender(); pysimProactiveLogRender(); pysimPliRender(); pysimPollStatusInit(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function pysimCmdKeydown(e) {
|
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">'
|
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()
|
+ e.toString(16).padStart(2, '0').toUpperCase()
|
||||||
+ '</span> ' + esc(name)
|
+ '</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;
|
el.innerHTML = html;
|
||||||
} catch (e) {
|
} 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 =====
|
// ===== proactive command log =====
|
||||||
const CMD_NAMES = {
|
const CMD_NAMES = {
|
||||||
'03': 'POLL INTERVAL', '05': 'SET UP EVENT LIST',
|
'03': 'POLL INTERVAL', '05': 'SET UP EVENT LIST',
|
||||||
@@ -3921,6 +3990,16 @@ const CMD_NAMES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CMD_QUALIFIER_SHORT = {
|
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',
|
'26': {0x00:'Loc', 0x01:'IMEI', 0x02:'NMR', 0x03:'Time', 0x04:'Lang', 0x05:'TA',
|
||||||
0x06:'AccTech', 0x08:'IMEISV', 0x09:'Search', 0x0A:'Batt', 0x0C:'WSID',
|
0x06:'AccTech', 0x08:'IMEISV', 0x09:'Search', 0x0A:'Batt', 0x0C:'WSID',
|
||||||
0x0D:'BCInfo', 0x0E:'MultiAT', 0x0F:'MultiLoc', 0x10:'MultiNMR',
|
0x0D:'BCInfo', 0x0E:'MultiAT', 0x0F:'MultiLoc', 0x10:'MultiNMR',
|
||||||
@@ -3943,19 +4022,42 @@ async function pysimProactiveLogRender() {
|
|||||||
let qual = '';
|
let qual = '';
|
||||||
if (e.qualifier) {
|
if (e.qualifier) {
|
||||||
const qs = CMD_QUALIFIER_SHORT[e.type_hex];
|
const qs = CMD_QUALIFIER_SHORT[e.type_hex];
|
||||||
if (qs) qual = ' [' + (qs[parseInt(e.qualifier,16)] || '0x'+e.qualifier.toUpperCase()) + ']';
|
if (qs) {
|
||||||
else qual = ' [0x' + e.qualifier.toUpperCase() + ']';
|
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">'
|
+ '<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="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="font-mono text-gray-400">0x' + e.type_hex.toUpperCase() + '</span>'
|
||||||
+ '<span class="truncate">' + esc(name) + '</span>'
|
+ '<span class="truncate">' + esc(name) + '</span>'
|
||||||
+ (qual ? '<span class="text-[10px] text-gray-500 dark:text-slate-400">' + esc(qual) + '</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>';
|
+ '</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;
|
el.innerHTML = html;
|
||||||
} catch (err) {
|
} 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 =====
|
// ===== PLI data dictionary =====
|
||||||
function decPlmn(hex3) {
|
function decPlmn(hex3) {
|
||||||
const h = hex3.replace(/\s/g, '').slice(0, 6);
|
const h = hex3.replace(/\s/g, '').slice(0, 6);
|
||||||
@@ -4541,6 +4630,8 @@ const LANG_RU = {
|
|||||||
'Card reader inserted': 'Картридер вставлен',
|
'Card reader inserted': 'Картридер вставлен',
|
||||||
'PROVIDE LOCAL INFORMATION response data': 'Данные для PROVIDE LOCAL INFORMATION',
|
'PROVIDE LOCAL INFORMATION response data': 'Данные для PROVIDE LOCAL INFORMATION',
|
||||||
'Send STATUS': 'Отправить STATUS',
|
'Send STATUS': 'Отправить STATUS',
|
||||||
|
'STATUS and Polling': 'STATUS и опрос',
|
||||||
|
'Polling': 'Опрос',
|
||||||
'STATUS...': 'STATUS...',
|
'STATUS...': 'STATUS...',
|
||||||
'Saving...': 'Сохранение...',
|
'Saving...': 'Сохранение...',
|
||||||
'Saved': 'Сохранено',
|
'Saved': 'Сохранено',
|
||||||
|
|||||||
Reference in New Issue
Block a user