v3.18.1: the raw envelope action takes the source override too

The events without a semantic builder (MT call, IMS registration, slices, ...)
are network-sourced, but the raw `envelope` action had no `src` override - the
only way to express them was the semantic set.  `envelope` now accepts
`src` (82 terminal / 83 network) exactly like the `event` action; the step
editor exposes it and the authoring guide gets the row.

Also: the guide marks `por` as SCP80-only (the normaliser refuses it
elsewhere) and the semantic-events section names the shared src override.

Tests: 813 frontend + 717 Python; sw cache simple-v382.
This commit is contained in:
2026-10-04 18:06:13 +03:00
parent fb4e4bd7b2
commit 1aa1b44063
9 changed files with 59 additions and 14 deletions
+2 -1
View File
@@ -666,7 +666,8 @@ server resolves it from the preset store, so a run always uses the stored
keysets, TARs and counters. The inline `preset` form stays for external
callers.
**Action steps** (`type: "action"`): `kind` is `envelope` (`event`, `data`),
**Action steps** (`type: "action"`): `kind` is `envelope` (`event`, `data`,
optional `src` - the device-identities source override),
`event` (`event` name or hex + `fields`, optional `src` - the semantic event
download for 0x03/0x0B/0x12/0x1D), `menu-select` (`item_id` 1-255, or `text` +
`mode` `exact`/`contains` with an
+7 -5
View File
@@ -14,7 +14,8 @@ localStorage export imports as-is), and any API client can create or run them
**Format**: server ≥ 3.15.0 (`kvn`, `por` and `files` checks), scripts stored
server-side since 3.16.0, `menu-select` by text and the spec-order TERMINAL
RESPONSE codings since 3.17.0, the semantic `event` action since 3.18.0.
RESPONSE codings since 3.17.0, the semantic `event` action since 3.18.0 and
the `envelope` action's `src` override since 3.18.1.
## Writing a test for an applet
@@ -131,7 +132,7 @@ continue correctly.
| kind | params | notes |
|---|---|---|
| `envelope` | `event` 0–255, `data` hex (optional) | ENVELOPE(Event Download) with raw data |
| `envelope` | `event` 0–255, `data` hex (optional), optional `src` | ENVELOPE(Event Download) with raw data; `src` overrides the device-identities source (`82` terminal / `83` network) |
| `event` | `event` (name or hex), `fields` (object), optional `src` | semantic ENVELOPE(Event Download) for 0x03/0x0B/0x12/0x1D (see below) |
| `menu-select` | `item_id` 1–255 **or** `text` + `mode` `exact`/`contains` (+ `case_sensitive`), not both | ENVELOPE(Menu Selection); the text is resolved against the card's cached menu - refreshed whenever the card sends SET UP MENU, so it follows the applet's install parameters |
| `file-write` | `path`, `data`, `mode` `auto`/`binary`/`record`, `record` 1–255 | UPDATE BINARY/RECORD |
@@ -147,7 +148,8 @@ or file names - e.g. `MF/7F20/6F07`, `ADF.USIM/EF.TEST`.
The server builds the event objects for the events a test needs most; an
event without a builder goes through the raw `envelope` action, whose data hex
the Phone tab's event form shows (**Event data (hex)**, copyable).
the Phone tab's event form shows (**Event data (hex)**, copyable). Both
actions take the same optional `src` device-identities override.
```json
{"type": "action", "kind": "event",
@@ -177,8 +179,8 @@ the default is the terminal, matching the Phone tab's forms.
- `sw` - default `{"mode": "exact", "value": "9000"}` (a `status` action with
`attempts > 1` defaults to the `91??` mask).
- `data` - the response data (for SCP80: the inline PoR packet).
- `por` - `"any"` (default, no check), `"ok"` (`response_status == por_ok`),
`"none"` (no PoR expected).
- `por` (SCP80 actions only) - `"any"` (default, no check), `"ok"`
(`response_status == por_ok`), `"none"` (no PoR expected).
- A plain hex string is exact; a value containing `?` is a mask. **A mask
must have exactly the same length as the actual value** (`?` is a per-nibble
wildcard) - e.g. `91??` matches `9102`, `027100????` matches a 4-byte
+9 -2
View File
@@ -1877,7 +1877,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.18.0';
const SIMPLE_VERSION = '3.18.1';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -7328,6 +7328,7 @@ function testStepRender() {
if (step.kind === 'envelope') {
html += testFormRow('Event type', testFormInput('test-f-event', p.event === undefined ? 3 : p.event, '0..255'));
html += testFormRow('Event data (hex, optional)', testFormInput('test-f-data', p.data, '9B0100'));
html += testFormRow('Source override (hex, optional)', testFormInput('test-f-eventsrc', p.src, '82 terminal / 83 network'));
} else if (step.kind === 'event') {
// The semantic event download: the four events the server builds
// (others go through the raw envelope action with the hex the
@@ -7537,7 +7538,12 @@ function testStepCollect() {
const kind = val('test-step-kind') || step.kind;
step.kind = kind;
const p = {};
if (kind === 'envelope') { p.event = num('test-f-event', 3); p.data = hexNoMask('test-f-data'); }
if (kind === 'envelope') {
p.event = num('test-f-event', 3);
p.data = hexNoMask('test-f-data');
const src = hexNoMask('test-f-eventsrc');
if (src) p.src = src;
}
else if (kind === 'menu-select') {
const text = val('test-f-itemtext');
if (text) {
@@ -21110,6 +21116,7 @@ const LANG_RU = {
'event is required': 'нужен тип события',
'event fields must be an object': 'поля события должны быть объектом',
'Event data (hex)': 'Данные события (hex)',
'Source override (hex, optional)': 'Переопределение источника (hex, опционально)',
'Could not load the test scripts from the server': 'Не удалось загрузить тест-скрипты с сервера',
'Could not save the test script': 'Не удалось сохранить тест-скрипт',
'Could not delete the test script': 'Не удалось удалить тест-скрипт',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v381';
const CACHE = 'simple-v382';
const URLS = [
'index.html',
'help.html',
+10
View File
@@ -209,6 +209,16 @@ test('the menu-select editor collects the item id or the text', () => {
assert.deepStrictEqual(_testEditStep.params, { item_id: 3 });
});
test('the envelope step collects the source override', () => {
_testEditStep = { type: 'action', kind: 'envelope', params: {} };
_testEditChecks = [];
_testEditStepIndex = 0;
fakeForm({ 'test-step-kind': 'envelope', 'test-f-event': '3', 'test-f-data': '9B0100',
'test-f-eventsrc': '83', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-fail': 'error' });
testStepCollect();
assert.deepStrictEqual(_testEditStep.params, { event: 3, data: '9B0100', src: '83' });
});
test('the event step collects the type and the JSON fields', () => {
_testEditStep = { type: 'action', kind: 'event', params: {} };
_testEditChecks = [];
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.18.0"
version = "3.18.1"
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.
+2 -2
View File
@@ -34,7 +34,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.18.0'
VERSION = '3.18.1'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -5953,7 +5953,7 @@ def _test_run_action(server, step, ctx):
if kind == 'envelope':
data, sw = _send_event_download(scc, p['event'],
bytes.fromhex(p['data']) if p['data'] else None,
drain=False)
drain=False, src=p.get('src'))
sent = 'ENVELOPE(Event Download) type=0x%02X' % p['event']
elif kind == 'event':
# The semantic event download: the builder owns the byte layout.
+9 -2
View File
@@ -218,8 +218,15 @@ def _normalise_params(kind, p):
if kind == 'envelope':
if p.get('event') is None:
raise ScriptError('envelope: event is required')
return {'event': _int(p['event'], 'envelope event', 0, 255),
'data': _data_hex(p.get('data'), 'envelope data', allow_empty=True)}
out = {'event': _int(p['event'], 'envelope event', 0, 255),
'data': _data_hex(p.get('data'), 'envelope data', allow_empty=True)}
src = str(p.get('src') or '').strip().upper()
if src:
if not re.fullmatch(r'[0-9A-F]{2}', src):
raise ScriptError('envelope: src must be one hex byte '
'(82 terminal / 83 network)')
out['src'] = src
return out
if kind == 'event':
# The semantic event download: the builder owns the field set and the
# byte layout (events.py mirrors the PWA's forms for the supported
+18
View File
@@ -167,6 +167,15 @@ class TestValidation(unittest.TestCase):
with self.assertRaises(T.ScriptError):
T.normalise_script({'steps': [{'type': 'action', 'kind': 'event',
'params': {'event': 3, 'src': 'zz'}}]}, _resolver)
# the raw envelope action carries the same source override
script = T.normalise_script({'steps': [
{'type': 'action', 'kind': 'envelope',
'params': {'event': 0x00, 'data': '', 'src': '83'}}]}, _resolver)
self.assertEqual(script['steps'][0]['params'],
{'event': 0x00, 'data': '', 'src': '83'})
with self.assertRaises(T.ScriptError):
T.normalise_script({'steps': [{'type': 'action', 'kind': 'envelope',
'params': {'event': 3, 'src': 'zz'}}]}, _resolver)
def test_scp80_keyset_number_and_new_content_checks(self):
# kvn (1..15) selects the preset's keyset; `por`/`files` are expect
@@ -533,6 +542,15 @@ class TestRunnerActions(RunnerTestCase):
self.assertEqual(run['status'], 'ok', run['steps'])
apdu = scc2.sent[0].upper()
self.assertIn('99010B82028381' + 'BF0108', apdu)
# the raw envelope action takes the same source override
scc3 = FakeScc()
scc3.push('80C2', '', '9000')
run = self.run_script(FakeServer(scc3), [
{'type': 'action', 'kind': 'envelope',
'params': {'event': 0x00, 'data': '', 'src': '83'}},
])
self.assertEqual(run['status'], 'ok', run['steps'])
self.assertIn('99010082028381', scc3.sent[0].upper())
def test_scp80_uses_the_preset_and_advances_the_counter(self):
scc = FakeScc()