v3.35.0: EF.SMSP decoded editor (the full SMS-parameter form)
TS 31.102 4.2.27 scope: alpha; TP-DA and TS-SCA with their own length conventions (DA = digits, SCA = octets, 0 = default SMSC) and a TON select; TP-PID; TP-DCS with TS 23.038 hints; the relative TP-VP with the TS 23.040 ladder; a per-parameter "absent (the ME default applies)" state. Unchanged slots are copied verbatim (the live fixture's DCS-absent 00 and present VP FF survive), alphanumeric TON-5 addresses stay read-only, and every checked record is validated before any write. Decoder fixes: the parameter-indicator bits were shifted (SCA on 5, PID on 2, DCS on 3, VP on 4; the spec is DA/SCA/PID/DCS/VP on bits 1-5 with 6-8 reserved, 1 = absent), and the TP-DA length was read as octets instead of digits (TS 23.040 9.1.2.5). Live fixture: ffffffffffffffffffffffffffe9ffffffffffffffffffffffff07919733824009f0ffffffff0000ff (SCA +79332804900, PID 00, DCS absent/00, VP FF) round-trips byte-identically. 1022 Python + 1058 frontend green; simple-v468.
This commit is contained in:
@@ -40,7 +40,7 @@ function extractConst(src, name) {
|
||||
}
|
||||
|
||||
const FNS = [
|
||||
'efBytes', 'efHex', 'efAllFf', 'efRstripFf', 'efUcs2Be', 'efGsm7Octets',
|
||||
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efUcs2Be', 'efGsm7Octets',
|
||||
'efAnnexA', 'efBcdAddress', 'efPlmn', 'efPlmnAct', 'efDecSpn', 'efDecPlmnWact',
|
||||
'efDecAdn', 'efFindDecoder', 'efDecodeBytes', 'efIsRawOnly', 'efTlv',
|
||||
'efPnnText', 'efDecItemsFromHex',
|
||||
@@ -56,14 +56,14 @@ const FNS = [
|
||||
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
|
||||
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
|
||||
'gsm7TextToSeptets', 'gsm7Encode',
|
||||
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir',
|
||||
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
|
||||
];
|
||||
|
||||
let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimFsDecModel = null;\n'
|
||||
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
|
||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_ENCODERS']) {
|
||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) {
|
||||
code += extractConst(html, c);
|
||||
}
|
||||
eval(code);
|
||||
@@ -905,3 +905,204 @@ test('F5: an untouched all-FF AD saves FF through the collected form value', asy
|
||||
assert.strictEqual(stubs.bodies.length, 1);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, 'FF'.repeat(6));
|
||||
});
|
||||
|
||||
// EF.SMSP: the custom (dedicated builder) entry kind.
|
||||
const SMSP_FIX = 'ffffffffffffffffffffffffffe9ffffffffffffffffffffffff07919733824009f0ffffffff0000ff';
|
||||
|
||||
function smspSaveBlock(orig, num, values) {
|
||||
const inputs = { alpha: { value: values.alpha } };
|
||||
for (const key of ['da', 'sca', 'pid', 'dcs', 'vp']) {
|
||||
const p = values.params[key];
|
||||
if (key === 'da' || key === 'sca') {
|
||||
inputs[key + '.number'] = { value: p.number };
|
||||
inputs[key + '.ton'] = { value: String(p.ton) };
|
||||
} else {
|
||||
inputs[key + '.value'] = { value: p.value };
|
||||
}
|
||||
}
|
||||
const rows = {};
|
||||
for (const key of ['da', 'sca', 'pid', 'dcs', 'vp']) {
|
||||
rows[key] = {
|
||||
querySelector: sel => sel === '[data-smsp-absent="' + key + '"]'
|
||||
? { checked: !values.params[key].present } : null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
querySelector: sel => {
|
||||
if (sel === '.pysim-fs-record-cb') return { checked: true };
|
||||
let m = /^\[data-smsp="(.+)"\]$/.exec(sel);
|
||||
if (m) return inputs[m[1]] || null;
|
||||
m = /^\[data-smsp-orig="(.+)"\]$/.exec(sel);
|
||||
return m ? (rows[m[1]] || null) : null;
|
||||
},
|
||||
querySelectorAll: () => [],
|
||||
getAttribute: k => k === 'data-rec' ? String(num) : (k === 'data-orig' ? orig : null),
|
||||
};
|
||||
}
|
||||
|
||||
test('the SMSP custom form renders the parameter rows, presence and hints', () => {
|
||||
const model = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [{ num: 1, data: SMSP_FIX }] });
|
||||
assert.strictEqual(model.mode, 'records');
|
||||
assert.strictEqual(model.records[0].values.params.sca.number, '+79332804900');
|
||||
const out = efDecEditHtml(model);
|
||||
assert.match(out, /class="pysim-dec-items/);
|
||||
assert.match(out, /data-smsp="alpha" value=""/);
|
||||
assert.match(out, /data-smsp-absent="da" checked/);
|
||||
assert.doesNotMatch(out, /data-smsp-absent="sca" checked/);
|
||||
assert.match(out, /data-smsp="sca.number" value="\+79332804900"/);
|
||||
assert.match(out, /data-smsp="pid.value" value="0x00"/);
|
||||
assert.match(out, /data-smsp="dcs.value" value="0x00"/);
|
||||
assert.match(out, /data-smsp="vp.value" value="0xFF"/);
|
||||
// an absent DCS shows the neutral ME-default note (not a decoded hint);
|
||||
// the present VP still shows its relative-VP hint
|
||||
assert.match(out, /data-smsp-hint>absent — the ME default applies/);
|
||||
assert.doesNotMatch(out, /general data coding/);
|
||||
assert.match(out, /63 weeks/);
|
||||
assert.match(out, /TS 23.040 TP-PID \(raw\)/);
|
||||
// a present DCS renders its decoded TS 23.038 hint (the 44-byte vector)
|
||||
const vec = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [{ num: 1,
|
||||
data: '534d5343ffffffffffffffffffffffffe1ffffffffffffffffffffffff0891945197109099f9ffffff0000a9' }] });
|
||||
assert.match(efDecEditHtml(vec), /general data coding, GSM 7-bit, uncompressed/);
|
||||
});
|
||||
|
||||
test('the SMSP custom collect reads the rows back with their original slots', () => {
|
||||
const model = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [{ num: 1, data: SMSP_FIX }] });
|
||||
const block = smspSaveBlock(SMSP_FIX, 1, model.records[0].values);
|
||||
assert.deepStrictEqual(efSmspCollect(block), {
|
||||
alpha: '',
|
||||
params: {
|
||||
da: { present: false, number: '', ton: 1 },
|
||||
sca: { present: true, number: '+79332804900', ton: 1 },
|
||||
pid: { present: true, value: '0x00' },
|
||||
dcs: { present: false, value: '0x00' },
|
||||
vp: { present: true, value: '0xFF' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('an untouched SMSP record saves byte-identically; an edit re-encodes', async () => {
|
||||
const entry = efFindEncoder('EF.SMSP', null);
|
||||
const model = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [{ num: 1, data: SMSP_FIX }] });
|
||||
pysimFsDecModel = model;
|
||||
const out = { querySelector: () => null,
|
||||
querySelectorAll: sel => sel === '.pysim-dec-rec'
|
||||
? [smspSaveBlock(SMSP_FIX, 1, model.records[0].values)] : [] };
|
||||
let stubs = saveStubs({ success: true, sw: '9000' });
|
||||
let status = { textContent: '' };
|
||||
await pysimFsDecSave(null, 'EF.SMSP', status, out, false);
|
||||
assert.strictEqual(stubs.bodies.length, 1);
|
||||
assert.strictEqual(stubs.bodies[0].body.record_nr, 1);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, SMSP_FIX.toUpperCase());
|
||||
// edit the SCA number
|
||||
const edited = JSON.parse(JSON.stringify(model.records[0].values));
|
||||
edited.params.sca.number = '+4915790109999';
|
||||
stubs = saveStubs({ success: true, sw: '9000' });
|
||||
status = { textContent: '' };
|
||||
const out2 = { querySelector: () => null,
|
||||
querySelectorAll: sel => sel === '.pysim-dec-rec' ? [smspSaveBlock(SMSP_FIX, 1, edited)] : [] };
|
||||
await pysimFsDecSave(null, 'EF.SMSP', status, out2, false);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, efEncSmsp(edited, SMSP_FIX));
|
||||
});
|
||||
|
||||
test('the SMSP absent toggle disables the row inputs and the hex hint is live', () => {
|
||||
const inputs = [{ disabled: false }, { disabled: false }];
|
||||
const hint = { textContent: '' };
|
||||
const row = {
|
||||
getAttribute: () => 'dcs',
|
||||
querySelector: sel => sel === '[data-smsp-hint]' ? hint : null,
|
||||
querySelectorAll: sel => sel === '[data-smsp]' ? inputs : [],
|
||||
};
|
||||
const cb = { checked: true, closest: sel => sel === '[data-smsp-orig]' ? row : null };
|
||||
pysimFsSmspToggle(cb);
|
||||
assert.ok(inputs.every(i => i.disabled));
|
||||
pysimFsSmspHexInput({ value: 'F4', closest: sel => sel === '[data-smsp-orig]' ? row : null });
|
||||
assert.strictEqual(hint.textContent, 'data coding, 8-bit data, class 0');
|
||||
const vpHint = { textContent: '' };
|
||||
const vpRow = {
|
||||
getAttribute: () => 'vp',
|
||||
querySelector: sel => sel === '[data-smsp-hint]' ? vpHint : null,
|
||||
};
|
||||
pysimFsSmspHexInput({ value: '90', closest: () => vpRow });
|
||||
assert.strictEqual(vpHint.textContent, '12 h 30 min');
|
||||
});
|
||||
|
||||
test('the SMSP entry is wired into the encoder registry', () => {
|
||||
const entry = efFindEncoder('EF.SMSP', null);
|
||||
assert.strictEqual(entry.fid, '6f42');
|
||||
assert.strictEqual(typeof entry.custom.html, 'function');
|
||||
assert.strictEqual(typeof entry.custom.collect, 'function');
|
||||
assert.strictEqual(typeof entry.custom.model, 'function');
|
||||
assert.strictEqual(efHasEncoder('EF.SMSP', '6f42'), true);
|
||||
});
|
||||
|
||||
const SMSP_ALNUM = 'FF'.repeat(13) + 'FE' + '0750414243444546FFFFFFFF'
|
||||
+ 'FF'.repeat(12) + 'FF' + 'FF' + 'FF';
|
||||
const SMSP_VEC = '534d5343ffffffffffffffffffffffffe1ffffffffffffffffffffffff0891945197109099f9ffffff0000a9';
|
||||
|
||||
test('the SMSP TON 5 alphanumeric slot is read-only in the form', () => {
|
||||
const model = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [{ num: 1, data: SMSP_ALNUM }] });
|
||||
assert.strictEqual(model.records[0].values.params.da.alnum, true);
|
||||
const out = efDecEditHtml(model);
|
||||
assert.doesNotMatch(out, /data-smsp="da.number"/);
|
||||
assert.doesNotMatch(out, /data-smsp="da.ton"/);
|
||||
assert.match(out, /alphanumeric address — not editable here; it is preserved as is/);
|
||||
assert.match(out, /0750414243444546FFFFFFFF/);
|
||||
// still togglable absent, and the encoder keeps it verbatim while present
|
||||
const block = smspSaveBlock(SMSP_ALNUM, 1, model.records[0].values);
|
||||
const vals = efSmspCollect(block);
|
||||
assert.strictEqual(vals.params.da.present, true);
|
||||
assert.strictEqual(efEncSmsp(vals, SMSP_ALNUM), SMSP_ALNUM.toUpperCase());
|
||||
});
|
||||
|
||||
test('the SMSP TON select keeps an unknown current value', () => {
|
||||
const model = efSmspModel(SMSP_FIX);
|
||||
model.params.sca.ton = 3;
|
||||
const html = efSmspHtml(model);
|
||||
assert.match(html, /<option value="3" selected>Current 3<\/option>/);
|
||||
});
|
||||
|
||||
test('a multi-record SMSP save scopes each block to its own record', async () => {
|
||||
const entry = efFindEncoder('EF.SMSP', null);
|
||||
// record 1 untouched, record 2 (the 44-byte vector) with an edited alpha
|
||||
const model = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [
|
||||
{ num: 1, data: SMSP_FIX }, { num: 2, data: SMSP_VEC },
|
||||
] });
|
||||
const edited = JSON.parse(JSON.stringify(model.records[1].values));
|
||||
edited.alpha = 'Two';
|
||||
pysimFsDecModel = model;
|
||||
const blocks = [smspSaveBlock(SMSP_FIX, 1, model.records[0].values),
|
||||
smspSaveBlock(SMSP_VEC, 2, edited)];
|
||||
const out = { querySelector: () => null,
|
||||
querySelectorAll: sel => sel === '.pysim-dec-rec' ? blocks : [] };
|
||||
const stubs = saveStubs({ success: true, sw: '9000' });
|
||||
const status = { textContent: '' };
|
||||
await pysimFsDecSave(null, 'EF.SMSP', status, out, false);
|
||||
assert.strictEqual(stubs.bodies.length, 2);
|
||||
assert.deepStrictEqual(stubs.bodies.map(b => b.body.record_nr), [1, 2]);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, SMSP_FIX.toUpperCase());
|
||||
assert.strictEqual(stubs.bodies[1].body.data, efEncSmsp(edited, SMSP_VEC));
|
||||
assert.notStrictEqual(stubs.bodies[1].body.data, SMSP_VEC.toUpperCase());
|
||||
});
|
||||
|
||||
test('the SMSP DCS hint follows the absent toggle', () => {
|
||||
const hint = { textContent: 'general data coding, GSM 7-bit, uncompressed' };
|
||||
const input = { value: '00' };
|
||||
const cb = { checked: false };
|
||||
const row = {
|
||||
getAttribute: () => 'dcs',
|
||||
querySelector: sel => {
|
||||
if (sel === '[data-smsp-hint]') return hint;
|
||||
if (sel === '[data-smsp-absent]') return cb;
|
||||
if (sel === '[data-smsp="dcs.value"]') return input;
|
||||
return null;
|
||||
},
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
cb.checked = true;
|
||||
cb.closest = sel => sel === '[data-smsp-orig]' ? row : null;
|
||||
pysimFsSmspToggle(cb);
|
||||
assert.strictEqual(hint.textContent, 'absent — the ME default applies');
|
||||
cb.checked = false;
|
||||
pysimFsSmspToggle(cb);
|
||||
assert.strictEqual(hint.textContent, 'general data coding, GSM 7-bit, uncompressed');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user