diff --git a/frontend/index.html b/frontend/index.html
index b2cc724..ed02af4 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1711,7 +1711,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
-const SIMPLE_VERSION = '3.6.18';
+const SIMPLE_VERSION = '3.6.19';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
diff --git a/frontend/sw.js b/frontend/sw.js
index 6145420..23e6c45 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v291';
+const CACHE = 'simple-v292';
const URLS = [
'index.html',
'help.html',
diff --git a/pyproject.toml b/pyproject.toml
index 8c60a51..d4b0614 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.6.18"
+version = "3.6.19"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py
index 35f21e5..c5aaf32 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
-VERSION = '3.6.18'
+VERSION = '3.6.19'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -1208,6 +1208,10 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
sp_hex, _ = _build_secured_packet(spi1, spi2, sp['kic'], sp['kid'], sp['tar'],
state['cntr'], apdu_hex,
sp['kic_key'], sp['kid_key'])
+ # Log the plaintext APDU and the packed packet like /api/send-ota does
+ # (the step name keeps the sequence readable).
+ sys.stderr.write('RAM C-APDU (%s): %s\n' % (step_name, apdu_hex))
+ sys.stderr.write('RAM SECURED-PACKET (%s): %s\n' % (step_name, sp_hex))
except ValueError as e:
state['encode_error'] = str(e)
state['steps'].append({'name': step_name, 'por_status': 'encode_error',
diff --git a/tests/test_ota_helpers.py b/tests/test_ota_helpers.py
index b1e8d0c..079b2d0 100644
--- a/tests/test_ota_helpers.py
+++ b/tests/test_ota_helpers.py
@@ -1475,3 +1475,38 @@ class ResponseScriptingTest(unittest.TestCase):
self.assertIsNone(_parse_response_scripting(bytes.fromhex('027100000263100BD2')))
self.assertIsNone(_parse_response_scripting(b''))
+
+
+class RamSendGpApduLoggingTest(unittest.TestCase):
+ """_ram_send_gp_apdu (the shared step sender of /api/ram-install and
+ /api/ram-install-app) logs the plaintext RAM APDU and the packed SCP80
+ packet for every step, like /api/send-ota does."""
+
+ def test_logs_plaintext_apdu_and_packed_packet(self):
+ import contextlib
+ import io
+ from pysim_simple_server import server as srv
+
+ class FakeServer:
+ sms_oa = '12345'
+ sms_sc = '12345678912'
+
+ sp = {'spi1': '16', 'spi2': '01', 'kic': '25', 'kid': '25', 'tar': '000000',
+ 'kic_key': 'AA', 'kid_key': 'BB', 'include_cpi': True}
+ state = {'steps': [], 'encode_error': None, 'failure': {}, 'cntr': '0000000001'}
+ orig_build = srv._build_secured_packet
+ orig_send = srv._send_secured_packet
+ try:
+ srv._build_secured_packet = lambda *a, **k: ('AABB', {})
+ srv._send_secured_packet = lambda *a, **k: {'success': False, 'error': 'stub'}
+ buf = io.StringIO()
+ with contextlib.redirect_stderr(buf):
+ ok = srv._ram_send_gp_apdu(FakeServer(), object(), sp, state,
+ 'LOAD (1/2)', '80E8800001AA')
+ finally:
+ srv._build_secured_packet = orig_build
+ srv._send_secured_packet = orig_send
+ self.assertFalse(ok)
+ out = buf.getvalue()
+ self.assertIn('RAM C-APDU (LOAD (1/2)): 80E8800001AA', out)
+ self.assertIn('RAM SECURED-PACKET (LOAD (1/2)): AABB', out)