diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95b47d3..1e08ee3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,28 @@ every change carries a descriptive commit message.
## [Unreleased]
+## [3.35.0] — 2026-10-23
+
+### Added
+
+- **EF.SMSP decoded editor** — the full SMS-parameter form (TS 31.102 §4.2.27): alpha,
+ TP-Destination Address and TS-Service Centre Address (each with its own length
+ convention and a TON select; an empty SCA means the default SMSC), TP-PID, TP-DCS and
+ the relative TP-VP — every parameter with an *absent (the ME default applies)* toggle.
+ Live hints decode the DCS (TS 23.038 §4: coding group, alphabet, class, compression,
+ message-waiting) and the relative validity period (TS 23.040 §9.2.3.12.1). Unchanged
+ parameters keep their exact bytes (a card quirk such as a DCS marked absent with byte
+ `00` survives), alphanumeric TON addresses are preserved read-only, and all checked
+ records are validated before anything is written.
+
+### Fixed
+
+- `efDecSmsp` mapped the parameter indicators shifted (SCA on bit 5, PID on bit 2, DCS
+ on bit 3, VP on bit 4); the spec order is DA/SCA/PID/DCS/VP on bits 1–5 with bits 6–8
+ reserved — a record with a mixed indicator (e.g. `E9`) showed DCS and hid VP.
+- The read-only EF.SMSP view decoded the TP-DA length as octets, although it is the digit
+ count (TS 23.040 §9.1.2.5); a slot padded with `00` gained phantom digits.
+
## [3.34.0] — 2026-10-23
### Added
diff --git a/README.md b/README.md
index e5ab468..213fe3c 100644
--- a/README.md
+++ b/README.md
@@ -595,7 +595,7 @@ Browse the UICC filesystem in a tree view. Files are shown with names, FIDs, and
- Entries are grouped with DFs above EFs and sorted by **FID** or symbolic **Name** (pills pinned above the tree together with **Probe all files**, remembered in `localStorage`)
- **Read raw** / **Read decoded** — read the selected file as a hex dump or as a decoded field table (client-side decoders for IMSI, ICCID, SPN, PLMN lists, LOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the highlighted pill is the current view and clicking either pill (re-)reads the file. The decoded table is built in the browser from the same raw bytes (no server-side decode); **Read decoded** is disabled when the client-side decoders do not cover the file (resolved by name/FID before reading)
- **Edit raw** — edit the raw hex data and **Save** to write back (or **Cancel**); transparent files use a textarea, record files one input per record with selection checkboxes. **Edit raw** switches to the hex view first and reads the file if needed
-- **Edit decoded** — edit through the file's own decoded form (client-side encoders; written via the normal update path): **EF.SPN**, **EF.PNN**, the PLMN lists **PLMNsel / FPLMN / EHPLMN** and **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, the dialling-number files **ADN / FDN / SDN / MSISDN / LND / BDN**, the service tables **UST / IST / EST** (**SST** stays read-only until the SIM allocated/activated editor exists), **ACC**, **PHASE**, **HPPLMN**, **AD**, **ICCID / IMSI** (behind a confirmation), and the cell-broadcast lists **CBMI / CBMID / CBMIR**. Transparent files get one form (PLMN rows and PNN items with add/remove); record files get one form per record with a checkbox that is auto-checked when a field changes, so only edited records are written. **Save** validates every selected record before writing; extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number continued in an extension file is refused. The shape survey and the planned coverage are in `docs/decoded-editing.md`
+- **Edit decoded** — edit through the file's own decoded form (client-side encoders; written via the normal update path): **EF.SPN**, **EF.PNN**, the PLMN lists **PLMNsel / FPLMN / EHPLMN** and **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, the dialling-number files **ADN / FDN / SDN / MSISDN / LND / BDN**, the service tables **UST / IST / EST** (**SST** stays read-only until the SIM allocated/activated editor exists), **ACC**, **PHASE**, **HPPLMN**, **AD**, **ICCID / IMSI** (behind a confirmation), the cell-broadcast lists **CBMI / CBMID / CBMIR**, and **EF.SMSP** (the full SMS-parameter form: alpha, TP-DA/TS-SCA with their addressing rules and TON, TP-PID, TP-DCS and relative TP-VP, each with an absent/default toggle and live DCS/validity hints). Transparent files get one form (PLMN rows and PNN items with add/remove); record files get one form per record with a checkbox that is auto-checked when a field changes, so only edited records are written. **Save** validates every selected record before writing; extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number continued in an extension file is refused. The shape survey and the planned coverage are in `docs/decoded-editing.md`
- **Probe all files** — walks the whole tree (incl. custom files), marks every entry present/absent with *N / total* progress, stoppable, and ends with a summary; browsing itself stays lazy. Missing files are shown in red (✗); a present but empty DF shows `(empty)`
- **ADM** — files that need the administrator PIN fail with `6982`/`9804`; when the matching card preset (same ICCID) carries an ADM key, a **Verify ADM** button appears next to the error and the header badge (`ADM ✓/✗ ⚿`) becomes clickable. Every wrong key consumes an attempt (the remaining attempts are shown and a retry asks for confirmation); a blocked ADM needs the card's unblock key
diff --git a/README_RUS.md b/README_RUS.md
index 293b66c..9b487c3 100644
--- a/README_RUS.md
+++ b/README_RUS.md
@@ -570,7 +570,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
- Элементы сгруппированы (DF выше EF) и отсортированы по **FID** или символьному **имени** (пиллы закреплены над деревом вместе с **«Проверить все файлы»**, выбор сохраняется в `localStorage`)
- **Read raw** / **Read decoded** — чтение выбранного файла как hex-дампа или как таблицы декодированных полей (клиентские декодеры: IMSI, ICCID, SPN, списки PLMN, LOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); подсвеченная пилла — текущий вид, клик по любой из них перечитывает файл. таблица декодируется в браузере из тех же сырых байт (без серверного декодирования); **Read decoded** отключается, когда клиентские декодеры не покрывают файл (определяется по имени/FID до чтения)
- **Edit raw** — редактирование сырых hex-данных и **Save** для записи (или **Cancel**); для transparent-файлов — textarea, для record-файлов — по полю на запись с флажками выбора. **Edit raw** сначала переключается на hex и при необходимости читает файл
-- **Edit decoded** — редактирование в собственной декодированной форме файла (клиентские кодировщики; запись обычным путём): **EF.SPN**, **EF.PNN**, списки PLMN **PLMNsel / FPLMN / EHPLMN** и **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, файлы номеров **ADN / FDN / SDN / MSISDN / LND / BDN**, таблицы сервисов **UST / IST / EST** (**SST** только для чтения до появления редактора allocated/activated), **ACC**, **PHASE**, **HPPLMN**, **AD**, **ICCID / IMSI** (с подтверждением) и списки cell broadcast **CBMI / CBMID / CBMIR**. Для transparent-файла — одна форма (строки PLMN и элементы PNN с добавлением/удалением); для record-файла — форма на запись с флажком, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. **Save** проверяет все выбранные записи до записи; указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется. Обзор форматов и план охвата — в `docs/decoded-editing.md`
+- **Edit decoded** — редактирование в собственной декодированной форме файла (клиентские кодировщики; запись обычным путём): **EF.SPN**, **EF.PNN**, списки PLMN **PLMNsel / FPLMN / EHPLMN** и **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, файлы номеров **ADN / FDN / SDN / MSISDN / LND / BDN**, таблицы сервисов **UST / IST / EST** (**SST** только для чтения до появления редактора allocated/activated), **ACC**, **PHASE**, **HPPLMN**, **AD**, **ICCID / IMSI** (с подтверждением) списки cell broadcast **CBMI / CBMID / CBMIR** и **EF.SMSP** (полная форма SMS-параметров: alpha, TP-DA/TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого — флажок «отсутствует/по умолчанию» и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (строки PLMN и элементы PNN с добавлением/удалением); для record-файла — форма на запись с флажком, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. **Save** проверяет все выбранные записи до записи; указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется. Обзор форматов и план охвата — в `docs/decoded-editing.md`
- **Проверить все файлы** — обход всего дерева (включая пользовательские) с пометкой «есть/нет», прогрессом *N / всего*, возможностью остановки и сводкой в конце; сам просмотр остаётся ленивым. Отсутствующие файлы показаны красным (✗); существующий пустой DF — `(пусто)`
- **ADM** — файлы, требующие администраторский PIN, возвращают `6982`/`9804`; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка **«Проверить ADM»**, а значок в заголовке (`ADM ✓/✗ ⚿`) становится кликабельным. Каждый неверный ключ расходует попытку (остаток показывается, повторная попытка требует подтверждения); заблокированный ADM требует ключа разблокировки карты
diff --git a/docs/decoded-editing.md b/docs/decoded-editing.md
index 78101f8..3468701 100644
--- a/docs/decoded-editing.md
+++ b/docs/decoded-editing.md
@@ -1,7 +1,8 @@
# Decoded editing — file-format survey and design
-Status: design note for the **Edit decoded** feature. Waves 1 and 2 shipped in
-v3.33.0 and v3.34.0; decoded editing for EF.DIR/EF.ARR/EF.SMS/EF.PBR is
+Status: design note for the **Edit decoded** feature. Waves 1–3 shipped in
+v3.33.0 (SPN/PLMN/ADN/PNN), v3.34.0 (service tables/scalars/PLMN lists/BCD/CB)
+and v3.35.0 (EF.SMSP); decoded editing for EF.DIR/EF.ARR/EF.SMS/EF.PBR is
**postponed by decision** (their decoded view is enough). It classifies
the EFs the workbench knows (the client-side decoder registry) by the spec's
own structure and content coding, assigns each an editing *shape*, and sets the
@@ -108,7 +109,7 @@ editing candidates.
| EF.ECC (SIM) | 6FB7 | TS 51.011 10.3.27 | 3n | emergency codes (TR) | S6 | 3.35 |
| EF.ECC (USIM) | 6FB7 | TS 31.102 4.2.21 | X+4 | code(3) + alpha(X) + category(1) | S7 | 3.35 |
| EF.EXT1–EXT5 | 6F4A–6F55 | TS 31.102 4.4.2.4 / 4.2.30/31/45/37 | 13 | type-dependent: additional data / subaddress | S9 | 3.36 |
-| EF.SMSP | 6F42 | TS 31.102 4.2.27 | 28+Y | alpha + parameter indicators + TP-DA/SCA/PID/DCS/VP | S10 | on request only |
+| EF.SMSP | 6F42 | TS 31.102 4.2.27 | 28+Y | alpha + parameter indicators + TP-DA/SCA/PID/DCS/VP | S10 | **3.35** |
| EF.SMS | 6F3C | TS 31.102 4.2.25 | 176 | status byte + TPDU | S10 | **postponed (read-only)** |
| EF.ARR | 2F06/6F06 | TS 31.102 4.2.55 | X | access-rule TLVs (ISO 7816-4) | S8/S9 | **postponed (read-only)** |
| EF.DIR | 2F00 | TS 51.011 10.3.3 | X | application template TLVs (61 > 4F/50/73…) | S8/S9 | **postponed (read-only)** |
@@ -169,7 +170,15 @@ beyond S9/S10.
count and 1 for an even one; the first encoder always wrote 9.
12. **SIM EF.LP/EF.PL use the 1-byte DCS language coding**, unlike the USIM
EF.LI's 2-byte ISO 639 letters — the shared decoder is wrong for the SIM
- files (queued with the 3.35 language work).
+ files (queued with the language work).
+13. **EF.SMSP's parameter indicators are a five-bit presence map with inverted
+ meaning** (TS 31.102 §4.2.27: bit 1 DA, 2 SCA, 3 PID, 4 DCS, 5 VP; bit = 1
+ means *absent*; bits 6–8 reserved = 1). The decoder had the bits shifted
+ (SCA on 5, PID on 2, DCS on 3, VP on 4), so mixed indicators were misread.
+14. **The two address slots use different length conventions**: TP-DA's length
+ is the digit count (TS 23.040 §9.1.2.5), TS-SCA's is the octet count of
+ TON/NPI + digits with 0 = default SMSC (TS 24.011 §8.2.5.2); the decoder
+ read the DA slot as octets. Both are handled explicitly now.
## 4. Release waves
@@ -183,7 +192,13 @@ beyond S9/S10.
CBMI/CBMID/CBMIR. Decoder fixes: ACC class numbering, the AD operation
mode / MNC-length rules, the SST 2-bit layout (read-only for now) and the
IMSI parity nibble.
-- **v3.35.0** — LI/LP/PL (the SIM files need the 1-byte DCS language coding
+- **v3.35.0 (shipped)** — EF.SMSP as the first S10 dedicated builder: the full
+ parameter form (alpha; TP-DA and TS-SCA with their own length conventions
+ and TON; 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,
+ verbatim preservation of unchanged slots (including alphanumeric TON
+ addresses) and the parameter-indicator and TP-DA-length decoder fixes.
+- **v3.36.0** — LI/LP/PL (the SIM files need the 1-byte DCS language coding
and a name-dependent decoder), GID1/GID2 (the clause states no coding —
verify TS 31.101 first), SPDI (the A3 > 80 wrapper), OPL (PNN record
select), ECC (SIM and USIM — needs a structure-aware decode), SUUME,
@@ -191,8 +206,6 @@ beyond S9/S10.
- **Postponed by decision (read-only; do not implement without confirmation)**
— EF.DIR, EF.ARR, EF.SMS, EF.PBR: they rarely need manual changes and the
decoded view is enough.
-- **On request only** — EF.SMSP (a parameter form; no TPDU/parameter builder
- exists in the PWA today).
- **Read-only forever (S0)** unless a dedicated flow appears: LOCI/PSLOCI/
EPSLOCI/EPSNSC/LOCIGPRS, Kc/KcGPRS, Keys/KeysPS, ACM, START-HFN/THRESHOLD,
call logs (ME-written), DCK/NETPAR.
diff --git a/frontend/help-ru.html b/frontend/help-ru.html
index 4f054dd..8adf50d 100644
--- a/frontend/help-ru.html
+++ b/frontend/help-ru.html
@@ -409,7 +409,7 @@
Читать сырые данные / Декодировать — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); таблица декодируется в браузере из тех же сырых байт (без серверного декодирования). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).
Редактировать сырые данные — изменение hex-данных, Сохранить для записи (или Отмена); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.
-
Редактировать декодированные — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают EF.SPN, EF.PNN, списки PLMN (PLMNsel / FPLMN / EHPLMN и списки с технологиями доступа PLMNwAcT / OPLMNwAcT / HPLMNwAcT), файлы номеров (ADN / FDN / SDN / MSISDN / LND / BDN), таблицы сервисов (UST / IST / EST; SST остаётся только для чтения до появления редактора allocated/activated), ACC, PHASE, HPPLMN, AD, ICCID / IMSI (с подтверждением — запись меняет идентичность карты) и списки cell broadcast (CBMI / CBMID / CBMIR). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. Сохранить проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.
+
Редактировать декодированные — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают EF.SPN, EF.PNN, списки PLMN (PLMNsel / FPLMN / EHPLMN и списки с технологиями доступа PLMNwAcT / OPLMNwAcT / HPLMNwAcT), файлы номеров (ADN / FDN / SDN / MSISDN / LND / BDN), таблицы сервисов (UST / IST / EST; SST остаётся только для чтения до появления редактора allocated/activated), ACC, PHASE, HPPLMN, AD, ICCID / IMSI (с подтверждением — запись меняет идентичность карты) списки cell broadcast (CBMI / CBMID / CBMIR) и EF.SMSP (полная форма SMS-параметров: alpha, TP-DA и TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого параметра — флажок «отсутствует (действует значение ME по умолчанию)» и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. Сохранить проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.
Проверить все файлы — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают (пусто). Отображается прогресс N / всего, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.
ADM — файлы, требующие администраторский PIN, возвращают 6982/9804; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка «Проверить ADM», а значок в заголовке (ADM ✓/✗ ⚿) становится кликабельным. Каждый неверный ключ расходует попытку: остаток показывается, а повторная попытка требует подтверждения. Заблокированный ADM здесь не восстановить — нужен ключ разблокировки карты.
Read raw / Read decoded — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the decoded table is built in the browser from the same raw bytes (no server-side decode). The highlighted pill is the current view; clicking either pill (re-)reads the file. Read decoded is disabled when the client-side decoders do not cover the file (name/FID check, before reading).
Edit raw — edit the raw hex data, Save to write back (or Cancel); transparent files use a textarea, record files one input per record with selection checkboxes. Edit raw switches to the hex view first and reads the file if needed.
-
Edit decoded — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover EF.SPN, EF.PNN, the PLMN lists (PLMNsel / FPLMN / EHPLMN and the access-technology lists PLMNwAcT / OPLMNwAcT / HPLMNwAcT), the dialling-number files (ADN / FDN / SDN / MSISDN / LND / BDN), the service tables (UST / IST / EST; SST stays read-only until the SIM allocated/activated editor exists), ACC, PHASE, HPPLMN, AD, ICCID / IMSI (behind a confirmation — writing them changes the card identity) and the cell-broadcast lists (CBMI / CBMID / CBMIR). A transparent file gets one form (SPN fields, PLMN rows with add/remove, PNN items with add/remove); a record file gets one form per record with a selection checkbox that is auto-checked when a field changes, so only the edited records are written. Save validates every selected record before writing anything; files without an encoder keep the button disabled with a tooltip. Extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number that continues in an extension file is refused.
+
Edit decoded — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover EF.SPN, EF.PNN, the PLMN lists (PLMNsel / FPLMN / EHPLMN and the access-technology lists PLMNwAcT / OPLMNwAcT / HPLMNwAcT), the dialling-number files (ADN / FDN / SDN / MSISDN / LND / BDN), the service tables (UST / IST / EST; SST stays read-only until the SIM allocated/activated editor exists), ACC, PHASE, HPPLMN, AD, ICCID / IMSI (behind a confirmation — writing them changes the card identity) the cell-broadcast lists (CBMI / CBMID / CBMIR) and EF.SMSP (the full SMS-parameter form: alpha, TP-DA and TS-SCA with their addressing rules and TON, TP-PID, TP-DCS and the relative TP-VP, each with an absent (the ME default applies) toggle and live DCS/validity hints). A transparent file gets one form (SPN fields, PLMN rows with add/remove, PNN items with add/remove); a record file gets one form per record with a selection checkbox that is auto-checked when a field changes, so only the edited records are written. Save validates every selected record before writing anything; files without an encoder keep the button disabled with a tooltip. Extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number that continues in an extension file is refused.
Probe all files — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show (empty). Shows progress N / total, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.
ADM — files that need the administrator PIN fail with 6982/9804; if the matching card preset (same ICCID) carries an ADM key, a Verify ADM button appears next to the error and the header badge (ADM ✓/✗ ⚿) becomes clickable. Every wrong key consumes an attempt: the remaining attempts are shown and a retry asks for confirmation. A blocked ADM cannot be recovered here — it needs the card’s unblock key.
diff --git a/frontend/index.html b/frontend/index.html
index ea9edb0..b295d73 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1944,7 +1944,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.34.0';
+const SIMPLE_VERSION = '3.35.0';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -16329,9 +16329,10 @@ async function pysimFsDecSave(node, rawName, statusEl, out, arrTouched) {
try {
if (model.mode === 'tr') {
const orig = root ? (root.getAttribute('data-orig') || '') : '';
- const values = entry.list
- ? efDecCollectRows(root, entry.list.itemFields)
- : efDecCollectValues(root, entry.fields);
+ const values = entry.custom
+ ? entry.custom.collect(root)
+ : (entry.list ? efDecCollectRows(root, entry.list.itemFields)
+ : efDecCollectValues(root, entry.fields));
const body = pysimFsSelectBody(node || { name: rawName, fid: null });
body.data = entry.enc(values, orig);
// identity writes ask once, and only when something actually changes
@@ -16358,9 +16359,9 @@ async function pysimFsDecSave(node, rawName, statusEl, out, arrTouched) {
const num = block.getAttribute('data-rec');
if (!num) continue;
const body = pysimFsSelectBody(node || { name: rawName, fid: null });
- const collected = entry.items
- ? efDecCollectItems(block)
- : efDecCollectValues(block, entry.fields);
+ const collected = entry.custom
+ ? entry.custom.collect(block)
+ : (entry.items ? efDecCollectItems(block) : efDecCollectValues(block, entry.fields));
try {
body.data = entry.enc(collected, block.getAttribute('data-orig') || '');
} catch (e) {
@@ -22753,7 +22754,7 @@ const EF_FIELD_LABELS = {
op_mode: 'Operation mode', test_mode: 'Test mode', additional_info: 'Additional info',
mnc_length: 'MNC length', services: 'Services', plmns: 'PLMNs', applications: 'Applications',
aid: 'AID', label: 'Label', eap: 'EAP', message_ids: 'Message IDs', ranges: 'Ranges',
- esc: 'Emergency category', param_indicators: 'Parameter indicators', tp_da: 'TP-DA',
+ esc: 'Emergency category', indicators: 'Parameter indicators', tp_da: 'TP-DA',
sca: 'SCA', tp_pid: 'TP-PID', tp_dcs: 'TP-DCS', tp_vp: 'TP-VP', alpha: 'Alpha',
direction: 'Direction', status: 'Status', tpdu: 'TPDU', display_condition: 'Display condition',
rfu: 'RFU', ccp: 'CCP', ext: 'Extension', tag: 'Tag', hex: 'Hex', lac_range: 'LAC range',
@@ -23248,6 +23249,17 @@ function efDecAcl(b) {
return apns.length ? { apns: apns } : { raw: efHex(b) };
}
+// TS 23.040 §9.1.2.5: the TP-DA length is the DIGIT count, so the BCD octets
+// are ceil(len/2) (the SCA length counts the TON/NPI + BCD octets instead).
+function efSmspDaNumber(slot) {
+ const n = slot.length ? slot[0] : 0xFF;
+ if (n === 0xFF || n === 0) return '';
+ const ton = (slot[1] >> 4) & 0x07;
+ const octets = Math.min(10, Math.ceil(n / 2));
+ const digits = efSwapNibbles(efHex(slot.subarray(2, 2 + octets))).replace(/[fF]+$/, '');
+ return digits ? (ton === 1 ? '+' + digits : digits) : '';
+}
+
function efDecSmsp(b) {
if (efAllFf(b)) return { empty: true };
if (b.length < 28) return { raw: efHex(b) };
@@ -23255,14 +23267,15 @@ function efDecSmsp(b) {
const out = {};
const alpha = efAnnexA(efRstripFf(b.subarray(0, y)));
if (alpha) out.alpha = alpha;
+ // TS 31.102 4.2.27: bits 1..5 = DA, SCA, PID, DCS, VP (1 = absent), bits
+ // 6..8 reserved (1).
const pi = b[y];
- const flags = [];
- out.param_indicators = '0x' + pi.toString(16).toUpperCase().padStart(2, '0');
- if (!(pi & 0x01)) out.tp_da = efBcdAddressLen(b.subarray(y + 1, y + 13));
- if (!(pi & 0x10)) out.sca = efBcdAddressLen(b.subarray(y + 13, y + 25));
- if (!(pi & 0x02)) out.tp_pid = '0x' + b[y + 25].toString(16).toUpperCase().padStart(2, '0');
- if (!(pi & 0x04)) out.tp_dcs = '0x' + b[y + 26].toString(16).toUpperCase().padStart(2, '0');
- if (!(pi & 0x08)) out.tp_vp = '0x' + b[y + 27].toString(16).toUpperCase().padStart(2, '0');
+ out.indicators = '0x' + pi.toString(16).toUpperCase().padStart(2, '0');
+ if (!(pi & 0x01)) out.tp_da = efSmspDaNumber(b.subarray(y + 1, y + 13));
+ if (!(pi & 0x02)) out.sca = efBcdAddressLen(b.subarray(y + 13, y + 25));
+ if (!(pi & 0x04)) out.tp_pid = '0x' + b[y + 25].toString(16).toUpperCase().padStart(2, '0');
+ if (!(pi & 0x08)) out.tp_dcs = '0x' + b[y + 26].toString(16).toUpperCase().padStart(2, '0');
+ if (!(pi & 0x10)) out.tp_vp = '0x' + b[y + 27].toString(16).toUpperCase().padStart(2, '0');
return out;
}
@@ -24290,6 +24303,343 @@ function efEncCbmir(values, origHex) {
return efHex(out);
}
+// ===== EF.SMSP (TS 31.102 4.2.27) =====
+// A linear-fixed record: alpha (Y = len-28 octets), the parameter indicator
+// byte (bits 1..5 = DA, SCA, PID, DCS, VP; 1 = absent; bits 6..8 = 1), the
+// 12-octet DA slot, the 12-octet SCA slot, then the PID, DCS and VP bytes.
+const EF_SMSP_PARAMS = [
+ { key: 'da', label: 'TP-DA', kind: 'address', bit: 0x01, off: 1 },
+ { key: 'sca', label: 'TS-SCA', kind: 'address', bit: 0x02, off: 13 },
+ { key: 'pid', label: 'TP-PID', kind: 'hex', bit: 0x04, off: 25 },
+ { key: 'dcs', label: 'TP-DCS', kind: 'hex', bit: 0x08, off: 26 },
+ { key: 'vp', label: 'TP-VP', kind: 'hex', bit: 0x10, off: 27 },
+];
+
+// Hint text goes through t() when the runtime has it (the pure test harness
+// extracts these without the dictionary and gets the English text).
+function smspT(s) {
+ return typeof t === 'function' ? t(s) : s;
+}
+
+// TS 23.038 §4 coding-group hint.
+function smspDcsHint(hex) {
+ const s = String(hex === undefined || hex === null ? '' : hex).replace(/^0x/i, '').trim();
+ if (!/^[0-9a-fA-F]{2}$/.test(s)) return '';
+ const d = parseInt(s, 16);
+ const top = d >> 4;
+ const classes = ['class 0', 'class 1', 'class 2', 'class 3'];
+ if (top <= 7) {
+ const parts = [
+ smspT(top >= 4 ? 'automatic deletion' : 'general data coding'),
+ smspT(['GSM 7-bit', '8-bit data', 'UCS2', 'reserved'][(d >> 2) & 3]),
+ smspT((d & 0x20) ? 'compressed' : 'uncompressed'),
+ ];
+ if (d & 0x10) parts.push(smspT(classes[d & 3]));
+ return parts.join(', ');
+ }
+ if (top <= 11) return smspT('reserved coding group');
+ if (top <= 14) {
+ return [
+ smspT(top === 12 ? 'MWI discard' : 'MWI store'),
+ smspT(top === 14 ? 'UCS2' : 'GSM 7-bit'),
+ smspT((d & 0x08) ? 'active' : 'inactive'),
+ smspT(['voicemail', 'fax', 'email', 'other'][d & 3]),
+ ].join(', ');
+ }
+ return [
+ smspT('data coding'),
+ smspT((d & 0x04) ? '8-bit data' : 'GSM 7-bit'),
+ smspT(classes[d & 3]),
+ ].join(', ');
+}
+
+function smspDurationText(secs) {
+ const h = Math.floor(secs / 3600);
+ const m = Math.round((secs % 3600) / 60);
+ if (!h) return m + ' ' + smspT('min');
+ return h + ' ' + smspT('h') + (m ? ' ' + m + ' ' + smspT('min') : '');
+}
+
+// Russian needs three plural forms (1 / 2-4 / 5+, and the 11-14 exception);
+// English needs the singular/plural pair. `few` is only used by the RU form.
+function smspPluralIndex(n) {
+ n = Math.abs(n) % 100;
+ if (n >= 11 && n <= 14) return 2;
+ const d = n % 10;
+ if (d === 1) return 0;
+ if (d >= 2 && d <= 4) return 1;
+ return 2;
+}
+
+function smspCount(n, one, few, many) {
+ const ru = typeof currentLang !== 'undefined' && currentLang === 'ru';
+ if (ru) {
+ const idx = smspPluralIndex(n);
+ return n + ' ' + smspT(idx === 0 ? one : (idx === 1 ? few : many));
+ }
+ return n + ' ' + smspT(n === 1 ? one : many);
+}
+
+// TS 23.040 §9.2.3.12.1 relative validity-period hint: minutes for 0-143,
+// 30-minute steps for 144-167, DAYS for 168-196 and WEEKS for 197-255.
+function smspVpHint(hex) {
+ const s = String(hex === undefined || hex === null ? '' : hex).replace(/^0x/i, '').trim();
+ if (!/^[0-9a-fA-F]{2}$/.test(s)) return '';
+ const v = parseInt(s, 16);
+ if (v <= 143) return smspDurationText((v + 1) * 300);
+ if (v <= 167) return smspDurationText(43200 + (v - 143) * 1800);
+ if (v <= 196) return smspCount(v - 166, 'day', 'days-few', 'days');
+ return smspCount(v - 192, 'week', 'weeks-few', 'weeks');
+}
+
+// A DA/SCA slot decoded for the form: the length byte is the digit count for
+// DA and the TON/NPI + BCD octet count for SCA (0 = the default SMSC). A
+// TON 5 (alphanumeric) address is not BCD text - it is shown raw and kept
+// verbatim by the encoder.
+function efSmspAddressModel(slot, present, isSca) {
+ const orig = efHex(slot);
+ const n = slot.length ? slot[0] : 0xFF;
+ let ton = 1, number = '';
+ if (n !== 0xFF && n !== 0) {
+ ton = (slot[1] >> 4) & 0x07;
+ const octets = Math.max(0, Math.min(10, isSca ? n - 1 : Math.ceil(n / 2)));
+ const digits = ton === 5 ? ''
+ : efSwapNibbles(efHex(slot.subarray(2, 2 + octets))).replace(/[fF]+$/, '');
+ number = digits ? (ton === 1 ? '+' + digits : digits) : '';
+ }
+ return { present: present, orig: orig, number: number, ton: ton, alnum: ton === 5 };
+}
+
+// The per-record form model: alpha + each parameter's presence, original slot
+// bytes and editable value.
+function efSmspModel(hex) {
+ const b = efBytes(hex || '');
+ const y = Math.max(0, b.length - 28);
+ const pi = b.length >= 28 ? b[y] : 0xFF;
+ const alpha = efAnnexA(efRstripFf(b.subarray(0, Math.min(y, b.length))));
+ const slot = (at, len) => b.subarray(at, Math.min(b.length, at + len));
+ const byte = (at, bit) => {
+ const v = b.length > at ? b[at] : 0xFF;
+ return { present: !(pi & bit), orig: efHex([v]),
+ value: '0x' + v.toString(16).toUpperCase().padStart(2, '0') };
+ };
+ return {
+ alpha: alpha,
+ params: {
+ da: efSmspAddressModel(slot(y + 1, 12), !(pi & 0x01), false),
+ sca: efSmspAddressModel(slot(y + 13, 12), !(pi & 0x02), true),
+ pid: byte(y + 25, 0x04),
+ dcs: byte(y + 26, 0x08),
+ vp: byte(y + 27, 0x10),
+ },
+ };
+}
+
+function efSmspTonOptions(ton) {
+ const opts = [{ v: 0, l: 'Unknown' }, { v: 1, l: 'International' }, { v: 2, l: 'National' }];
+ if (!opts.some(o => o.v === ton)) opts.push({ v: ton, l: 'Current', current: true });
+ return opts.map(o => '').join('');
+}
+
+function efSmspParamRow(key, label, slot) {
+ const absent = !slot.present;
+ const dis = absent ? ' disabled' : '';
+ const raw = slot.alnum && slot.present;
+ let body;
+ if ((key === 'da' || key === 'sca') && raw) {
+ // TS 23.040 9.1.2.5 TON 5: GSM 7-bit alphanumeric contents, not BCD -
+ // keep the slot verbatim, only its presence is editable here.
+ body = '' + esc(t(label)) + '' +
+ '' + esc(slot.orig || '') + '' +
+ '' +
+ esc(t('alphanumeric address — not editable here; it is preserved as is')) + '';
+ } else if (key === 'da' || key === 'sca') {
+ body = '' + esc(t(label)) + '' +
+ '' +
+ '';
+ if (key === 'sca') {
+ body += '' + esc(t('empty = default SMSC')) + '';
+ }
+ } else {
+ const hint = absent ? t('absent — the ME default applies')
+ : (key === 'dcs' ? smspDcsHint(slot.value) : (key === 'vp' ? smspVpHint(slot.value) : ''));
+ body = '' + esc(t(label)) + '' +
+ '';
+ body += key === 'pid'
+ ? '' + esc(t('TS 23.040 TP-PID (raw); 0x00 = short message type 0')) + ''
+ : '' + esc(hint) + '';
+ }
+ return '
' +
+ '' +
+ body + '
';
+}
+
+function efSmspHtml(values) {
+ const v = values || {};
+ const p = v.params || {};
+ let html = '
' +
+ '
' +
+ '' + esc(t('Alpha')) + '' +
+ '' +
+ '
';
+ for (const spec of EF_SMSP_PARAMS) html += efSmspParamRow(spec.key, spec.label, p[spec.key] || {});
+ return html + '
';
+}
+
+function efSmspCollect(root) {
+ const pick = key => root.querySelector('[data-smsp="' + key + '"]');
+ const out = { alpha: '', params: {} };
+ const alpha = pick('alpha');
+ if (alpha) out.alpha = alpha.value;
+ for (const spec of EF_SMSP_PARAMS) {
+ const row = root.querySelector('[data-smsp-orig="' + spec.key + '"]');
+ const absent = row ? row.querySelector('[data-smsp-absent="' + spec.key + '"]') : null;
+ const present = !(absent && absent.checked);
+ if (spec.kind === 'address') {
+ const num = pick(spec.key + '.number');
+ const ton = pick(spec.key + '.ton');
+ out.params[spec.key] = { present: present,
+ number: num ? num.value : '', ton: ton ? parseInt(ton.value, 10) : 1 };
+ } else {
+ const val = pick(spec.key + '.value');
+ out.params[spec.key] = { present: present, value: val ? val.value : '' };
+ }
+ }
+ return out;
+}
+
+function pysimFsSmspToggle(cb) {
+ const row = cb && cb.closest ? cb.closest('[data-smsp-orig]') : null;
+ if (!row) return;
+ const absent = !!cb.checked;
+ row.querySelectorAll('[data-smsp]').forEach(el => { el.disabled = absent; });
+ const key = row.getAttribute('data-smsp-orig');
+ const hint = row.querySelector('[data-smsp-hint]');
+ if (hint) {
+ const input = row.querySelector('[data-smsp="' + key + '.value"]');
+ hint.textContent = absent ? t('absent — the ME default applies')
+ : (key === 'dcs' ? smspDcsHint(input && input.value) : smspVpHint(input && input.value));
+ }
+ pysimFsDecRowCheck(cb);
+}
+
+function pysimFsSmspHexInput(input) {
+ const row = input && input.closest ? input.closest('[data-smsp-orig]') : null;
+ if (row) {
+ const key = row.getAttribute('data-smsp-orig');
+ const cb = row.querySelector('[data-smsp-absent]');
+ const hint = row.querySelector('[data-smsp-hint]');
+ if (hint) {
+ hint.textContent = (cb && cb.checked) ? t('absent — the ME default applies')
+ : (key === 'dcs' ? smspDcsHint(input.value) : smspVpHint(input.value));
+ }
+ }
+ pysimFsDecRowCheck(input);
+}
+
+function efSmspHexByte(value) {
+ const s = String(value === undefined || value === null ? '' : value).replace(/^0x/i, '').trim();
+ if (!/^[0-9a-fA-F]{2}$/.test(s)) throw efEncError('The parameter value must be 2 hex digits');
+ return parseInt(s, 16);
+}
+
+// A DA/SCA slot: [length][TON/NPI][10 BCD bytes], FF beyond the digits. DA's
+// length is the digit count, SCA's the TON/NPI+BCD octet count (0 = default).
+// A leading '+' forces TON 1 (international) whatever the select says.
+function efSmspAddressBytes(key, val) {
+ const num = String(val && val.number !== undefined && val.number !== null ? val.number : '').replace(/\s+/g, '');
+ const plus = num.charAt(0) === '+';
+ const digits = num.replace(/^\+/, '');
+ if (digits !== '' && !/^\d+$/.test(digits)) throw efEncError('The number must be digits with an optional leading +');
+ if (digits.length > 20) throw efEncError('The number is too long for SMSP (max 20 digits)');
+ if (key === 'da' && digits.length === 0) throw efEncError('The TP-DA number is required when present');
+ const ton = plus ? 1 : Number(val && val.ton);
+ const out = new Uint8Array(12);
+ out.fill(0xFF);
+ if (key === 'da') out[0] = digits.length;
+ else out[0] = digits.length ? 1 + Math.ceil(digits.length / 2) : 0;
+ if (!digits.length) return out;
+ out[1] = 0x80 | ((Number.isInteger(ton) ? ton : 1) << 4) | 0x01; // extension + TON + NPI(ISDN)
+ for (let i = 0; i < 10; i++) {
+ const lo = i * 2 < digits.length ? digits[i * 2] : 'F';
+ const hi = i * 2 + 1 < digits.length ? digits[i * 2 + 1] : 'F';
+ out[2 + i] = parseInt(hi + lo, 16);
+ }
+ return out;
+}
+
+function efSmspAddrUnchanged(slot, key, val) {
+ const model = efSmspAddressModel(slot, true, key === 'sca');
+ const num = String(val && val.number !== undefined && val.number !== null ? val.number : '').replace(/\s+/g, '');
+ const ton = val && val.ton !== undefined && val.ton !== null ? Number(val.ton) : model.ton;
+ return num === model.number && ton === model.ton;
+}
+
+function efSmspByteUnchanged(origByte, value) {
+ const s = String(value === undefined || value === null ? '' : value).replace(/^0x/i, '').trim();
+ if (origByte === undefined || !/^[0-9a-fA-F]{2}$/.test(s)) return false;
+ return parseInt(s, 16) === origByte;
+}
+
+// EF.SMSP: the alpha, the five parameters' slots and the indicator byte. An
+// unchanged slot is copied verbatim (the fixture's absent DCS 00 and VP FF
+// included); the indicator byte is kept when all five presence flags match.
+function efEncSmsp(values, origHex) {
+ const orig = efBytes(origHex || '');
+ if (orig.length < 28) throw efEncError('The record is too short for the SMSP layout');
+ const y = orig.length - 28;
+ const origPi = orig[y];
+ const out = new Uint8Array(orig.length);
+ out.fill(0xFF);
+ const alpha = String(values && values.alpha !== undefined && values.alpha !== null ? values.alpha : '');
+ const origAlpha = efAnnexA(efRstripFf(orig.subarray(0, y)));
+ if (alpha === origAlpha) out.set(orig.subarray(0, y), 0);
+ else out.set(efEncTextBytes(alpha, y, 'The alpha identifier is too long for this record'), 0);
+ const params = (values && values.params) || {};
+ const presence = {};
+ for (const spec of EF_SMSP_PARAMS) {
+ const v = params[spec.key] || {};
+ const origPresent = !(origPi & spec.bit);
+ const present = v.present === undefined ? origPresent : !!v.present;
+ presence[spec.key] = present;
+ const at = y + spec.off;
+ const samePresence = present === origPresent;
+ if (spec.kind === 'address') {
+ const slot = orig.subarray(at, at + 12);
+ const origTon = slot.length > 1 ? (slot[1] >> 4) & 0x07 : 0;
+ // a present alphanumeric (TON 5) address is not editable here: it
+ // stays verbatim, and only its presence can change
+ if (origPresent && origTon === 5) {
+ if (present) out.set(slot, at);
+ else for (let i = 0; i < 12; i++) out[at + i] = 0xFF;
+ } else if (samePresence && efSmspAddrUnchanged(slot, spec.key, v)) out.set(slot, at);
+ else if (!present) for (let i = 0; i < 12; i++) out[at + i] = 0xFF;
+ else out.set(efSmspAddressBytes(spec.key, v), at);
+ } else {
+ const origByte = orig.length > at ? orig[at] : undefined;
+ if (samePresence && efSmspByteUnchanged(origByte, v.value)) out[at] = origByte;
+ else if (!present) out[at] = 0xFF;
+ else out[at] = efSmspHexByte(v.value);
+ }
+ }
+ const allSame = EF_SMSP_PARAMS.every(spec => presence[spec.key] === !(origPi & spec.bit));
+ if (allSame) out[y] = origPi;
+ else {
+ let pi = 0xE0;
+ for (const spec of EF_SMSP_PARAMS) if (!presence[spec.key]) pi |= spec.bit;
+ out[y] = pi;
+ }
+ return efHex(out);
+}
+
+const EF_SMSP_CUSTOM = { model: efSmspModel, html: efSmspHtml, collect: efSmspCollect };
+
// The decoded editor's field schemas: one per file family, the keys matching
// the decoder output. `kind` is text, digits, bool, select or techs (the
// access technology checkboxes, the value being the decoder's ', '-joined
@@ -24423,6 +24773,7 @@ const EF_ENCODERS = [
{ name: 'EF.CBMI', fid: '6f45', enc: efEncCbmi, model: efCbmiModel, list: EF_CBMI_LIST },
{ name: 'EF.CBMID', fid: '6f48', enc: efEncCbmi, model: efCbmiModel, list: EF_CBMI_LIST },
{ name: 'EF.CBMIR', fid: '6f50', enc: efEncCbmir, model: efCbmirModel, list: EF_CBMIR_LIST },
+ { name: 'EF.SMSP', fid: '6f42', enc: efEncSmsp, custom: EF_SMSP_CUSTOM },
{ name: 'EF.ADN', fid: '6f3a', enc: efEncAdnExt(14, 'EXT1'), ext: 'EXT1', fields: EF_ADN_FIELDS },
{ name: 'EF.FDN', fid: '6f3b', enc: efEncAdnExt(14, 'EXT2'), ext: 'EXT2', fields: EF_ADN_FIELDS },
{ name: 'EF.SDN', fid: '6f49', enc: efEncAdnExt(14, 'EXT3'), ext: 'EXT3', fields: EF_ADN_FIELDS },
@@ -24652,16 +25003,19 @@ function efDecEditModel(name, fid, content) {
const entry = efFindEncoder(name, fid);
if (!entry) return null;
const dec = efFindDecoder(name, fid);
+ const modelValues = hex => entry.custom
+ ? entry.custom.model(hex)
+ : efDecModelFor(entry, dec, hex);
if (content && content.kind === 'record') {
return {
entry: entry,
mode: 'records',
records: (content.records || []).map(r => entry.items
? { num: r.num, orig: r.data || '', items: efDecItemsFromHex(entry, r.data || '') }
- : { num: r.num, orig: r.data || '', values: efDecModelFor(entry, dec, r.data || '') }),
+ : { num: r.num, orig: r.data || '', values: modelValues(r.data || '') }),
};
}
- return { entry: entry, mode: 'tr', values: efDecModelFor(entry, dec, (content && content.data) || '') };
+ return { entry: entry, mode: 'tr', values: modelValues((content && content.data) || '') };
}
// The value of a techs field (the decoder's ', '-joined string) as a list.
@@ -24793,7 +25147,8 @@ function efDecRecordsHtml(entry, records) {
'' +
- '