contacts, modem: implement direct QRTR QMI UIM SIM phonebook import and export for SIM 1 and SIM 2

This commit is contained in:
Luxferre
2026-10-25 15:30:24 +02:00
parent ef18a33c9b
commit 7c707844cc
5 changed files with 1095 additions and 173 deletions
Binary file not shown.
+385 -111
View File
@@ -77,6 +77,9 @@ detect_qmicli_modem() {
if timeout 3 qmicli --silent -pd qrtr://0 --uim-noop >/dev/null 2>&1; then
QMICLI_MODEM="timeout 4 qmicli --silent -pd qrtr://0"
return 0
elif timeout 3 qmicli --silent -d qrtr://0 --uim-noop >/dev/null 2>&1; then
QMICLI_MODEM="timeout 4 qmicli --silent -d qrtr://0"
return 0
elif [ -e "/dev/modem" ]; then
if timeout 3 qmicli --silent -pd /dev/modem --uim-noop >/dev/null 2>&1; then
QMICLI_MODEM="timeout 4 qmicli --silent -pd /dev/modem"
@@ -1181,6 +1184,151 @@ case "$cmd" in
fi
fi
tmp_contacts="/tmp/sim_import_$$.tsv"
rm -f "$tmp_contacts"
: > "$tmp_contacts"
# Method 1: Fast direct QRTR QMI UIM EF_ADN read (supports SIM 1 & SIM 2 without switching GW session)
if command -v python3 >/dev/null 2>&1; then
python3 - "$target_slot" > "$tmp_contacts" 2>/dev/null << 'PY' || true
import socket, struct, select, sys
slot = int(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1].isdigit() else 1
AF_QIPCRTR = 42
QRTR_PORT_CTRL = 0xfffffffe
QRTR_TYPE_NEW_SERVER = 4
QRTR_TYPE_NEW_LOOKUP = 10
QMI_SERVICE_UIM = 11
ls = socket.socket(AF_QIPCRTR, socket.SOCK_DGRAM)
node, _ = ls.getsockname()
ls.sendto(struct.pack("<IIIII", QRTR_TYPE_NEW_LOOKUP, QMI_SERVICE_UIM, 0, 0, 0), (node, QRTR_PORT_CTRL))
uim_addr = None
while True:
r, _, _ = select.select([ls], [], [], 0.15)
if not r: break
data, _ = ls.recvfrom(256)
if len(data) >= 20:
cmd, srv, inst, srv_node, srv_port = struct.unpack("<IIIII", data[:20])
if cmd == QRTR_TYPE_NEW_SERVER:
if srv == QMI_SERVICE_UIM and srv_port != 0:
uim_addr = (srv_node, srv_port)
if srv == 0 and srv_node == 0 and srv_port == 0:
break
ls.close()
if not uim_addr:
sys.exit(1)
s = socket.socket(AF_QIPCRTR, socket.SOCK_DGRAM)
session_type = 7 if slot == 2 else 6
tlv_f = None
rec_size = 0
rec_count = 0
txn = 1
for stype in [session_type, 0]:
for fid, path in [(0x6F3A, [0x3F00, 0x7F10]), (0x4F3A, [0x3F00, 0x7F10, 0x5F3A])]:
pbytes = b"".join(struct.pack("<H", x) for x in path)
fval = struct.pack("<HB", fid, len(pbytes)) + pbytes
tf = struct.pack("<BH", 0x02, len(fval)) + fval
ts = struct.pack("<BHB", 0x01, 2, stype) + b"\x00"
tlvs = ts + tf
s.sendto(struct.pack("<BHHH", 0x00, txn, 0x0024, len(tlvs)) + tlvs, uim_addr)
txn += 1
r, _, _ = select.select([s], [], [], 0.15)
if not r: continue
resp, _ = s.recvfrom(512)
if len(resp) < 14 or resp[0] != 0x02 or resp[3] != 0x24: continue
pos = 7
ok = False
rs = rc = 0
while pos + 3 <= len(resp):
tt, tl = struct.unpack("<BH", resp[pos:pos+3])
pos += 3
if tt == 0x02 and tl >= 4:
res, err = struct.unpack("<HH", resp[pos:pos+4])
if res == 0 and err == 0: ok = True
elif tt == 0x11 and tl >= 9:
_, _, _, rs, rc = struct.unpack("<HHBHH", resp[pos:pos+9])
pos += tl
if ok and rs >= 14 and rc >= 1:
session_type = stype
tlv_f = tf
rec_size = rs
rec_count = min(rc, 500)
break
if tlv_f:
break
if not tlv_f:
s.close()
sys.exit(1)
tlv_session = struct.pack("<BHB", 0x01, 2, session_type) + b"\x00"
bcd_map = "0123456789*#p"
for rnum in range(1, rec_count + 1):
tlv_rec = struct.pack("<BHHH", 0x03, 4, rnum, rec_size)
tlvs = tlv_session + tlv_f + tlv_rec
s.sendto(struct.pack("<BHHH", 0x00, txn, 0x0021, len(tlvs)) + tlvs, uim_addr)
txn += 1
r, _, _ = select.select([s], [], [], 0.15)
if not r: continue
resp, _ = s.recvfrom(512)
pos = 7
raw = None
ok = False
while pos + 3 <= len(resp):
tt, tl = struct.unpack("<BH", resp[pos:pos+3])
pos += 3
if tt == 0x02 and tl >= 4:
res, err = struct.unpack("<HH", resp[pos:pos+4])
if res == 0 and err == 0: ok = True
elif tt == 0x11 and tl >= 2:
clen = struct.unpack("<H", resp[pos:pos+2])[0]
raw = resp[pos+2:pos+2+clen]
pos += tl
if not (ok and raw and len(raw) >= 14):
continue
alpha_len = len(raw) - 14
alpha = raw[:alpha_len]
bcd_len = raw[alpha_len]
ton_npi = raw[alpha_len + 1]
if bcd_len == 0 or bcd_len > 11 or bcd_len == 0xFF:
continue
num = "+" if ((ton_npi >> 4) & 0x07) == 1 else ""
for b in raw[alpha_len + 2 : alpha_len + 2 + min(10, bcd_len - 1)]:
if b == 0xFF: break
lo = b & 0x0F
hi = (b >> 4) & 0x0F
if lo < len(bcd_map): num += bcd_map[lo]
else: break
if hi == 0x0F: break
if hi < len(bcd_map): num += bcd_map[hi]
else: break
if not num or num == "+":
continue
while alpha and alpha[-1] == 0xFF:
alpha = alpha[:-1]
if not alpha:
name = num
elif alpha[0] == 0x80:
u16 = alpha[1:]
if len(u16) % 2 == 1: u16 = u16[:-1]
name = u16.decode("utf-16-be", errors="replace").replace("\t", " ").replace("\n", " ").strip()
else:
name = "".join(chr(b) for b in alpha if 32 <= b < 127 and b != 9).strip()
print(f"{num}\t{name or num}")
s.close()
PY
fi
if [ -s "$tmp_contacts" ]; then
cat "$tmp_contacts"
rm -f "$tmp_contacts"
exit 0
fi
orig_gw_slot="$PRIMARY_GW_SLOT"
orig_gw_aid=""
if [ "$orig_gw_slot" = "1" ]; then
@@ -1210,57 +1358,7 @@ case "$cmd" in
fi
fi
tmp_contacts="/tmp/sim_import_$$.tsv"
rm -f "$tmp_contacts"
: > "$tmp_contacts"
# Method 1: Standard 3GPP AT+CPBS="SM" / AT+CPBR
at_info="$("$0" at auto 'AT+CMEE=2;+CSCS="GSM";+CPBS="SM";+CPBR=?' 2>/dev/null || true)"
max_idx="$(printf "%s\n" "$at_info" | sed -n 's/.*+CPBR:[[:space:]]*([0-9]*-\([0-9]*\)).*/\1/p' | head -n1)"
if [ -n "$max_idx" ] && [ "$max_idx" -gt 0 ] 2>/dev/null; then
if [ "$max_idx" -gt 500 ]; then
max_idx=500
fi
start_idx=1
while [ "$start_idx" -le "$max_idx" ]; do
end_idx=$((start_idx + 49))
if [ "$end_idx" -gt "$max_idx" ]; then
end_idx="$max_idx"
fi
chunk="$("$0" at auto "AT+CPBR=${start_idx},${end_idx}" 2>/dev/null || true)"
if [ -n "$chunk" ]; then
printf "%s\n" "$chunk" | awk '
/^\+CPBR:[[:space:]]*[0-9]+,/ {
line = $0
sub(/^\+CPBR:[[:space:]]*[0-9]+,[[:space:]]*"/, "", line)
q1 = index(line, "\"")
if (q1 <= 0) next
num = substr(line, 1, q1 - 1)
rest = substr(line, q1 + 1)
sub(/^[[:space:]]*,[[:space:]]*/, "", rest)
c2 = index(rest, ",")
if (c2 <= 0) next
ton = substr(rest, 1, c2 - 1) + 0
name = substr(rest, c2 + 1)
sub(/^[[:space:]]*"/, "", name)
sub(/"[[:space:]]*$/, "", name)
gsub(/[\t\r\n]/, " ", name)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", num)
if (num == "") next
if (ton == 145 && substr(num, 1, 1) != "+") {
num = "+" num
}
if (name == "") name = num
printf "%s\t%s\n", num, name
}
' >> "$tmp_contacts"
fi
start_idx=$((end_idx + 1))
done
fi
# Method 2: Direct QMI UIM EF_ADN (3F00:7F10:6F3A) record fallback
# Method 2: Direct qmicli UIM EF_ADN (3F00-7F10-6F3A) record fallback
if [ ! -s "$tmp_contacts" ]; then
adn_attr="$($QMICLI_MODEM --uim-get-file-attributes=0x3F00,0x7F10,0x6F3A 2>/dev/null || true)"
reclen="$(printf "%s\n" "$adn_attr" | awk '/Record (size|length):/ {gsub(/[^0-9]/, "", $NF); print $NF + 0; exit}')"
@@ -1271,9 +1369,9 @@ case "$cmd" in
fi
rec=1
while [ "$rec" -le "$reccount" ]; do
raw_rec="$($QMICLI_MODEM --uim-read-record="file=0x3F00,0x7F10,0x6F3A,record-number=${rec},record-length=${reclen}" 2>/dev/null || true)"
raw_rec="$($QMICLI_MODEM --uim-read-record="file=0x3F00-0x7F10-0x6F3A,record-number=${rec},record-length=${reclen}" 2>/dev/null || true)"
if [ -n "$raw_rec" ]; then
printf "%s\n" "$raw_rec" | awk -v rlen="$reclen" '
printf "%s\n" "$raw_rec" | awk '
function hex2val(h, i, c, v, res) {
res = 0
h = toupper(h)
@@ -1291,15 +1389,21 @@ case "$cmd" in
if (n == 11) return "#"
return ""
}
/^[[:space:]]*([0-9A-Fa-f]{2}:)+[0-9A-Fa-f]{2}[[:space:]]*$/ {
/Read result:/ { in_res = 1; full = ""; next }
in_res && /^[[:space:]]*[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2})*:?[[:space:]]*$/ {
line = $0
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
n = split(line, hex, ":")
if (n < 14) next
sub(/:$/, "", line)
full = (full == "") ? line : (full ":" line)
}
END {
if (full == "") exit
n = split(full, hex, ":")
if (n < 14) exit
alpha_len = n - 14
bcd_len = hex2val(hex[alpha_len + 1])
ton_npi = hex2val(hex[alpha_len + 2])
if (bcd_len <= 0 || bcd_len > 11 || bcd_len == 255) next
if (bcd_len <= 0 || bcd_len > 11 || bcd_len == 255) exit
num = ""
if (int(ton_npi / 16) % 8 == 1) {
num = "+"
@@ -1318,7 +1422,7 @@ case "$cmd" in
if (d2 == "") break
num = num d2
}
if (num == "" || num == "+") next
if (num == "" || num == "+") exit
name = ""
for (i = 1; i <= alpha_len; i++) {
b = hex2val(hex[i])
@@ -1396,64 +1500,234 @@ case "$cmd" in
fi
fi
orig_gw_slot="$PRIMARY_GW_SLOT"
orig_gw_aid=""
if [ "$orig_gw_slot" = "1" ]; then
orig_gw_aid="$SLOT1_AID"
elif [ "$orig_gw_slot" = "2" ]; then
orig_gw_aid="$SLOT2_AID"
fi
switched_gw=0
restore_gw_slot_exp() {
if [ "$switched_gw" = "1" ]; then
$QMICLI_MODEM --uim-change-provisioning-session='activate=no,session-type=primary-gw-provisioning' >/dev/null 2>&1 || true
if [ -n "$orig_gw_slot" ] && [ "$orig_gw_slot" != "0" ] && [ -n "$orig_gw_aid" ]; then
$QMICLI_MODEM --uim-change-provisioning-session="slot=$orig_gw_slot,activate=yes,session-type=primary-gw-provisioning,aid=$orig_gw_aid" >/dev/null 2>&1 || true
fi
switched_gw=0
fi
}
trap restore_gw_slot_exp EXIT INT TERM
if command -v python3 >/dev/null 2>&1; then
exp_out="$(python3 - "$target_slot" "$src_tsv" << 'PY'
import socket, struct, select, sys
if [ "$PRIMARY_GW_SLOT" != "$target_slot" ] && [ -n "$target_aid" ]; then
if [ "$PRIMARY_GW_SLOT" != "0" ]; then
$QMICLI_MODEM --uim-change-provisioning-session='activate=no,session-type=primary-gw-provisioning' >/dev/null 2>&1 || true
slot = int(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1].isdigit() else 1
tsv_path = sys.argv[2]
rows = []
with open(tsv_path, "r", encoding="utf-8", errors="replace") as f:
for line in f:
line = line.rstrip("\r\n")
if "\t" not in line: continue
num, name = line.split("\t", 1)
num = "".join(c for c in num if c in "+0123456789*#")
name = name.strip()
if num:
rows.append((num, name or num))
if not rows:
print("ERROR: No contacts to export")
sys.exit(1)
AF_QIPCRTR = 42
QRTR_PORT_CTRL = 0xfffffffe
QRTR_TYPE_NEW_SERVER = 4
QRTR_TYPE_NEW_LOOKUP = 10
QMI_SERVICE_UIM = 11
ls = socket.socket(AF_QIPCRTR, socket.SOCK_DGRAM)
node, _ = ls.getsockname()
ls.sendto(struct.pack("<IIIII", QRTR_TYPE_NEW_LOOKUP, QMI_SERVICE_UIM, 0, 0, 0), (node, QRTR_PORT_CTRL))
uim_addr = None
while True:
r, _, _ = select.select([ls], [], [], 0.15)
if not r: break
data, _ = ls.recvfrom(256)
if len(data) >= 20:
cmd, srv, inst, srv_node, srv_port = struct.unpack("<IIIII", data[:20])
if cmd == QRTR_TYPE_NEW_SERVER:
if srv == QMI_SERVICE_UIM and srv_port != 0:
uim_addr = (srv_node, srv_port)
if srv == 0 and srv_node == 0 and srv_port == 0:
break
ls.close()
if not uim_addr:
print("ERROR: Modem UIM service unavailable")
sys.exit(1)
s = socket.socket(AF_QIPCRTR, socket.SOCK_DGRAM)
session_type = 7 if slot == 2 else 6
tlv_f = None
rec_size = 0
rec_count = 0
txn = 1
for stype in [session_type, 0]:
for fid, path in [(0x6F3A, [0x3F00, 0x7F10]), (0x4F3A, [0x3F00, 0x7F10, 0x5F3A])]:
pbytes = b"".join(struct.pack("<H", x) for x in path)
fval = struct.pack("<HB", fid, len(pbytes)) + pbytes
tf = struct.pack("<BH", 0x02, len(fval)) + fval
ts = struct.pack("<BHB", 0x01, 2, stype) + b"\x00"
tlvs = ts + tf
s.sendto(struct.pack("<BHHH", 0x00, txn, 0x0024, len(tlvs)) + tlvs, uim_addr)
txn += 1
r, _, _ = select.select([s], [], [], 0.15)
if not r: continue
resp, _ = s.recvfrom(512)
if len(resp) < 14 or resp[0] != 0x02 or resp[3] != 0x24: continue
pos = 7
ok = False
rs = rc = 0
while pos + 3 <= len(resp):
tt, tl = struct.unpack("<BH", resp[pos:pos+3])
pos += 3
if tt == 0x02 and tl >= 4:
res, err = struct.unpack("<HH", resp[pos:pos+4])
if res == 0 and err == 0: ok = True
elif tt == 0x11 and tl >= 9:
_, _, _, rs, rc = struct.unpack("<HHBHH", resp[pos:pos+9])
pos += tl
if ok and rs >= 14 and rc >= 1:
session_type = stype
tlv_f = tf
rec_size = rs
rec_count = min(rc, 500)
break
if tlv_f:
break
if not tlv_f:
s.close()
print(f"ERROR: SIM {slot} phonebook not ready")
sys.exit(1)
tlv_session = struct.pack("<BHB", 0x01, 2, session_type) + b"\x00"
bcd_map_str = "0123456789*#p"
existing_by_norm = {}
existing_by_sfx = {}
empty_slots = []
def sfx_key(norm):
d = "".join(c for c in norm if c.isdigit())
return d[-9:] if len(d) >= 9 else norm
for rnum in range(1, rec_count + 1):
tlv_rec = struct.pack("<BHHH", 0x03, 4, rnum, rec_size)
tlvs = tlv_session + tlv_f + tlv_rec
s.sendto(struct.pack("<BHHH", 0x00, txn, 0x0021, len(tlvs)) + tlvs, uim_addr)
txn += 1
r, _, _ = select.select([s], [], [], 0.15)
if not r: continue
resp, _ = s.recvfrom(512)
pos = 7
raw = None
ok = False
while pos + 3 <= len(resp):
tt, tl = struct.unpack("<BH", resp[pos:pos+3])
pos += 3
if tt == 0x02 and tl >= 4:
res, err = struct.unpack("<HH", resp[pos:pos+4])
if res == 0 and err == 0: ok = True
elif tt == 0x11 and tl >= 2:
clen = struct.unpack("<H", resp[pos:pos+2])[0]
raw = resp[pos+2:pos+2+clen]
pos += tl
if not (ok and raw and len(raw) >= 14):
continue
alpha_len = len(raw) - 14
bcd_len = raw[alpha_len]
ton_npi = raw[alpha_len + 1]
if bcd_len == 0 or bcd_len > 11 or bcd_len == 0xFF:
empty_slots.append(rnum)
continue
num = "+" if ((ton_npi >> 4) & 0x07) == 1 else ""
for b in raw[alpha_len + 2 : alpha_len + 2 + min(10, bcd_len - 1)]:
if b == 0xFF: break
lo = b & 0x0F
hi = (b >> 4) & 0x0F
if lo < len(bcd_map_str): num += bcd_map_str[lo]
else: break
if hi == 0x0F: break
if hi < len(bcd_map_str): num += bcd_map_str[hi]
else: break
if not num or num == "+":
empty_slots.append(rnum)
continue
existing_by_norm[num] = rnum
existing_by_sfx[sfx_key(num)] = rnum
def encode_adn(name, num, rsize):
alpha_len = rsize - 14
try:
nb = name.encode("ascii")[:alpha_len]
alpha = nb + b"\xFF" * (alpha_len - len(nb))
except UnicodeEncodeError:
u16 = name.encode("utf-16-be")[:max(0, (alpha_len - 1) // 2 * 2)]
alpha = b"\x80" + u16 + b"\xFF" * (alpha_len - 1 - len(u16))
ton_npi = 0x91 if num.startswith("+") else 0x81
digits = [c for c in num if c != "+"][:20]
if not digits: return None
bcd = bytearray([0xFF] * 10)
bmap = {str(i): i for i in range(10)}
bmap["*"] = 0x0A
bmap["#"] = 0x0B
for idx, ch in enumerate(digits):
nib = bmap.get(ch, 0x0F)
bi = idx // 2
if idx % 2 == 0:
bcd[bi] = (bcd[bi] & 0xF0) | nib
else:
bcd[bi] = (bcd[bi] & 0x0F) | (nib << 4)
bcd_len = 1 + (len(digits) + 1) // 2
return alpha + bytes([bcd_len, ton_npi]) + bytes(bcd) + b"\xFF\xFF"
exported = 0
empty_idx = 0
for num, name in rows:
trec = existing_by_norm.get(num) or existing_by_sfx.get(sfx_key(num))
if not trec:
if empty_idx < len(empty_slots):
trec = empty_slots[empty_idx]
empty_idx += 1
else:
break
rdata = encode_adn(name, num, rec_size)
if not rdata: continue
wval = struct.pack("<HH", trec, len(rdata)) + rdata
tlv_w = struct.pack("<BH", 0x03, len(wval)) + wval
tlvs = tlv_session + tlv_f + tlv_w
s.sendto(struct.pack("<BHHH", 0x00, txn, 0x0023, len(tlvs)) + tlvs, uim_addr)
txn += 1
r, _, _ = select.select([s], [], [], 0.2)
if not r: continue
resp, _ = s.recvfrom(512)
pos = 7
ok = False
while pos + 3 <= len(resp):
tt, tl = struct.unpack("<BH", resp[pos:pos+3])
pos += 3
if tt == 0x02 and tl >= 4:
res, err = struct.unpack("<HH", resp[pos:pos+4])
if res == 0 and err == 0: ok = True
pos += tl
if ok:
exported += 1
existing_by_norm[num] = trec
existing_by_sfx[sfx_key(num)] = trec
s.close()
if exported == 0 and empty_idx >= len(empty_slots):
print(f"ERROR: SIM {slot} phonebook is full")
sys.exit(1)
print(f"EXPORTED={exported}")
print(f"SLOT={slot}")
PY
)"
if printf "%s\n" "$exp_out" | grep -q "^EXPORTED="; then
printf "%s\n" "$exp_out"
exit 0
fi
if $QMICLI_MODEM --uim-change-provisioning-session="slot=$target_slot,activate=yes,session-type=primary-gw-provisioning,aid=$target_aid" >/dev/null 2>&1; then
switched_gw=1
sleep 1
if printf "%s\n" "$exp_out" | grep -q "^ERROR:"; then
printf "%s\n" "$exp_out"
exit 1
fi
fi
at_info="$("$0" at auto 'AT+CMEE=2;+CSCS="GSM";+CPBS="SM";+CPBR=?' 2>/dev/null || true)"
if printf "%s\n" "$at_info" | grep -qE "ERROR"; then
echo "ERROR: SIM $target_slot phonebook not ready"
exit 1
fi
tlen="$(printf "%s\n" "$at_info" | sed -n 's/.*+CPBR:[[:space:]]*([0-9]*-[0-9]*),[0-9]*,\([0-9]*\).*/\1/p' | head -n1)"
if [ -z "$tlen" ] || [ "$tlen" -le 0 ] 2>/dev/null; then
tlen=14
fi
exported=0
while IFS="$(printf '\t')" read -r raw_num raw_name; do
num="$(printf "%s" "$raw_num" | tr -cd '+0-9*#')"
name="$(printf "%s" "$raw_name" | tr -d '"\r\n\t\\' | cut -c1-"$tlen")"
[ -n "$num" ] || continue
[ -n "$name" ] || name="$(printf "%s" "$num" | cut -c1-"$tlen")"
ton=129
case "$num" in
+*) ton=145 ;;
esac
res="$("$0" at auto "AT+CPBW=,\"${num}\",${ton},\"${name}\"" 2>/dev/null || true)"
if printf "%s\n" "$res" | grep -q "OK"; then
exported=$((exported + 1))
fi
done < "$src_tsv"
echo "EXPORTED=${exported}"
echo "SLOT=${target_slot}"
exit 0
echo "ERROR: SIM $target_slot phonebook not ready"
exit 1
;;
*)