-
OTAMan SIM OTA with a Human Face v1.9.3
+
OTAMan SIM OTA with a Human Face v1.9.4
github
@@ -814,6 +814,7 @@
+
@@ -2068,7 +2069,7 @@ function genSp() {
const kicHex = document.getElementById('sp-kic-hex').value;
const kidHex = document.getElementById('sp-kid-hex').value;
const tarHex = (document.getElementById('sp-tar').value || '').replace(/[^0-9a-fA-F]/g, '').padEnd(6, '0').slice(0, 6);
- const cntrHex = (document.getElementById('sp-cntr').value || '').replace(/[^0-9a-fA-F]/g, '').padEnd(10, '0').slice(0, 10);
+ const cntrHex = (document.getElementById('sp-cntr').value || '').replace(/[^0-9a-fA-F]/g, '').padStart(10, '0').slice(0, 10);
const kicKeyHex = (document.getElementById('sp-kic-key').value || '').replace(/[^0-9a-fA-F]/g, '');
const kidKeyHex = (document.getElementById('sp-kid-key').value || '').replace(/[^0-9a-fA-F]/g, '');
const padByte = parseInt(document.getElementById('sp-padding').value, 16);
@@ -4034,6 +4035,8 @@ async function pysimSendOta() {
const sp = document.getElementById('sp-result').value.replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (!sp) { alert('No secured packet to send. Generate a secure packet first.'); return; }
const statusEl = document.getElementById('pysim-status');
+ const sendResultEl = document.getElementById('sp-send-result');
+ sendResultEl.classList.add('hidden');
statusEl.innerHTML = '' + t('Sending OTA...') + '';
try {
const data = await pysimFetch('/api/send-ota', { sp, ...spParams() });
@@ -4047,13 +4050,32 @@ async function pysimSendOta() {
const por = data.por;
if (por && por.response_status) {
msg += ' | PoR status: ' + por.response_status + ' (TAR ' + por.tar + ')';
+ if (por.cntr) msg += ' | PoR CNTR: ' + por.cntr;
if (por.decoded && por.decoded.last_status_word) msg += ' | last SW ' + por.decoded.last_status_word + ' ' + (por.decoded.last_response_data || '');
}
+ sendResultEl.textContent = msg;
+ if (por && por.raw) {
+ const rawLine = document.createElement('div');
+ rawLine.className = 'break-all';
+ rawLine.textContent = 'raw PoR: ' + por.raw;
+ sendResultEl.appendChild(rawLine);
+ }
+ sendResultEl.classList.remove('hidden', 'text-red-600');
+ sendResultEl.classList.add('text-green-600');
statusEl.innerHTML = '' + esc(msg) + '';
} else {
- statusEl.innerHTML = 'OTA failed: ' + (data.error || 'SW: ' + data.sw) + '';
+ let msg = 'OTA failed';
+ if (data.error) msg += ': ' + data.error;
+ if (data.sw) msg += (data.error ? ' | ' : ': ') + 'SW ' + data.sw;
+ sendResultEl.textContent = msg;
+ sendResultEl.classList.remove('hidden', 'text-green-600');
+ sendResultEl.classList.add('text-red-600');
+ statusEl.innerHTML = '' + esc(msg) + '';
}
} catch (e) {
+ sendResultEl.textContent = 'Error: ' + e.message;
+ sendResultEl.classList.remove('hidden', 'text-green-600');
+ sendResultEl.classList.add('text-red-600');
statusEl.innerHTML = 'Error: ' + esc(e.message) + '';
}
}
diff --git a/frontend/sw.js b/frontend/sw.js
index 4af3d94..5763878 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'otaman-v14';
+const CACHE = 'otaman-v15';
const URLS = [
'index.html',
'help.html',
diff --git a/pyproject.toml b/pyproject.toml
index d14e4bb..29ced13 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-otaman-server"
-version = "1.9.3"
+version = "1.9.4"
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
diff --git a/pysim_otaman_server/server.py b/pysim_otaman_server/server.py
index 363b149..d93e794 100644
--- a/pysim_otaman_server/server.py
+++ b/pysim_otaman_server/server.py
@@ -18,7 +18,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
-VERSION = '1.9.3'
+VERSION = '1.9.4'
# Static file serving (the PWA lives in /frontend, served by this server
@@ -334,7 +334,12 @@ def _decode_por(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex, respon
out = {
'response_status': str(res['response_status']),
'tar': res['tar'].hex().upper(),
+ 'cntr': res['cntr'].hex().upper(),
'pcntr': res['pcntr'],
+ 'rpl': res['rpl'],
+ 'rhl': res['rhl'],
+ 'cc_rc': res['cc_rc'].hex(),
+ 'raw': response_hex,
}
# Try ExpandedRemoteResponse first (TS 102 226 ยง5.2.2)
@@ -1825,6 +1830,10 @@ class PysimHandler(BaseHTTPRequestHandler):
max_chunk = 130
chunks = [sp_bytes[i:i+max_chunk] for i in range(0, len(sp_bytes), max_chunk)]
total = len(chunks)
+ sys.stderr.write('OTA SEND: SPI %s %s KIc %s KID %s TAR %s CNTR %s LEN %dB CHUNKS %d\n' % (
+ body.get('spi1', ''), body.get('spi2', ''), body.get('kic', ''),
+ body.get('kid', ''), body.get('tar', ''), body.get('cntr', ''),
+ len(sp_bytes), total))
last_data = None
last_sw = None
for i, chunk in enumerate(chunks):
@@ -1836,20 +1845,34 @@ class PysimHandler(BaseHTTPRequestHandler):
last_sw = sw
if sw != '9000' and not sw.startswith('91'):
resp = {'success': False, 'sw': sw, 'error': 'ENVELOPE failed at chunk %d' % (i + 1)}
+ sys.stderr.write('OTA SEND FAILED: chunk %d SW %s\n' % (i + 1, sw))
break
else:
resp = {'success': True, 'sw': last_sw, 'response_data': last_data if last_data else None}
+ por_src = 'envelope'
por_hex = resp['response_data']
if submit_handler and submit_handler.submit_tpdu_hex:
tpdu_b = bytes.fromhex(submit_handler.submit_tpdu_hex)
idx = tpdu_b.find(b'\x02\x71\x00')
if idx >= 0:
por_hex = tpdu_b[idx:].hex()
+ por_src = 'sms-submit'
por = _decode_por(body.get('spi1', ''), body.get('spi2', ''), body.get('kic', ''),
body.get('kid', ''), body.get('cntr', ''), body.get('kicKey', ''),
body.get('kidKey', ''), por_hex)
if por:
resp['por'] = por
+ extra = ''
+ if por.get('decoded'):
+ extra = ' (compact: %s cmd, last SW %s)' % (por['decoded'].get('number_of_commands', '?'),
+ por['decoded'].get('last_status_word', '?'))
+ sys.stderr.write('OTA PoR[%s]: status=%s TAR=%s CNTR=%s PCNTR=%s RPL=%s RHL=%s%s\n' % (
+ por_src, por.get('response_status'), por.get('tar'), por.get('cntr'),
+ por.get('pcntr'), por.get('rpl'), por.get('rhl'), extra))
+ elif por_hex:
+ sys.stderr.write('OTA PoR[%s]: undecodable raw=%s\n' % (por_src, str(por_hex)[:64]))
+ else:
+ sys.stderr.write('OTA PoR[%s]: none\n' % por_src)
finally:
if submit_handler and hasattr(scc, '_tp'):
scc._tp.proactive_handler = old_proactive
diff --git a/tests/test_ota_helpers.py b/tests/test_ota_helpers.py
index 00c72f5..582c6b8 100644
--- a/tests/test_ota_helpers.py
+++ b/tests/test_ota_helpers.py
@@ -202,6 +202,30 @@ class TestDecodePor(unittest.TestCase):
self.assertEqual(r['response_status'], 'por_ok')
self.assertEqual(r['decoded']['last_status_word'], '612f')
+ def test_complete_field_report(self):
+ """All parsed PoR fields are surfaced verbatim (v1.9.4)."""
+ raw = '027100000e0ab000110000000000000001612f'
+ r = _decode_por('06', '01', '35', '35', '0000000001', KIC3, KID3, raw)
+ self.assertEqual(r['response_status'], 'por_ok')
+ self.assertEqual(r['tar'], 'B00011')
+ self.assertEqual(r['cntr'], '0000000000')
+ self.assertEqual(r['pcntr'], 0)
+ self.assertEqual(r['rpl'], 14)
+ self.assertEqual(r['rhl'], 10)
+ self.assertEqual(r['cc_rc'], '')
+ self.assertEqual(r['raw'], raw)
+ self.assertNotIn('cntr_low', str(r))
+
+ def test_cntr_low_fields(self):
+ r = _decode_por('02', '01', '15', '15', '0000000001', K, K,
+ '027100000b0ab0000000000000070002')
+ self.assertEqual(r['response_status'], 'cntr_low')
+ self.assertEqual(r['tar'], 'B00000')
+ self.assertEqual(r['cntr'], '0000000007')
+ self.assertEqual(r['rpl'], 11)
+ self.assertEqual(r['rhl'], 10)
+ self.assertIsNone(r.get('decoded'))
+
def test_sysmocom_bad_cc_returns_none(self):
r = _decode_por('06', '09', '35', '35', '0000000001', KIC3, KID3,
'027100001612b000110000000000000055f47118381175fb02612f')