ui: group the TCP request with the channel/link triggers; identification packet as a same-message follow-up

The Push commands pill now has three sub-pills: Trigger (Push SMS), Store
(SD admin params) and Channel / link trigger, which builds all three TS 102 226
§9 establishment requests - 01 BIP channel opening (OPEN CHANNEL), 02 CAT_TP
link, 03 TCP connection - with one shared destination-port field and
per-request field visibility.  The TCP parameters are the OPEN CHANNEL TCP
set (BIP, or a direct IP connection per TS 102 483 where supported), so 03
belongs with the other channel/link requests.

The identification packet (04) is not a trigger: it presupposes an already
open TCP channel and only makes sense as a follow-up in the same message, so
it is no longer a standalone request.  The TCP request carries an "Also send
the identification packet (04) in the same message" checkbox (optional data,
ICCID when empty): the preview then shows both commands (a command string sent
in one secured packet) and → Expanded Script appends one 22 Command TLV per
command.  pushSectionApdus() returns the APDU list; 04 stays available in the
RAM/GP chain's PUSH row for script sequences.

Help 2.7 EN/RU, READMEs and AGENTS describe the three sub-pills and the BIP vs
direct-IP nuance; sw cache -> simple-v249; version stays 3.5.0.

html.test.js also asserts that the document is complete (ends with </html>,
every <script> closed, the inline script parses and defines cApduSwitchSubtab):
a truncated index.html made the browser fail to parse the inline script, so
every onclick handler reported "function is not defined" while the Node tests
still passed.
This commit is contained in:
2026-09-23 22:11:30 +03:00
parent 731cb53105
commit 9cbf301f09
8 changed files with 162 additions and 134 deletions
+22 -14
View File
@@ -31,7 +31,7 @@ function extractFunc(src, name) {
const FNS = ['berLenStr', 'buildApdu', 'buildSelect', 'escHtml', 'esc', 'chainInit',
'chainKind', 'chainIsEmbedded', 'chainCommands', 'chainBuildRowHex',
'chainSimBuildRowHex', 'chainRamBuildRowHex', 'chainPushData', '_hotaAsciiHex',
'chainApduList', 'chainBuildFcp', 'pushSectionApdu'];
'chainApduList', 'chainBuildFcp', 'pushSectionApdus'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['CHAIN_CMDS_SIM', 'CHAIN_CMDS_USIM', 'CHAIN_CMDS_RAM']) {
@@ -227,22 +227,30 @@ test('CREATE FILE FCP template skeleton (TS 102 222 table 4)', () => {
assert.strictEqual(r4.fields.fcp, '620883026F0780020010');
});
test('pushSectionApdu builds the guided §9 push commands (Remote APDU pill)', () => {
test('pushSectionApdus builds the guided §9 channel/link requests', () => {
// BIP channel opening: OPEN CHANNEL TLVs are optional.
assert.strictEqual(pushSectionApdu('bip', { request: '01', pushData: '350103' }), '80EC010103350103');
assert.strictEqual(pushSectionApdu('bip', { request: '01', pushData: '' }), '80EC0101');
assert.deepStrictEqual(pushSectionApdus('link', { request: '01', pushData: '350103' }), ['80EC010103350103']);
assert.deepStrictEqual(pushSectionApdus('link', { request: '01', pushData: '' }), ['80EC0101']);
// CAT_TP: the destination port is mandatory (9.2.2).
assert.strictEqual(pushSectionApdu('bip', { request: '02', pushPort: '1F90' }), '80EC0102053C03001F90');
assert.strictEqual(pushSectionApdu('bip', { request: '02', pushPort: '' }), '');
assert.deepStrictEqual(pushSectionApdus('link', { request: '02', pushPort: '1F90' }), ['80EC0102053C03001F90']);
assert.deepStrictEqual(pushSectionApdus('link', { request: '02', pushPort: '' }), []);
// TCP: port and destination address are mandatory (9.2.3).
assert.strictEqual(
pushSectionApdu('tcp', { request: '03', pushPort: '0050', pushAddr: '210A000001', pushApn: 'internet' }),
'80EC010319' + '3501033C030200503E05210A0000014708696E7465726E6574');
assert.strictEqual(pushSectionApdu('tcp', { request: '03', pushPort: '0050' }), '');
assert.strictEqual(pushSectionApdu('tcp', { request: '03', pushAddr: '210A000001' }), '');
// Identification packet: optional data, ICCID is used when absent (9.2.4).
assert.strictEqual(pushSectionApdu('tcp', { request: '04', pushIdent: '0102' }), '80EC01040436020102');
assert.strictEqual(pushSectionApdu('tcp', { request: '04', pushIdent: '' }), '80EC0104');
const tcp = '80EC010319' + '3501033C030200503E05210A0000014708696E7465726E6574';
assert.deepStrictEqual(
pushSectionApdus('link', { request: '03', pushPort: '0050', pushAddr: '210A000001', pushApn: 'internet' }),
[tcp]);
assert.deepStrictEqual(pushSectionApdus('link', { request: '03', pushPort: '0050' }), []);
assert.deepStrictEqual(pushSectionApdus('link', { request: '03', pushAddr: '210A000001' }), []);
// The identification packet (9.1.5) rides in the same message right after
// the TCP request; its data is optional (ICCID when absent).
assert.deepStrictEqual(
pushSectionApdus('link', { request: '03', pushPort: '0050', pushAddr: '210A000001', pushApn: 'internet', withIdp: true }),
[tcp, '80EC0104']);
assert.deepStrictEqual(
pushSectionApdus('link', { request: '03', pushPort: '0050', pushAddr: '210A000001', pushApn: 'internet', withIdp: true, idpIdent: '0102' }),
[tcp, '80EC01040436020102']);
// The follow-up only applies to the TCP request.
assert.deepStrictEqual(pushSectionApdus('link', { request: '01', pushData: '', withIdp: true }), ['80EC0101']);
});
test('chainApduList drops GET RESPONSE and splits multi-APDU rows', () => {