terminal profile: fix the per-byte bit order (b1 = LSB), add the inference notes (v2.2.20)

The spec tables (TS 102 223 5.2) list b1 - the least significant bit - first:
verified from the PDF connector geometry (label row 1 connects to the
rightmost cell) and from Wireshark's packet-gsm_sim.c masks ('Profile
download' = 0x01, 'E-UTRAN bearer' = 0x40, 'HSDPA bearer' = 0x80). The
Configure dialog had it mirrored: TP_BITS[n] was matched with 0x80 >> (n%8)
and displayed as b8..b1, so every label toggled the wrong physical bit
(our own BIP-capable default proved it: byte 12 = 0x1F read as DECLARE
SERVICE.. instead of OPEN/CLOSE/RECEIVE/SEND/GET CHANNEL STATUS).

- tpGetBit/tpSetBit now use 1 << (n%8); the form lists b1 first, matching
  the spec; tpBitLabel names the RFU fallback bit accordingly.
- Tests pin the new mapping and the corrected spot-check indices, and the
  project default is asserted to decode as BIP-capable (OPEN/SEND DATA,
  E-UTRAN, HSDPA).
- docs/TERMINAL_PROFILE.md (in projects/docs): everything gathered about
  inferring device characteristics from a TP - bit order, profile-length era
  bands, headless/modem markers (Annex S ND/NK plus the byte 11/screen-size
  heuristics), radio generation (byte 17 b7/b8 with the 'capability, not
  attach' caveat), standards family (ESN/IMEISV/TIA/CCAT), BIP capability,
  UI paradigm, and a suggested inference model. Corpus: samples/tp_db.csv
  (185 profiles, CC-BY-SA, kept out of the repo).
- Help EN/RU note the bit order and point at the new document; AGENTS
  records the mapping rule and the doc location.
This commit is contained in:
2026-09-19 01:21:06 +03:00
parent 2a03c74941
commit 0fe51491d8
7 changed files with 50 additions and 43 deletions
+12 -12
View File
@@ -18,7 +18,7 @@
<div class="max-w-7xl mx-auto px-6 py-2">
<div class="flex items-center justify-between mb-3">
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.19</span></h1>
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.20</span></h1>
<div class="flex items-center gap-4">
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting..."></span>
@@ -7170,15 +7170,11 @@ const PLI_QUALIFIERS = [
{code:'1A',name:'Supported Radio Access Technologies'},
];
// TERMINAL PROFILE bit allocation (TS 102 223 V18.3.0 5.2 + 3GPP
// annex bits; bytes 1..33). Index 0 = bit 1 = first byte b8; bit n is
// byte floor(n/8)+1, mask 0x80 >> (n%8). Bytes beyond 33 fall back to
// generic labels in the UI.
// TERMINAL PROFILE bit allocation: bytes 1..39 per ETSI TS 102 223
// V18.3.0 5.2, 3GPP-defined bits named per ETSI TS 131 111 V18.12.0 5.2
// (no "reserved by 3GPP" placeholders). Index 0 = bit 1 = first byte b8;
// bit n is byte floor(n/8)+1, mask 0x80 >> (n%8). Bytes beyond the
// table fall back to generic labels in the UI.
// (no "reserved by 3GPP" placeholders). Index 0 = the first spec table row
// = byte 1 b1 (the LSB); bit n is byte floor(n/8)+1, mask 1 << (n%8).
// Bytes beyond the table fall back to generic labels in the UI.
const TP_BITS = [
'Profile download', 'SMS-PP data download', 'Cell Broadcast data download', 'Menu selection',
'SMS-PP data download', 'Timer expiration', 'USSD string data object support in Call Control by USIM', 'Call Control by NAA',
@@ -7278,11 +7274,15 @@ function tpNorm(hex) { return (hex || '').toUpperCase().replace(/[^0-9A-F]/g, ''
function tpValid(hex) { return hex.length > 0 && hex.length % 2 === 0 && hex.length <= 510; }
// Bit numbering follows TS 102 223 5.2 exactly: within a byte the spec tables
// list b1 (the least significant bit) first, so TP_BITS[n] is bit b((n % 8)+1)
// of byte (n >> 3)+1. tpGetBit/tpSetBit therefore use the LSB-first mask
// (cross-checked against Wireshark's packet-gsm_sim.c: "Profile download"=0x01).
function tpGetBit(hex, n) {
const byteIdx = n >> 3;
if (byteIdx * 2 + 2 > hex.length) return null;
const b = parseInt(hex.substr(byteIdx * 2, 2), 16);
return !!(b & (0x80 >> (n & 7)));
return !!(b & (1 << (n & 7)));
}
function tpSetBit(hex, n, on) {
@@ -7290,7 +7290,7 @@ function tpSetBit(hex, n, on) {
const byteIdx = n >> 3;
while (bytes.length <= byteIdx) bytes.push('00');
let b = parseInt(bytes[byteIdx], 16);
const mask = 0x80 >> (n & 7);
const mask = 1 << (n & 7);
b = on ? (b | mask) : (b & ~mask);
bytes[byteIdx] = b.toString(16).padStart(2, '0').toUpperCase();
return bytes.join('');
@@ -7298,7 +7298,7 @@ function tpSetBit(hex, n, on) {
function tpBitLabel(n) {
if (n < TP_BITS.length) return TP_BITS[n];
return 'RFU (byte ' + ((n >> 3) + 1) + ' b' + (8 - (n & 7)) + ')';
return 'RFU (byte ' + ((n >> 3) + 1) + ' b' + ((n & 7) + 1) + ')';
}
// Fixed byte-block layout: [firstByte, lastByte, columns] with 1-based
@@ -7340,7 +7340,7 @@ function tpRenderForm() {
html += '<div class="text-gray-500 dark:text-slate-400 mb-0.5">' + esc(t('Byte')) + ' ' + (bi + 1) + '</div>';
for (let k = 0; k < 8; k++) {
const n = bi * 8 + k;
const mask = 'b' + (8 - k);
const mask = 'b' + (k + 1); // b1 first, matching the spec tables
const label = tpBitLabel(n);
html += '<label class="flex items-start gap-1 mb-0.5">' +
'<input type="checkbox" class="mt-0.5 shrink-0" ' +