feat: Test script pill in the Phone simulator (v3.5.11)
- Phone simulator tab: a fourth pill **Test script** - script list (localStorage `simple_tests`) with New/Clone/Delete/Export/Import, a form-based step editor (modal) and a run panel. - Actions: ENVELOPE (Event Download), Menu Selection, UPDATE/READ file by path, raw APDU, SCP80 (C-APDU or a pre-built packet, per-step TAR/SPI1/ SPI2 overrides; the preset is matched by the equipped card's ICCID and the final counter is written back) and STATUS with an optional poll. Every action has SW/data checks (exact or ?-mask) and a PoR check for SCP80, with an error (stops) / warning (continues) fail level. - Expectations: FETCH the command announced by the previous step, verify type/qualifier/text/item/raw and answer with the scripted TERMINAL RESPONSE (result, item id, text, extra TLVs); templates for STK menu browsing and an applet via SCP80. - Run panel: progress banner (plus a marker on the pill), per-step OK/Warning/Error badges with expected vs actual values, sent/received hex, decoded commands and the SCP80 counter; 500 ms polling while a run is active, and a running script disables other card controls (data-needs gate; the server answers 409 to other card endpoints). - Help EN/RU (8.9 Test script), AGENTS; version 3.5.11; sw simple-v264. 579 frontend / 467 Python green.
This commit is contained in:
@@ -544,6 +544,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mb-10">
|
<section class="mb-10">
|
||||||
|
<h3 id="test-script" class="text-lg font-medium mb-2">8.9 Тест-скрипт</h3>
|
||||||
|
<p class="text-sm mb-2"><strong>Тест-скрипт</strong> — упорядоченный список <strong>действий</strong> и <strong>ожиданий</strong> проактивных команд, выполняемый на подключённой карте на стороне сервера. Пока запуск активен, карта принадлежит скрипту: остальные операции с картой отклоняются (HTTP 409), фоновый опрос STATUS приостанавливается; отчёт о запуске хранится в памяти (в том числе после перезагрузки страницы) до нажатия <strong>Очистить отчёт</strong>. Скрипты хранятся в localStorage (<code class="font-mono text-sm">simple_tests</code>) и экспортируются/импортируются как JSON.</p>
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
|
<li><strong>Действия</strong> — <em>Envelope</em> (Event Download: тип события + данные), <em>Menu Selection</em> (item id 1–255), <em>UPDATE/READ файла</em> (путь + режим; чтение проверяет данные файла), <em>сырой APDU</em> (без авто-обработчика), <em>SCP80</em> (C-APDU или готовый защищённый пакет, опциональные переопределения TAR/SPI1/SPI2; KIc/KID и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты, а счётчик записывается обратно после запуска) и <em>STATUS</em> (при <em>попытках</em> > 1 опрашивает, пока карта не объявит команду). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой <code class="font-mono text-sm">?</code> — <code class="font-mono text-sm">91??</code>) и проверка PoR для SCP80 (<em>любой / без PoR / por_ok</em>).</li>
|
||||||
|
<li><strong>Ожидания</strong> — карта должна объявить проактивную команду на предыдущем шаге (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); <code class="font-mono text-sm">9000</code> означает «команды нет» и считается ошибкой — добавьте действие STATUS, если апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое (текст / пункт / raw), а <strong>TERMINAL RESPONSE</strong> (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.</li>
|
||||||
|
<li><strong>Уровни ошибок</strong> — несовпадение помечает шаг как <em>Ошибка</em> (скрипт останавливается) или <em>Предупреждение</em> (продолжает); каждая проверка показывается с ожидаемым и фактическим значением.</li>
|
||||||
|
<li><strong>Шаблоны</strong> — Empty, <em>Обзор меню STK</em>, <em>Апплет через SCP80</em>; редактор шагов — формный, отдельные скрипты экспортируются/импортируются как JSON.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">9. Установка сервера</h2>
|
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">9. Установка сервера</h2>
|
||||||
<p class="mb-3">Для работы с картой (вкладка «Картридер», «Симулятор телефона», доставка OTA) нужен локальный <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> — встроенный в SIMple HTTP-сервер, оборачивающий pySim, работающий с ридером через PC/SC или serial и раздающий сам PWA (откройте <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
|
<p class="mb-3">Для работы с картой (вкладка «Картридер», «Симулятор телефона», доставка OTA) нужен локальный <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> — встроенный в SIMple HTTP-сервер, оборачивающий pySim, работающий с ридером через PC/SC или serial и раздающий сам PWA (откройте <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
|
||||||
|
|
||||||
|
|||||||
@@ -543,6 +543,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mb-10">
|
<section class="mb-10">
|
||||||
|
<h3 id="test-script" class="text-lg font-medium mb-2">8.9 Test script</h3>
|
||||||
|
<p class="text-sm mb-2">A <strong>test script</strong> is an ordered list of <strong>actions</strong> and proactive-command <strong>expectations</strong> executed server-side on the equipped card. While a run is active the card belongs to the script: other card actions are refused (HTTP 409) and background STATUS polling is paused; the run report is kept in memory (also after reloading the page) until <strong>Clear report</strong>. Scripts live in localStorage (<code class="font-mono text-sm">simple_tests</code>) and export/import as JSON.</p>
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
|
<li><strong>Actions</strong> — <em>Envelope</em> (Event Download: event type + data), <em>Menu Selection</em> (item id 1–255), <em>UPDATE/READ file</em> (path + mode; reads verify the file data), <em>raw APDU</em> (no auto-handler), <em>SCP80</em> (C-APDU or a pre-built secured packet, optional TAR/SPI1/SPI2 overrides; KIc/KID and the counter come from the card preset matched by the equipped card's ICCID, and the counter is written back to it after the run) and <em>STATUS</em> (with <em>attempts</em> > 1 it polls until the card announces a command). Every action has a response check: expected SW and data (exact, or a mask with <code class="font-mono text-sm">?</code> nibble wildcards — <code class="font-mono text-sm">91??</code>) plus a PoR check for SCP80 (<em>any / no PoR / por_ok</em>).</li>
|
||||||
|
<li><strong>Expectations</strong> — the card must have announced a proactive command in the previous step (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); a <code class="font-mono text-sm">9000</code> means “no command” and is an error — add a STATUS action when the applet delivers on poll. The command type, qualifier and contents (text / item / raw checks) are verified, and the scripted <strong>TERMINAL RESPONSE</strong> (result, item identifier, text, extra TLVs) is part of the instruction.</li>
|
||||||
|
<li><strong>Fail levels</strong> — a mismatch flags the step as <em>Error</em> (terminates the script) or <em>Warning</em> (continues); each check is reported with expected vs actual values.</li>
|
||||||
|
<li><strong>Templates</strong> — Empty, <em>STK menu browsing</em>, <em>Applet via SCP80</em>; the step editor is form-based and individual scripts export/import as JSON.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">9. Server installation</h2>
|
<h2 id="server" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">9. Server installation</h2>
|
||||||
<p class="mb-3">Live card operations (Card reader tab, Phone simulator, OTA delivery) require the local <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> — a small HTTP server bundled with SIMple that wraps pySim, talks to the reader over PC/SC or serial, and also serves the PWA itself (open <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
|
<p class="mb-3">Live card operations (Card reader tab, Phone simulator, OTA delivery) require the local <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> — a small HTTP server bundled with SIMple that wraps pySim, talks to the reader over PC/SC or serial, and also serves the PWA itself (open <code class="font-mono text-sm">http://127.0.0.1:8080</code>).</p>
|
||||||
|
|
||||||
|
|||||||
+933
-4
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v263';
|
const CACHE = 'simple-v264';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ code += extractFunc(html, 'cardsScp80Complete') + '\n';
|
|||||||
code += extractFunc(html, 'cardsScp81Complete') + '\n';
|
code += extractFunc(html, 'cardsScp81Complete') + '\n';
|
||||||
code += extractFunc(html, 'cardsMatchedPreset') + '\n';
|
code += extractFunc(html, 'cardsMatchedPreset') + '\n';
|
||||||
code += 'globalThis.t = s => s;\n';
|
code += 'globalThis.t = s => s;\n';
|
||||||
|
code += 'globalThis.testRunActive = () => false;\n';
|
||||||
eval(code);
|
eval(code);
|
||||||
|
|
||||||
function fakeEl() {
|
function fakeEl() {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const code = extractFunc(html, 'phoneSwitchSubtab') + '\n' +
|
|||||||
'globalThis.pysimPliRender = () => { globalThis._pli = (globalThis._pli || 0) + 1; };\n' +
|
'globalThis.pysimPliRender = () => { globalThis._pli = (globalThis._pli || 0) + 1; };\n' +
|
||||||
'globalThis.tpRefresh = () => { globalThis._tp = (globalThis._tp || 0) + 1; };\n' +
|
'globalThis.tpRefresh = () => { globalThis._tp = (globalThis._tp || 0) + 1; };\n' +
|
||||||
'globalThis.esimFetchAll = () => { globalThis._esim = (globalThis._esim || 0) + 1; };\n' +
|
'globalThis.esimFetchAll = () => { globalThis._esim = (globalThis._esim || 0) + 1; };\n' +
|
||||||
|
'globalThis.testInit = () => { globalThis._test = (globalThis._test || 0) + 1; };\n' +
|
||||||
'globalThis.netStateFetch = () => { globalThis._netstate = (globalThis._netstate || 0) + 1; };\n';
|
'globalThis.netStateFetch = () => { globalThis._netstate = (globalThis._netstate || 0) + 1; };\n';
|
||||||
eval(code);
|
eval(code);
|
||||||
|
|
||||||
@@ -46,11 +47,13 @@ function setup() {
|
|||||||
{ dataset: { phoneSub: 'phone' }, classList: makeClassList() },
|
{ dataset: { phoneSub: 'phone' }, classList: makeClassList() },
|
||||||
{ dataset: { phoneSub: 'tr' }, classList: makeClassList() },
|
{ dataset: { phoneSub: 'tr' }, classList: makeClassList() },
|
||||||
{ dataset: { phoneSub: 'esim' }, classList: makeClassList() },
|
{ dataset: { phoneSub: 'esim' }, classList: makeClassList() },
|
||||||
|
{ dataset: { phoneSub: 'test' }, classList: makeClassList() },
|
||||||
];
|
];
|
||||||
const panels = {
|
const panels = {
|
||||||
'phone-sub-phone': { classList: makeClassList() },
|
'phone-sub-phone': { classList: makeClassList() },
|
||||||
'phone-sub-tr': { classList: makeClassList() },
|
'phone-sub-tr': { classList: makeClassList() },
|
||||||
'phone-sub-esim': { classList: makeClassList() },
|
'phone-sub-esim': { classList: makeClassList() },
|
||||||
|
'phone-sub-test': { classList: makeClassList() },
|
||||||
};
|
};
|
||||||
globalThis.document = {
|
globalThis.document = {
|
||||||
querySelectorAll: sel => (sel === '.phone-subtab' ? buttons : []),
|
querySelectorAll: sel => (sel === '.phone-subtab' ? buttons : []),
|
||||||
@@ -59,6 +62,7 @@ function setup() {
|
|||||||
globalThis._anchor = null;
|
globalThis._anchor = null;
|
||||||
globalThis._stk = globalThis._events = globalThis._log = globalThis._poll = globalThis._pli = globalThis._tp = 0;
|
globalThis._stk = globalThis._events = globalThis._log = globalThis._poll = globalThis._pli = globalThis._tp = 0;
|
||||||
globalThis._esim = globalThis._netstate = 0;
|
globalThis._esim = globalThis._netstate = 0;
|
||||||
|
globalThis._test = 0;
|
||||||
return { buttons, panels };
|
return { buttons, panels };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||||
|
|
||||||
|
function extractBlock(startMarker, endMarker) {
|
||||||
|
const start = html.indexOf(startMarker);
|
||||||
|
const end = html.indexOf(endMarker, start);
|
||||||
|
if (start < 0 || end < 0) throw new Error('block not found: ' + startMarker);
|
||||||
|
return html.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFunc(src, name, asyncFn) {
|
||||||
|
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||||
|
const m = re.exec(src);
|
||||||
|
if (!m) throw new Error('function ' + name + ' not found');
|
||||||
|
let i = m.index + m[0].length - 1;
|
||||||
|
let depth = 0;
|
||||||
|
for (; i < src.length; i++) {
|
||||||
|
if (src[i] === '{') depth++;
|
||||||
|
else if (src[i] === '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
eval(extractBlock('const CMD_NAMES = {', 'function cmdQualifierShort').replace(/^const /gm, 'var '));
|
||||||
|
eval(extractBlock('const TEST_ACTION_KINDS = [', 'let _testScripts').replace(/^const /gm, 'var '));
|
||||||
|
eval(extractFunc(html, 'testTemplate'));
|
||||||
|
eval(extractFunc(html, 'testScriptProblem'));
|
||||||
|
eval(extractFunc(html, 'testCheckSummary'));
|
||||||
|
eval(extractFunc(html, 'testStepSummary'));
|
||||||
|
eval(extractFunc(html, 'testCommandOptions'));
|
||||||
|
globalThis.t = s => s;
|
||||||
|
|
||||||
|
test('the templates contain usable steps', () => {
|
||||||
|
const menu = testTemplate('menu');
|
||||||
|
assert.strictEqual(menu.steps.length, 3);
|
||||||
|
assert.strictEqual(menu.steps[0].kind, 'menu-select');
|
||||||
|
assert.deepStrictEqual(menu.steps[0].check.sw, { mode: 'mask', value: '91??' });
|
||||||
|
assert.strictEqual(menu.steps[1].command, 'SELECT ITEM');
|
||||||
|
assert.strictEqual(menu.steps[1].checks[0].kind, 'item');
|
||||||
|
assert.strictEqual(menu.steps[2].command, 'DISPLAY TEXT');
|
||||||
|
const scp80 = testTemplate('scp80');
|
||||||
|
assert.strictEqual(scp80.steps[0].kind, 'scp80');
|
||||||
|
assert.strictEqual(scp80.steps[0].check.por, 'none');
|
||||||
|
assert.ok(scp80.steps[0].params.tar);
|
||||||
|
assert.strictEqual(testTemplate('empty').steps.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('testScriptProblem accepts good scripts and names bad ones', () => {
|
||||||
|
assert.strictEqual(testScriptProblem(testTemplate('menu')), '');
|
||||||
|
assert.strictEqual(testScriptProblem(testTemplate('scp80')), '');
|
||||||
|
assert.match(testScriptProblem(null), /object/);
|
||||||
|
assert.match(testScriptProblem({ steps: [] }), /no steps/);
|
||||||
|
assert.match(testScriptProblem({ steps: [{ type: 'nope' }] }), /action or an expectation/);
|
||||||
|
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'nope' }] }), /unknown action kind/);
|
||||||
|
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'apdu', params: {} }] }), /APDU is empty/);
|
||||||
|
assert.match(testScriptProblem({
|
||||||
|
steps: [{ type: 'action', kind: 'scp80', params: { apdu: '' } }] }), /SCP80/);
|
||||||
|
assert.match(testScriptProblem({
|
||||||
|
steps: [{ type: 'action', kind: 'menu-select', params: { item_id: 0 } }] }), /item id/);
|
||||||
|
assert.match(testScriptProblem({
|
||||||
|
steps: [{ type: 'action', kind: 'file-read', params: {} }] }), /file path/);
|
||||||
|
assert.match(testScriptProblem({ steps: [{ type: 'expect' }] }), /command is required/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('testStepSummary renders actions', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
testStepSummary({ type: 'action', kind: 'menu-select', params: { item_id: 3 },
|
||||||
|
check: { sw: { mode: 'mask', value: '91??' } } }),
|
||||||
|
'ENVELOPE(Menu Selection) item=3 · SW ~91??');
|
||||||
|
assert.strictEqual(
|
||||||
|
testStepSummary({ type: 'action', kind: 'apdu', params: { apdu: '00A4' } }),
|
||||||
|
'APDU 00A4');
|
||||||
|
assert.strictEqual(
|
||||||
|
testStepSummary({ type: 'action', kind: 'scp80', params: { apdu: '80E2', tar: 'B00000' },
|
||||||
|
check: { sw: { mode: 'mask', value: '91??' }, por: 'none' } }),
|
||||||
|
'SCP80 80E2 TAR=B00000 · SW ~91??, PoR none');
|
||||||
|
assert.strictEqual(
|
||||||
|
testStepSummary({ type: 'action', kind: 'status', params: { attempts: 5 } }),
|
||||||
|
'STATUS x5 (poll)');
|
||||||
|
assert.strictEqual(
|
||||||
|
testStepSummary({ type: 'action', kind: 'file-read', params: { path: 'MF/2FE2' } }),
|
||||||
|
'READ MF/2FE2');
|
||||||
|
assert.ok(testStepSummary({ type: 'action', kind: 'status', params: {},
|
||||||
|
check: { sw: { mode: 'exact', value: '9000' } }, on_fail: 'warning' }).includes('⚠'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('testStepSummary renders expectations with checks and the response', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
testStepSummary({ type: 'expect', command: 'SELECT ITEM',
|
||||||
|
checks: [{ kind: 'item', id: 1, text: 'test', mode: 'contains' }],
|
||||||
|
respond: { result: 'ok', item_id: 1 } }),
|
||||||
|
'EXPECT SELECT ITEM · item #1 ~"test" · TR item=1');
|
||||||
|
assert.strictEqual(
|
||||||
|
testStepSummary({ type: 'expect', command: 'DISPLAY TEXT',
|
||||||
|
checks: [{ kind: 'text', mode: 'contains', value: 'hello' }], respond: { result: 'ok' } }),
|
||||||
|
'EXPECT DISPLAY TEXT · text ~"hello" · TR ok');
|
||||||
|
assert.ok(testStepSummary({ type: 'expect', command: 'DISPLAY TEXT',
|
||||||
|
checks: [{ kind: 'text', value: 'x' }], respond: { result: 'cancel' } }).includes('TR cancel'));
|
||||||
|
assert.ok(testStepSummary({ type: 'expect', command: 'GET INPUT', respond: { result: 'ok', text: 'hi' } })
|
||||||
|
.includes('text="hi"'));
|
||||||
|
assert.strictEqual(testCheckSummary({ kind: 'raw', mode: 'mask', value: 'AA??' }), 'raw ~AA??');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('testCommandOptions covers the proactive names and keeps custom values', () => {
|
||||||
|
const opts = testCommandOptions('DISPLAY TEXT');
|
||||||
|
assert.ok(opts.some(o => o.v === 'ANY'));
|
||||||
|
assert.ok(opts.some(o => o.v === 'DISPLAY TEXT'));
|
||||||
|
assert.ok(opts.some(o => o.v === 'SELECT ITEM'));
|
||||||
|
const custom = testCommandOptions('0x74');
|
||||||
|
assert.ok(custom.some(o => o.v === '0x74'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the Phone simulator hosts the Test script pill and its wiring', () => {
|
||||||
|
assert.match(html, /data-phone-sub="test"[^>]*data-l10n="Test script"/);
|
||||||
|
assert.match(html, /id="phone-sub-test"/);
|
||||||
|
assert.match(html, /\['phone', 'tr', 'esim', 'test'\]/);
|
||||||
|
assert.match(html, /else if \(name === 'test'\) \{\s*testInit\(\);/);
|
||||||
|
assert.match(html, /test: 'test-script'/);
|
||||||
|
assert.match(html, /testRunActive\(\) && el\.closest && !el\.closest\('#phone-sub-test'\)/);
|
||||||
|
assert.match(html, /\/api\/test\/run/);
|
||||||
|
assert.match(html, /\/api\/test\/status/);
|
||||||
|
assert.match(html, /id="test-step-modal"/);
|
||||||
|
});
|
||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.5.10"
|
version = "3.5.11"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.5.10'
|
VERSION = '3.5.11'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user